Просмотр исходного кода

可观测性:接入 Global Data 执行链并锁定 SDK 依赖

在现有 with_node_metrics 包装器中启动和结束节点观测,在本地 Agent metrics 记录完成后发送经过脱敏的模型与工具调用摘要。

在 run_global_data 入口按 fresh、resume 和终态 noop 模式包裹完整 Graph 生命周期,同时保持原有 registry、checkpoint、正式终态验证和 invocation metrics 顺序。

增加 obagent-sdk 0.5 系列依赖和公司 Python 源锁定,确保安装结果可复现。

观测写入继续是非阻塞辅助能力,任何 SDK 异常都不能使正确的业务 Delivery 失败。
SamLee 2 дней назад
Родитель
Сommit
d620f0aebd
5 измененных файлов с 95 добавлено и 27 удалено
  1. 10 0
      production_build_agents/run/graph_metrics.py
  2. 16 1
      production_build_agents/run/metrics.py
  3. 9 0
      pyproject.toml
  4. 46 26
      run_global_data.py
  5. 14 0
      uv.lock

+ 10 - 0
production_build_agents/run/graph_metrics.py

@@ -6,6 +6,7 @@ from pathlib import Path
 from time import perf_counter
 from typing import Any, Callable, Mapping
 
+from ..observability import start_graph_node_observation
 from .metrics import record_phase_duration
 
 
