refactor(agent): inject deps via context_schema, LLM-driven photo/confirm nodes

- graph.py: LangGraph-native DI - StateGraph(context_schema=GraphDeps),
  nodes take (state, runtime: Runtime[GraphDeps]); build_graph() decoupled
  from LLM; deleted scaffold _event_reply/_fake_llm_propose/stub nodes
- photo.py: model proposes stage+body via per-mode prompts (direct/addon);
  validator enforces clientMode exits; deterministic event forcing removed
- confirm.py: single runtime for single/dual confirm, extraction call
  isolated per person_index
- llm.py: workflow_fake_llm() (offline workflow-faithful responder,
  history-aware) for smoke/tests
- backend: LangGraphBackend(graph, llm) passes context per call
- tests: invoke helpers inject GraphDeps; illegal-proposal test asserts
  error + state untouched via second well-behaved fake

Verified: 143 passed; smoke turns 1/2/1 with original welcome text
This commit is contained in:
Eric Wang
2026-10-21 15:11:16 +08:00
parent a5407e03b2
commit 6d0a2169c8
12 changed files with 794 additions and 187 deletions
+12 -4
View File
@@ -14,18 +14,21 @@
from collections.abc import AsyncIterator
from ..agent.graph import build_graph
from ..agent.graph import GraphDeps, build_graph, workflow_fake_llm
from .chat import ChatInput, ChatResult, ChatStreamEvent, ChatBackend, TextDelta
class LangGraphBackend(ChatBackend):
"""Adapt a compiled LangGraph to the neutral chat contract."""
def __init__(self, graph=None):
def __init__(self, graph=None, llm=None):
"""参数:
graph: 编译后的图;缺省调用 ``build_graph()``(内存 checkpoint)。
graph: 编译后的图;缺省调用 ``build_graph()``。
llm: LLM 服务(运行时依赖);缺省为离线 workflow fake。
生产环境由 lifespan 注入 ``build_llm_service()`` 的实例。
"""
self._graph = graph if graph is not None else build_graph()
self._llm = llm if llm is not None else workflow_fake_llm()
async def complete(self, chat_input: ChatInput) -> ChatResult:
"""非流式:执行一轮图并翻译结果。
@@ -40,8 +43,13 @@ class LangGraphBackend(ChatBackend):
(作业 6 起实现)。
"""
result = await self._graph.ainvoke(
{"session_id": chat_input.session_id, "input_text": chat_input.text},
{
"session_id": chat_input.session_id,
"input_text": chat_input.text,
"client_mode": chat_input.client_mode,
},
config={"configurable": {"thread_id": chat_input.session_id}},
context=GraphDeps(llm=self._llm),
)
return ChatResult(
content = result["response_text"],