observability.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. "supervisor": "自主编排",
  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. GRAPH_MODULE_KEYS = {
  27. key: f"{OBAGENT_PROJECT}.{OBAGENT_AGENT}.{key}"
  28. for key in ("supervisor", "search", "evidence", "evaluator")
  29. }
  30. GRAPH_TITLES = {
  31. key: MODULE_TITLES[key]
  32. for key in ("supervisor", "search", "evidence", "evaluator")
  33. }
  34. def graph_spec_for(app) -> dict[str, Any]:
  35. """Build the visualization spec from the compiled LangGraph itself."""
  36. try:
  37. from obagent_sdk.integrations.langgraph import graph_spec
  38. return graph_spec(
  39. app,
  40. module_keys=GRAPH_MODULE_KEYS,
  41. titles=GRAPH_TITLES,
  42. )
  43. except Exception:
  44. return GRAPH_SPEC
  45. _configured = False
  46. @dataclass(frozen=True)
  47. class InputSlot:
  48. title: str
  49. value: str
  50. key: str
  51. source: str
  52. optional: bool = True
  53. class NullRunHandle:
  54. run_uid: str | None = None
  55. def finish(self, final_output: Any = None) -> None:
  56. del final_output
  57. class NullModuleHandle:
  58. def declare(self, *, fallback: str, **_kwargs) -> str:
  59. return fallback
  60. def record_react(self, **_kwargs) -> None:
  61. return None
  62. def set_output(self, _output: Any, *, ok: bool = True) -> None:
  63. del ok
  64. class NullObserver:
  65. """Test-only observer. Production construction uses :class:`ObagentObserver`."""
  66. @contextmanager
  67. def run(self, **_kwargs) -> Iterator[NullRunHandle]:
  68. yield NullRunHandle()
  69. @contextmanager
  70. def round(self, **_kwargs) -> Iterator[NullModuleHandle]:
  71. yield NullModuleHandle()
  72. @contextmanager
  73. def node(self, **_kwargs) -> Iterator[NullModuleHandle]:
  74. yield NullModuleHandle()
  75. def console_endpoint() -> str:
  76. return (os.getenv("OBAGENT_ENDPOINT") or DEFAULT_ENDPOINT).strip().rstrip("/")
  77. def run_url(run_uid: str) -> str:
  78. return f"{console_endpoint()}/#client_uid={run_uid}"
  79. def ensure_configured() -> None:
  80. global _configured
  81. if _configured:
  82. return
  83. from obagent_sdk import configure
  84. kwargs: dict[str, Any] = {
  85. "endpoint": console_endpoint(),
  86. "project": OBAGENT_PROJECT,
  87. "timeout": float(os.getenv("OBAGENT_TIMEOUT", "120")),
  88. "timeout_per_op": float(os.getenv("OBAGENT_TIMEOUT_PER_OP", "1.0")),
  89. }
  90. api_key = os.getenv("OBAGENT_API_KEY", "").strip()
  91. if api_key:
  92. kwargs["api_key"] = api_key
  93. wal_dir = os.getenv("OBAGENT_WAL_DIR", "").strip()
  94. if wal_dir:
  95. kwargs["wal_dir"] = wal_dir
  96. enabled = os.getenv("OBAGENT_ENABLED", "1").strip().lower()
  97. if enabled in {"0", "false", "no", "off"}:
  98. kwargs["enabled"] = False
  99. configure(**kwargs)
  100. _configured = True
  101. def _blocks(slots: tuple[InputSlot, ...]):
  102. from obagent_sdk.observe import InputBlock
  103. return [
  104. InputBlock(
  105. slot.title,
  106. slot.value,
  107. key=slot.key,
  108. source=slot.source,
  109. optional=slot.optional,
  110. )
  111. for slot in slots
  112. ]
  113. class _ModuleHandle:
  114. def __init__(self, ctx) -> None:
  115. self.ctx = ctx
  116. def declare(
  117. self,
  118. *,
  119. fallback: str,
  120. system_prompt: str,
  121. slots: tuple[InputSlot, ...],
  122. tools: tuple,
  123. model: str,
  124. refs: dict[str, str] | None = None,
  125. ) -> str:
  126. del fallback, refs
  127. return self.ctx.declare(
  128. system_prompt=system_prompt,
  129. blocks=_blocks(slots),
  130. tools=list(tools),
  131. model=model,
  132. )
  133. def record_react(self, *, output: dict[str, Any], ok: bool) -> None:
  134. # One stable code stage per agent module; the payload holds the complete
  135. # custom ReAct message chain because this project does not use LangChain hooks.
  136. self.ctx.record_stage(
  137. "react",
  138. fn=_react_stage_identity,
  139. title="ReAct 运行",
  140. output=output,
  141. ok=ok,
  142. )
  143. def set_output(self, output: Any, *, ok: bool = True) -> None:
  144. self.ctx.set_output(output, ok=ok)
  145. def _react_stage_identity(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
  146. """Stable source identity for the custom ReAct runtime stage."""
  147. return messages
  148. class ObagentObserver:
  149. @contextmanager
  150. def run(
  151. self,
  152. *,
  153. run_id: str,
  154. demand_word: str,
  155. model: str,
  156. models_by_role: dict[str, str],
  157. ):
  158. ensure_configured()
  159. from obagent_sdk import observe
  160. with observe.run(
  161. agent=OBAGENT_AGENT,
  162. objective=f"寻找视频 · {demand_word} · run_id={run_id}",
  163. project=OBAGENT_PROJECT,
  164. model_name=model,
  165. payload={"run_id": run_id, "demand_word": demand_word,
  166. "models_by_role": models_by_role},
  167. tags={"engine": "staged-react", "version": "v2"},
  168. meta={"run_name": f"寻找 Agent v2 · {demand_word}",
  169. "run_id": run_id, "models_by_role": models_by_role},
  170. round_anchor=OBAGENT_ROUND_ANCHOR,
  171. ) as handle:
  172. yield handle
  173. @contextmanager
  174. def round(self, *, round_index: int, spec: dict[str, Any] | None = None):
  175. from obagent_sdk import observe
  176. with observe.module(
  177. "graph",
  178. kind="workflow",
  179. title=f"寻找 Agent · 第 {round_index} 轮",
  180. module_key="graph",
  181. spec=spec or GRAPH_SPEC,
  182. ) as ctx:
  183. from obagent_sdk.observe import InputBlock
  184. ctx.declare(blocks=[InputBlock(
  185. "当前轮次", str(round_index), key="round_index",
  186. source="FindAgentV2.begin_round", optional=False,
  187. )])
  188. yield _ModuleHandle(ctx)
  189. @contextmanager
  190. def node(self, *, node: str, branch_key: str = ""):
  191. from obagent_sdk import observe
  192. with observe.module(
  193. node,
  194. kind="agent",
  195. title=MODULE_TITLES[node],
  196. module_key=node,
  197. branch_key=branch_key,
  198. ) as ctx:
  199. yield _ModuleHandle(ctx)