@@ -18,10 +19,19 @@ def with_node_metrics(
     def invoke(state: Mapping[str, Any]) -> dict[str, Any]:
         started = perf_counter()
         outcome = "ERROR"
+        observation = start_graph_node_observation(
+            phase,
+            state,
+            source_fn=node,
+        )
         try:
             update = node(state)
             outcome = str(update.get("status") or "RUNNING")
+            observation.finish(update)
             return update
+        except BaseException as exc:
+            observation.finish(None, exception=exc)
+            raise
         finally:
             if state.get("output_dir") and state.get("run_id"):
                 try:

+ 16 - 1
production_build_agents/run/metrics.py

@@ -10,6 +10,7 @@ from typing import Any, Iterable, Mapping
 
 from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
 
+from ..observability import record_sanitized_agent_messages
 from .layout import run_metrics_path
 from .records import replace_json
 
@@ -233,6 +234,7 @@ def record_agent_messages(
     message_list = list(messages)
     metrics = _load(run_dir, run_id)
     seen = set(metrics.get("_seen_events", []))
+    new_message_indexes: set[int] = set()
     corrections = metrics.setdefault("corrections", [])
     role_metrics = metrics["models"].setdefault(
         role,
@@ -264,6 +266,7 @@ def record_agent_messages(
             )
             if event in seen:
                 continue
+            new_message_indexes.add(index)
             name = _model_name(model, message)
             if name not in role_metrics["models"]:
                 role_metrics["models"].append(name)
@@ -292,6 +295,7 @@ def record_agent_messages(
             )
             if event in seen:
                 continue
+            new_message_indexes.add(index)
             try:
                 result = json.loads(message.content)
             except (json.JSONDecodeError, TypeError):
@@ -343,7 +347,18 @@ def record_agent_messages(
 
     metrics["_seen_events"] = sorted(seen)
     _refresh_model_totals(metrics)
-    return _save(run_dir, metrics)
+    path = _save(run_dir, metrics)
+    try:
+        record_sanitized_agent_messages(
+            role=role,
+            agent_run_id=agent_run_id,
+            model=model,
+            messages=message_list,
+            message_indexes=new_message_indexes,
+        )
+    except Exception:
+        pass
+    return path
 
 
 def record_phase_duration(

+ 9 - 0
pyproject.toml

@@ -9,6 +9,7 @@ dependencies = [
     "langchain-openai>=1.1,<2",
     "langgraph>=1.2,<1.3",
     "langgraph-checkpoint-sqlite>=3.1,<4",
+    "obagent-sdk>=0.5.2,<0.6",
     "opencv-python-headless>=4.11,<5",
     "oss2>=2.19,<3",
     "pillow>=11,<12",
@@ -34,6 +35,14 @@ production_build_agents = [
     "tools/models/*.txt",
 ]
 
+[[tool.uv.index]]
+name = "aiddit"
+url = "https://pypi.aiddit.com/repository/pypi-group/simple"
+explicit = true
+
+[tool.uv.sources]
+obagent-sdk = { index = "aiddit" }
+
 [dependency-groups]
 dev = [
     "langgraph-cli[inmem]>=0.4.31",

+ 46 - 26
run_global_data.py

@@ -15,6 +15,7 @@ from production_build_agents.global_data.graph import create_global_data_graph
 from production_build_agents.global_data_stage_finalization import (
     validate_completed_run,
 )
+from production_build_agents.observability import GlobalDataObservation
 from production_build_agents.run.langgraph_checkpointer import (
     create_sqlite_checkpointer,
 )
@@ -130,34 +131,53 @@ def run_global_data(
                 validator_model=validator_model,
             )
             saved = dict(graph.get_state(config).values)
-            if saved:
-                validate_resume_identity(
-                    saved,
-                    thread_id=thread_id,
-                    protocol_version=PROTOCOL_VERSION,
-                    input_path=source,
-                    input_sha256=input_sha256,
-                    run_dir=run_dir,
+            execution_mode = (
+                (
+                    "completed_noop"
+                    if saved.get("status") == "COMPLETED"
+                    else "failed_noop"
                 )
-                if saved.get("status") in TERMINAL_STATUSES:
-                    if saved.get("status") == "COMPLETED":
-                        validate_completed_run(saved)
-                    return saved
-                graph_invoked = True
-                result = dict(graph.invoke(None, config))
-            else:
-                graph_invoked = True
-                result = dict(
-                    graph.invoke(
-                        _initial_state(
-                            thread_id=thread_id,
-                            input_path=source,
-                            input_sha256=input_sha256,
-                            run_dir=run_dir,
-                        ),
-                        config,
+                if saved and saved.get("status") in TERMINAL_STATUSES
+                else ("resume" if saved else "fresh")
+            )
+            with GlobalDataObservation(
+                project_root=Path(__file__).resolve().parent,
+                thread_id=thread_id,
+                input_sha256=input_sha256,
+                protocol_version=PROTOCOL_VERSION,
+                execution_mode=execution_mode,
+                graph=graph,
+            ) as observation:
+                if saved:
+                    validate_resume_identity(
+                        saved,
+                        thread_id=thread_id,
+                        protocol_version=PROTOCOL_VERSION,
+                        input_path=source,
+                        input_sha256=input_sha256,
+                        run_dir=run_dir,
                     )
-                )
+                    if saved.get("status") in TERMINAL_STATUSES:
+                        if saved.get("status") == "COMPLETED":
+                            validate_completed_run(saved)
+                        observation.finish(saved, graph_invoked=False)
+                        return saved
+                    graph_invoked = True
+                    result = dict(graph.invoke(None, config))
+                else:
+                    graph_invoked = True
+                    result = dict(
+                        graph.invoke(
+                            _initial_state(
+                                thread_id=thread_id,
+                                input_path=source,
+                                input_sha256=input_sha256,
+                                run_dir=run_dir,
+                            ),
+                            config,
+                        )
+                    )
+                observation.finish(result, graph_invoked=graph_invoked)
         if graph_invoked:
             try:
                 record_run_invocation(

+ 14 - 0
uv.lock

@@ -738,6 +738,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" },
 ]
 
+[[package]]
+name = "obagent-sdk"
+version = "0.5.2"
+source = { registry = "https://pypi.aiddit.com/repository/pypi-group/simple" }
+dependencies = [
+    { name = "langchain-core" },
+]
+sdist = { url = "https://pypi.aiddit.com/repository/pypi-group/packages/obagent-sdk/0.5.2/obagent-sdk-0.5.2.tar.gz", hash = "sha256:b94ced06884f40de76f2faa1527fa26ca4de2d9d35bf446188c3d5ebbad29923" }
+wheels = [
+    { url = "https://pypi.aiddit.com/repository/pypi-group/packages/obagent-sdk/0.5.2/obagent_sdk-0.5.2-py3-none-any.whl", hash = "sha256:942d501b0426beebb4bf741a875573314f7a7d450f8bc2196076fe7f32c192b6" },
+]
+
 [[package]]
 name = "openai"
 version = "2.48.0"
@@ -981,6 +993,7 @@ dependencies = [
     { name = "langchain-openai" },
     { name = "langgraph" },
     { name = "langgraph-checkpoint-sqlite" },
+    { name = "obagent-sdk" },
     { name = "opencv-python-headless" },
     { name = "oss2" },
     { name = "pillow" },
@@ -1003,6 +1016,7 @@ requires-dist = [
     { name = "langchain-openai", specifier = ">=1.1,<2" },
     { name = "langgraph", specifier = ">=1.2,<1.3" },
     { name = "langgraph-checkpoint-sqlite", specifier = ">=3.1,<4" },
+    { name = "obagent-sdk", specifier = ">=0.5.2,<0.6", index = "https://pypi.aiddit.com/repository/pypi-group/simple" },
     { name = "opencv-python-headless", specifier = ">=4.11,<5" },
     { name = "oss2", specifier = ">=2.19,<3" },
     { name = "pillow", specifier = ">=11,<12" },