observability.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. """obagent is the only visualization backend for find_agent_v2."""
  2. from __future__ import annotations
  3. import os
  4. from contextlib import contextmanager
  5. from dataclasses import dataclass
  6. from typing import Any, Iterator
  7. OBAGENT_PROJECT = "find_agent_v2"
  8. OBAGENT_AGENT = "find_agent_v2"
  9. OBAGENT_ROUND_ANCHOR = {"in": "run", "on": ["graph"]}
  10. DEFAULT_ENDPOINT = "http://ob.aiddit.com"
  11. MODULE_TITLES = {
  12. "planner": "搜索规划",
  13. "search": "候选搜索",
  14. "evidence": "证据补全",
  15. "evaluator": "评估与分池",
  16. "report": "结果报告",
  17. }
  18. GRAPH_SPEC = {
  19. "nodes": [
  20. {"key": key, "title": title,
  21. "module_key": f"{OBAGENT_PROJECT}.{OBAGENT_AGENT}.{key}"}
  22. for key, title in MODULE_TITLES.items()
  23. if key != "report"
  24. ]
  25. }
  26. _configured = False
  27. @dataclass(frozen=True)
  28. class InputSlot:
  29. title: str
  30. value: str
  31. key: str
  32. source: str
  33. optional: bool = True
  34. class NullRunHandle:
  35. run_uid: str | None = None
  36. def finish(self, final_output: Any = None) -> None:
  37. del final_output
  38. class NullModuleHandle:
  39. def declare(self, *, fallback: str, **_kwargs) -> str:
  40. return fallback
  41. def record_react(self, **_kwargs) -> None:
  42. return None
  43. def set_output(self, _output: Any, *, ok: bool = True) -> None:
  44. del ok
  45. class NullObserver:
  46. """Test-only observer. Production construction uses :class:`ObagentObserver`."""
  47. @contextmanager
  48. def run(self, **_kwargs) -> Iterator[NullRunHandle]:
  49. yield NullRunHandle()
  50. @contextmanager
  51. def round(self, **_kwargs) -> Iterator[NullModuleHandle]:
  52. yield NullModuleHandle()
  53. @contextmanager
  54. def node(self, **_kwargs) -> Iterator[NullModuleHandle]:
  55. yield NullModuleHandle()
  56. def console_endpoint() -> str:
  57. return (os.getenv("OBAGENT_ENDPOINT") or DEFAULT_ENDPOINT).strip().rstrip("/")
  58. def run_url(run_uid: str) -> str:
  59. return f"{console_endpoint()}/#client_uid={run_uid}"
  60. def ensure_configured() -> None:
  61. global _configured
  62. if _configured:
  63. return
  64. from obagent_sdk import configure
  65. kwargs: dict[str, Any] = {
  66. "endpoint": console_endpoint(),
  67. "project": OBAGENT_PROJECT,
  68. "timeout": float(os.getenv("OBAGENT_TIMEOUT", "120")),
  69. "timeout_per_op": float(os.getenv("OBAGENT_TIMEOUT_PER_OP", "1.0")),
  70. }
  71. api_key = os.getenv("OBAGENT_API_KEY", "").strip()
  72. if api_key:
  73. kwargs["api_key"] = api_key
  74. wal_dir = os.getenv("OBAGENT_WAL_DIR", "").strip()
  75. if wal_dir:
  76. kwargs["wal_dir"] = wal_dir
  77. enabled = os.getenv("OBAGENT_ENABLED", "1").strip().lower()
  78. if enabled in {"0", "false", "no", "off"}:
  79. kwargs["enabled"] = False
  80. configure(**kwargs)
  81. _configured = True
  82. def _blocks(slots: tuple[InputSlot, ...]):
  83. from obagent_sdk.observe import InputBlock
  84. return [
  85. InputBlock(
  86. slot.title,
  87. slot.value,
  88. key=slot.key,
  89. source=slot.source,
  90. optional=slot.optional,
  91. )
  92. for slot in slots
  93. ]
  94. class _ModuleHandle:
  95. def __init__(self, ctx) -> None:
  96. self.ctx = ctx
  97. def declare(
  98. self,
  99. *,
  100. fallback: str,
  101. system_prompt: str,
  102. slots: tuple[InputSlot, ...],
  103. tools: tuple,
  104. model: str,
  105. ) -> str:
  106. del fallback
  107. return self.ctx.declare(
  108. system_prompt=system_prompt,
  109. blocks=_blocks(slots),
  110. tools=list(tools),
  111. model=model,
  112. )
  113. def record_react(self, *, output: dict[str, Any], ok: bool) -> None:
  114. # One stable code stage per agent module; the payload holds the complete
  115. # custom ReAct message chain because this project does not use LangChain hooks.
  116. self.ctx.record_stage(
  117. "react",
  118. fn=_react_stage_identity,
  119. title="ReAct 运行",
  120. output=output,
  121. ok=ok,
  122. )
  123. def set_output(self, output: Any, *, ok: bool = True) -> None:
  124. self.ctx.set_output(output, ok=ok)
  125. def _react_stage_identity(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
  126. """Stable source identity for the custom ReAct runtime stage."""
  127. return messages
  128. class ObagentObserver:
  129. @contextmanager
  130. def run(
  131. self,
  132. *,
  133. run_id: str,
  134. demand_word: str,
  135. model: str,
  136. models_by_role: dict[str, str],
  137. ):
  138. ensure_configured()
  139. from obagent_sdk import observe
  140. with observe.run(
  141. agent=OBAGENT_AGENT,
  142. objective=f"寻找视频 · {demand_word} · run_id={run_id}",
  143. project=OBAGENT_PROJECT,
  144. model_name=model,
  145. payload={"run_id": run_id, "demand_word": demand_word,
  146. "models_by_role": models_by_role},
  147. tags={"engine": "staged-react", "version": "v2"},
  148. meta={"run_name": f"寻找 Agent v2 · {demand_word}",
  149. "run_id": run_id, "models_by_role": models_by_role},
  150. round_anchor=OBAGENT_ROUND_ANCHOR,
  151. ) as handle:
  152. yield handle
  153. @contextmanager
  154. def round(self, *, round_index: int):
  155. from obagent_sdk import observe
  156. with observe.module(
  157. "graph",
  158. kind="workflow",
  159. title=f"寻找 Agent · 第 {round_index} 轮",
  160. module_key="graph",
  161. spec=GRAPH_SPEC,
  162. ) as ctx:
  163. from obagent_sdk.observe import InputBlock
  164. ctx.declare(blocks=[InputBlock(
  165. "当前轮次", str(round_index), key="round_index",
  166. source="FindAgentV2.begin_round", optional=False,
  167. )])
  168. yield _ModuleHandle(ctx)
  169. @contextmanager
  170. def node(self, *, node: str):
  171. from obagent_sdk import observe
  172. with observe.module(
  173. node,
  174. kind="agent",
  175. title=MODULE_TITLES[node],
  176. module_key=node,
  177. ) as ctx:
  178. yield _ModuleHandle(ctx)