feat: 添加 Action 协议

This commit is contained in:
2026-09-23 15:33:28 +08:00
parent f0d33ff058
commit 5ef873bfe9
14 changed files with 284 additions and 25 deletions
+5 -1
View File
@@ -12,6 +12,7 @@ from app.processor.nodes.intent_node import IntentNode
from app.processor.nodes.requirement_node import RequirementNode
from app.processor.nodes.wait_for_user_node import WaitForUserNode
from app.processor.nodes.qa_node import QANode
from app.processor.nodes.scope_node import ScopeNode
from app.processor.state import QueryState
@@ -39,6 +40,7 @@ def build_graph(checkpointer=None):
"clarify_node": ClarifyNode(),
"wait_for_user_node": WaitForUserNode(),
"qa_node": QANode(),
"scope_node": ScopeNode(),
}
# 注册节点,异步节点使用 BaseNode 的异步入口
@@ -65,10 +67,11 @@ def build_graph(checkpointer=None):
workflow.add_conditional_edges(
source="requirement_node",
path=route_after_requirement,
path_map={True: "clarify_node", False: END},
path_map={True: "clarify_node", False: "scope_node"},
)
workflow.add_edge("clarify_node", "wait_for_user_node")
workflow.add_edge("qa_node", END)
workflow.add_edge("scope_node", END)
workflow.add_conditional_edges(
"wait_for_user_node",
route_after_wait,
@@ -81,6 +84,7 @@ def build_graph(checkpointer=None):
("app.schemas.input_context", "InputContext"),
("app.schemas.decision", "DecisionResponse"),
("app.schemas.requirement", "Requirement"),
("app.schemas.change_scope", "ChangeScope"),
("app.schemas.clarification", "Clarification"),
("app.schemas.clarification_reply", "ClarificationReply"),
]
+1
View File
@@ -43,6 +43,7 @@ class InputNode(BaseNode):
"intent_decision": None,
"intent": None,
"requirement": None,
"change_scope": None,
"clarification": None,
"answer": None,
"status": "running",
+3 -6
View File
@@ -21,20 +21,17 @@ class RequirementNode(BaseNode):
"intent": intent,
"input_context": context.model_dump(mode="json", exclude_unset=True),
}
llm = get_llm(Requirement)
result = await llm.ainvoke(
llm_client = get_llm(Requirement)
llm_res = await llm_client.ainvoke(
[
SystemMessage(content=REQUIREMENT_SYSTEM_PROMPT),
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
)
print(f"LLM 返回的需求: {result}")
print("=" * 66)
# 沿用门控后的意图,合并后重新校验字段一致性
requirement = Requirement.model_validate(
{**result.model_dump(), "intent": intent}
{**llm_res.model_dump(), "intent": intent}
)
node_ids = {node.id for node in context.screen.nodes}
for change in requirement.changes:
+109
View File
@@ -0,0 +1,109 @@
"""判断需求涉及的范围、规划和资源需求"""
import json
from langchain_core.messages import HumanMessage, SystemMessage
from app.clients.llm import get_llm
from app.processor.base import BaseNode
from app.processor.state import QueryState
from app.prompt.scope import SCOPE_SYSTEM_PROMPT
from app.schemas.change_scope import ChangeScope
class ScopeNode(BaseNode):
name = "scope_node"
async def process(self, state: QueryState) -> QueryState:
requirement = state["requirement"]
if requirement.needs_clarification or requirement.intent not in (
"create_screen",
"modify_screen",
):
raise ValueError("ScopeNode 只处理信息完整的新建或修改需求")
context = state["input_context"]
llm_client = get_llm(ChangeScope)
scope = await llm_client.ainvoke(
[
SystemMessage(content=SCOPE_SYSTEM_PROMPT),
HumanMessage(
content=json.dumps(
{
"requirement": requirement.model_dump(mode="json"),
"input_context": context.model_dump(
mode="json", exclude_unset=True
),
},
ensure_ascii=False,
)
),
]
)
existing_ids = {node.id for node in context.screen.nodes}
if unknown_ids := set(scope.target_ids) - existing_ids:
raise ValueError(f"范围引用了不存在的节点: {sorted(unknown_ids)}")
required_ids = {
node_id for change in requirement.changes for node_id in change.target_ids
}
if missing_ids := required_ids - set(scope.target_ids):
raise ValueError(f"范围遗漏了需求中的节点: {sorted(missing_ids)}")
return {**state, "change_scope": scope}
if __name__ == "__main__":
import asyncio
from pydantic import BaseModel
from app.processor.nodes.input_node import InputNode
from app.schemas.requirement import Requirement
async def main():
# 直接提供已理解的需求,单独测试范围判断,仅需 LLM 配置
state: QueryState = {
"request": {
"prompt": "把销售额统计向右移动 50px,其他不变",
"screen": {
"canvas": {"width": 1920, "height": 1080},
"nodes": [
{
"id": "sales-chart",
"type": "bar-chart",
"name": "销售额统计",
"layout": {"x": 26, "y": 34.8, "width": 400, "height": 260},
}
],
},
}
}
state = InputNode().process(state)
state["requirement"] = Requirement(
intent="modify_screen",
goal="销售额统计向右移动 50px",
changes=[
{
"target_scope": "nodes",
"target_ids": ["sales-chart"],
"operation": "move",
"parameters": {"delta_x": 50},
}
],
needs_clarification=False,
constraints=["其他不变"],
)
node = ScopeNode()
final_state = await node.process(state)
print(
json.dumps(
{
key: value.model_dump(mode="json")
if isinstance(value, BaseModel)
else value
for key, value in final_state.items()
},
ensure_ascii=False,
indent=2,
)
)
asyncio.run(main())
+16 -14
View File
@@ -1,22 +1,24 @@
from typing import Literal, NotRequired, TypedDict
from app.schemas.base import JsonObject
from app.schemas.input_context import InputContext
from app.schemas.decision import DecisionResponse
from app.schemas.requirement import Intent, Requirement
from app.schemas.change_scope import ChangeScope
from app.schemas.clarification import Clarification
from app.schemas.clarification_reply import ClarificationReply
from app.schemas.change_scope import ChangeScope
from app.schemas.decision import DecisionResponse
from app.schemas.input_context import InputContext
from app.schemas.requirement import Intent, Requirement
class QueryState(TypedDict):
request: JsonObject
input_context: NotRequired[InputContext]
intent_decision: NotRequired[DecisionResponse | None]
intent: NotRequired[Intent | None]
requirement: NotRequired[Requirement | None]
change_scope: NotRequired[ChangeScope | None]
clarification: NotRequired[Clarification | None]
clarification_reply: NotRequired[ClarificationReply | None]
answer: NotRequired[str | None]
status: NotRequired[Literal["running", "cancelled"]]
request: JsonObject # 原始请求,保留提交时的内容
input_context: NotRequired[InputContext] # 校验后的上下文,包含最新大屏和澄清历史
intent_decision: NotRequired[DecisionResponse | None] # Jev 原始意图判断及分数
intent: NotRequired[Intent | None] # 经过置信度门控的最终意图
requirement: NotRequired[Requirement | None] # 结构化需求及信息完整性判断
change_scope: NotRequired[ChangeScope | None] # 修改范围、规划和资源需求
clarification: NotRequired[Clarification | None] # 当前澄清问题及选项
clarification_reply: NotRequired[
ClarificationReply | None
] # 待合并的用户回答或取消指令
answer: NotRequired[str | None] # 问答节点生成的回答或建议
status: NotRequired[Literal["running", "cancelled"]] # 流程继续或用户取消