Files
BrunoGaoSZandClaude Sonnet 4.6 2360e788d3 feat: implement P10 — SOP profiles, integration tests, smoke gate
TASK 1 — Profile Schema (core/profile/schema.py)
已存在:EvidenceThresholds / ModelRouting / WorkflowStep / ApprovalRule /
Permissions / CachePolicy / Profile,全部 frozen=True,含 tool-reference 验证器。

TASK 2 — 三个行业 SOP Profile YAML
- profiles/presales/profile.yaml        企业预售方案生成
- profiles/security_analysis/profile.yaml  安全事件分析(host_isolation 需审批)
- profiles/knowledge_qa/profile.yaml    知识问答(严格模式,阈值全 0.90)

TASK 3 — loader / registry / 测试
- core/profile/loader.py + registry.py(线程安全单例 + 热更新)
- tests/unit/test_profiles.py(19 个用例)
- tests/unit/test_scorer.py(12 个用例,覆盖 EvidenceScorer 全路径)

TASK 4 — 端到端集成测试 + 成本追踪
- tests/integration/test_full_pipeline.py  7 个 mock-based 测试
- tests/integration/test_cost_tracking.py  3 个成本测试
- scripts/smoke_test.sh 更新为与最终门禁一致的 ruff 规则

Bug fixes(使全部测试通过):
- LangGraph: ask_node 显式初始化 error/pending_approval,确保 ainvoke()
  结果 dict 中始终包含这两个字段
- GraphInterrupt 不再被 act_node 的 except Exception 吞掉
- Citation.model_dump(mode="json") 修复 Decimal JSON 序列化问题
- act_node 直接调用 audit_logger.write_tool_call(集成测试 mock executor 时仍可审计)
- 修复 mypy 错误:config.py 重复字段、encryptor __init__ 返回值、
  registry 属性声明、sanitizer.py 类型注解
- 修复 47 处 ruff lint(F401/F841/E501/I001)
- 添加 pytest-timeout>=2.3.0 依赖

最终门禁全部通过:
  ruff E,F,I 0 errors | mypy 0 errors
  143 unit tests, 80.38% coverage | 11 integration tests

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-02-27 09:21:43 +01:00

110 lines
3.0 KiB
Python

"""
LangGraph state graph construction and compilation.
Builds the agent execution flow with conditional edges:
- ask → plan (or END if clarification/error)
- plan → act (or END if error)
- act → verify (or END if error/approval pending)
- verify → report (or END if error)
- report → END
Uses PostgresSaver for production, MemorySaver for tests.
"""
import logging
from typing import Any
from langgraph.graph import END, StateGraph
from core.engine.nodes import act_node, ask_node, plan_node, report_node, verify_node
from core.engine.state import AgentState
logger = logging.getLogger(__name__)
def build_agent_graph(checkpointer: Any = None) -> Any:
"""
Build and compile the agent state graph.
Args:
checkpointer: Optional checkpointer (PostgresSaver or MemorySaver)
Returns:
Compiled StateGraph
"""
graph = StateGraph(AgentState)
# Add nodes
graph.add_node("ask", ask_node)
graph.add_node("plan", plan_node)
graph.add_node("act", act_node)
graph.add_node("verify", verify_node)
graph.add_node("report", report_node)
# Set entry point
graph.set_entry_point("ask")
# Conditional edges
# ask → plan or END (clarification/error)
graph.add_conditional_edges(
"ask",
lambda s: END if s.is_clarification_needed() or s.has_error() else "plan",
)
# plan → act or END (error)
graph.add_conditional_edges(
"plan",
lambda s: END if s.has_error() else "act",
)
# act → verify or END (error)
# Note: approval pending is handled by interrupt_after, not routing
graph.add_conditional_edges(
"act",
lambda s: END if s.has_error() else "verify",
)
# verify → report or END (error)
graph.add_conditional_edges(
"verify",
lambda s: END if s.has_error() else "report",
)
# report → END
graph.add_edge("report", END)
# Compile with checkpointer; approval interrupts are driven by interrupt()
# inside act_node rather than the interrupt_after mechanism.
return graph.compile(checkpointer=checkpointer)
def create_agent_graph_with_postgres(database_url: str) -> Any:
"""
Create agent graph with PostgreSQL checkpointer for production.
Args:
database_url: PostgreSQL connection string
Returns:
Compiled StateGraph with PostgresSaver
"""
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
checkpointer = AsyncPostgresSaver.from_conn_string(database_url)
logger.info("Created agent graph with PostgresSaver")
return build_agent_graph(checkpointer)
def create_agent_graph_for_tests() -> Any:
"""
Create agent graph with in-memory checkpointer for tests.
Returns:
Compiled StateGraph with MemorySaver
"""
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
logger.info("Created agent graph with MemorySaver (test mode)")
return build_agent_graph(checkpointer)