feat:简化部分代码
This commit is contained in:
@@ -4,7 +4,7 @@ import json
|
||||
|
||||
from langchain_core.exceptions import OutputParserException
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.actions.plan import assemble_plan_actions, plan_block_reason, plan_materials
|
||||
from app.clients.llm import get_llm
|
||||
@@ -84,12 +84,6 @@ class ActionNode(BaseNode):
|
||||
)
|
||||
return updated_state
|
||||
|
||||
# 如果未通过动作能力检查,抛出异常,阻止不受支持的需求进入生成流程
|
||||
if not can_generate_actions(state):
|
||||
raise ValueError(
|
||||
"ActionNode 当前只支持无需规划和资源的 move、resize、update、delete、update_canvas 需求"
|
||||
)
|
||||
|
||||
# 将输入上下文、结构化需求和修改范围转成字典,作为本次模型输入
|
||||
payload = {
|
||||
"input_context": state["input_context"].model_dump(mode="json"),
|
||||
@@ -143,11 +137,13 @@ class ActionNode(BaseNode):
|
||||
]
|
||||
)
|
||||
|
||||
# 将模型对象转成字典,非模型返回值保持原样,统一交给后面的协议校验
|
||||
raw = result.model_dump() if isinstance(result, BaseModel) else result
|
||||
# 响应中没有工具调用时解析器会返回 None
|
||||
if result is None:
|
||||
raise OutputParserException("模型未返回结构化结果")
|
||||
raw = result.model_dump()
|
||||
|
||||
# 规划生成的新增节点只保留显式提供的字段,避免模型默认值覆盖物料默认配置
|
||||
if isinstance(result, ActionBatch) and state.get("screen_plan") is not None:
|
||||
if state.get("screen_plan") is not None:
|
||||
# 将原动作对象与序列化结果逐项对应,只调整新增动作的节点数据
|
||||
for source, target in zip(result.actions, raw["actions"]):
|
||||
if isinstance(source, AddAction):
|
||||
@@ -277,6 +273,7 @@ if __name__ == "__main__":
|
||||
)
|
||||
# 加 --modify 测试规划到已有组件调整,无需新增物料
|
||||
if "--modify" in sys.argv:
|
||||
state = ComponentResourceNode().process(state)
|
||||
state["change_scope"].needs_planning = True
|
||||
state["screen_plan"] = ScreenPlan(
|
||||
goal="向右移动 50px",
|
||||
|
||||
@@ -21,26 +21,10 @@ class PlannerNode(BaseNode):
|
||||
requirement = state["requirement"]
|
||||
scope = state["change_scope"]
|
||||
|
||||
# 如果需求仍需澄清、不是新建或修改大屏,或无需规划,则拒绝进入本节点
|
||||
if (
|
||||
requirement.needs_clarification
|
||||
or requirement.intent
|
||||
not in (
|
||||
"create_screen",
|
||||
"modify_screen",
|
||||
)
|
||||
or not scope.needs_planning
|
||||
):
|
||||
raise ValueError("PlannerNode 只处理信息完整且需要规划的新建或修改需求")
|
||||
|
||||
# 获取当前大屏和用户输入,供模型结合已有内容安排布局与组件
|
||||
context = state["input_context"]
|
||||
# 获取可选的组件查询结果
|
||||
resources = state.get("component_resources")
|
||||
|
||||
# 如果已有查询结果但目录未就绪,则停止规划,避免忽略资源查询失败
|
||||
if resources is not None and resources.status != "resolved":
|
||||
raise ValueError("组件目录尚未就绪,不能继续规划")
|
||||
# 图保证组件目录已就绪后才进入规划
|
||||
resources = state["component_resources"]
|
||||
|
||||
llm_client = get_llm(ScreenPlan)
|
||||
result = await llm_client.ainvoke(
|
||||
@@ -54,10 +38,8 @@ class PlannerNode(BaseNode):
|
||||
"input_context": context.model_dump(mode="json"),
|
||||
"requirement": requirement.model_dump(mode="json"),
|
||||
"change_scope": scope.model_dump(mode="json"),
|
||||
# 仅提供规划需要的组件摘要,省略完整默认配置,没有目录时传入 None
|
||||
"component_resources": resources.to_planner_context()
|
||||
if resources is not None
|
||||
else None,
|
||||
# 仅提供规划需要的组件摘要,省略完整默认配置
|
||||
"component_resources": resources.to_planner_context(),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
@@ -65,26 +47,23 @@ class PlannerNode(BaseNode):
|
||||
]
|
||||
)
|
||||
|
||||
# 将模型结果转成字典后重新校验,确保规划字段及组件关系符合输出协议
|
||||
plan = ScreenPlan.model_validate(result.model_dump())
|
||||
plan = result
|
||||
|
||||
# 如果提供了组件目录,则额外校验规划中的物料类型是否真实受支持
|
||||
if resources is not None:
|
||||
# 提取目录支持的前端组件类型,使用集合进行成员检查
|
||||
supported_types = {component.type for component in resources.components}
|
||||
for component in plan.components:
|
||||
# 如果新增组件没有指定类型,无法选择对应物料,立即报错
|
||||
if component.disposition == "add" and component.component_type is None:
|
||||
raise ValueError(f"新增规划组件未指定物料类型: {component.plan_id}")
|
||||
# 新增或修改组件时,只要明确指定了类型,就必须在目录中存在
|
||||
if (
|
||||
component.disposition in ("add", "modify")
|
||||
and component.component_type is not None
|
||||
):
|
||||
if component.component_type not in supported_types:
|
||||
raise ValueError(
|
||||
f"规划使用了目录中不存在的组件类型: {component.component_type}"
|
||||
)
|
||||
# 校验规划中的物料类型是否真实受支持
|
||||
supported_types = {component.type for component in resources.components}
|
||||
for component in plan.components:
|
||||
# 如果新增组件没有指定类型,无法选择对应物料,立即报错
|
||||
if component.disposition == "add" and component.component_type is None:
|
||||
raise ValueError(f"新增规划组件未指定物料类型: {component.plan_id}")
|
||||
# 新增或修改组件时,只要明确指定了类型,就必须在目录中存在
|
||||
if (
|
||||
component.disposition in ("add", "modify")
|
||||
and component.component_type is not None
|
||||
and component.component_type not in supported_types
|
||||
):
|
||||
raise ValueError(
|
||||
f"规划使用了目录中不存在的组件类型: {component.component_type}"
|
||||
)
|
||||
|
||||
# 收集当前大屏中真实存在的节点 ID,用于检查规划引用的已有目标
|
||||
existing_ids = {node.id for node in context.screen.nodes}
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
|
||||
from langchain_core.exceptions import OutputParserException
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.actions.plan import plan_materials
|
||||
from app.clients.llm import get_llm
|
||||
@@ -48,7 +48,7 @@ class ReflectionNode(BaseNode):
|
||||
# 只捕获输出结构和解析错误,配置、网络等异常继续向上抛出
|
||||
try:
|
||||
# 异步生成修正建议,本节点不直接修改动作
|
||||
result = await llm_client.ainvoke(
|
||||
plan = await llm_client.ainvoke(
|
||||
[
|
||||
# 系统消息提供稳定的失败分析和修正规则
|
||||
SystemMessage(content=REFLECTION_SYSTEM_PROMPT),
|
||||
@@ -56,10 +56,9 @@ class ReflectionNode(BaseNode):
|
||||
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
|
||||
]
|
||||
)
|
||||
# 将模型对象转成字典后统一校验,非模型返回值直接交给协议校验
|
||||
plan = ActionRepairPlan.model_validate(
|
||||
result.model_dump() if isinstance(result, BaseModel) else result
|
||||
)
|
||||
# 响应中没有工具调用时解析器会返回 None
|
||||
if plan is None:
|
||||
raise OutputParserException("模型未返回结构化结果")
|
||||
except (ValidationError, OutputParserException):
|
||||
# 如果未能得到合法建议,清空旧修正计划,避免再次使用已失效的建议
|
||||
updated_state["repair_plan"] = None
|
||||
|
||||
@@ -21,13 +21,6 @@ class ScopeNode(BaseNode):
|
||||
# 获取需求节点提取的结构化需求,作为范围判断的依据
|
||||
requirement = state["requirement"]
|
||||
|
||||
# 如果需求仍需澄清,或意图不是新建、修改大屏,则不能进入范围判断
|
||||
if requirement.needs_clarification or requirement.intent not in (
|
||||
"create_screen",
|
||||
"modify_screen",
|
||||
):
|
||||
raise ValueError("ScopeNode 只处理信息完整的新建或修改需求")
|
||||
|
||||
# 获取当前大屏和用户输入,帮助模型结合已有组件判断影响范围
|
||||
context = state["input_context"]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""校验动作范围,再按顺序修改大屏 JSON 副本,检查整批动作能否执行"""
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.actions.plan import validate_plan_actions, validate_plan_result
|
||||
from app.actions.registry import apply_action
|
||||
@@ -22,17 +22,9 @@ class ValidatorNode(BaseNode):
|
||||
actions = state["actions"]
|
||||
|
||||
try:
|
||||
# 重新校验模型内容,避免赋值后绕过字段校验
|
||||
# 转回字典重新校验,避免赋值后绕过字段校验
|
||||
batch = ActionBatch.model_validate(
|
||||
{
|
||||
"actions": [
|
||||
# 将动作模型转回字典,确保重新触发字段校验,字典输入则保持原样
|
||||
action.model_dump() if isinstance(action, BaseModel) else action
|
||||
for action in actions
|
||||
]
|
||||
if isinstance(actions, list)
|
||||
else actions
|
||||
}
|
||||
{"actions": [action.model_dump() for action in actions]}
|
||||
)
|
||||
except ValidationError as error:
|
||||
# 将结构异常转换成统一错误列表,供后续修正流程定位问题
|
||||
@@ -43,8 +35,7 @@ class ValidatorNode(BaseNode):
|
||||
updated_state["simulated_screen"] = None
|
||||
return updated_state
|
||||
|
||||
# 获取可选的结构化需求,没有需求和规划时仅做结构校验与独立模拟执行
|
||||
requirement = state.get("requirement")
|
||||
requirement = state["requirement"]
|
||||
|
||||
# 如果已有规划,检查动作是否覆盖规划目标,以及是否修改了规划外的内容
|
||||
if state.get("screen_plan") is not None:
|
||||
@@ -55,7 +46,7 @@ class ValidatorNode(BaseNode):
|
||||
updated_state["validation_errors"] = errors
|
||||
updated_state["simulated_screen"] = None
|
||||
return updated_state
|
||||
elif requirement is not None:
|
||||
else:
|
||||
# 此分支没有规划,只校验简单修改,新增动作需要走上面的规划分支
|
||||
if any(isinstance(action, AddAction) for action in batch.actions):
|
||||
# 为每个新增动作记录下标和范围错误,便于修正时逐项定位
|
||||
@@ -64,7 +55,7 @@ class ValidatorNode(BaseNode):
|
||||
ActionValidationError(
|
||||
action_index=index,
|
||||
code="scope_error",
|
||||
message="新增动作尚未接入需求范围校验,当前仅支持独立模拟执行",
|
||||
message="新增动作需要先经过规划,不能作为简单修改直接校验",
|
||||
)
|
||||
for index, action in enumerate(batch.actions)
|
||||
if isinstance(action, AddAction)
|
||||
@@ -175,6 +166,7 @@ if __name__ == "__main__":
|
||||
|
||||
from app.processor.nodes.input_node import InputNode
|
||||
from app.schemas.actions import MoveAction, ResizeAction, UpdateAction
|
||||
from app.schemas.requirement import Requirement
|
||||
|
||||
# 1. 创建 state,不需要调用模型
|
||||
state = InputNode().process(
|
||||
@@ -195,6 +187,15 @@ if __name__ == "__main__":
|
||||
}
|
||||
}
|
||||
)
|
||||
state["requirement"] = Requirement(
|
||||
intent="modify_screen",
|
||||
goal="移动、调整尺寸并修改标题",
|
||||
needs_clarification=False,
|
||||
changes=[
|
||||
{"target_scope": "nodes", "target_ids": ["sales-chart"], "operation": op}
|
||||
for op in ("move", "resize", "update")
|
||||
],
|
||||
)
|
||||
state["actions"] = [
|
||||
MoveAction(target_id="sales-chart", delta_x=50),
|
||||
ResizeAction(target_id="sales-chart", width=500),
|
||||
@@ -217,6 +218,15 @@ if __name__ == "__main__":
|
||||
# 3. 在第二个动作制造错误,验证整批失败且不返回部分结果
|
||||
failed_state = {
|
||||
**final_state,
|
||||
"requirement": Requirement(
|
||||
intent="modify_screen",
|
||||
goal="移动两个组件",
|
||||
needs_clarification=False,
|
||||
changes=[
|
||||
{"target_scope": "nodes", "target_ids": [target], "operation": "move"}
|
||||
for target in ("sales-chart", "missing")
|
||||
],
|
||||
),
|
||||
"actions": [
|
||||
state["actions"][0],
|
||||
MoveAction(target_id="missing", delta_x=50),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""暂停图并接收用户回答,不在恢复时重复调用模型"""
|
||||
|
||||
from langgraph.types import interrupt
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.processor.base import BaseNode
|
||||
from app.processor.state import QueryState
|
||||
@@ -13,25 +12,10 @@ class WaitForUserNode(BaseNode):
|
||||
|
||||
def process(self, state: QueryState) -> QueryState:
|
||||
updated_state: QueryState = state.copy()
|
||||
payload = state["clarification"].model_dump(mode="json")
|
||||
while True:
|
||||
# interrupt 的控制异常必须交给 LangGraph 处理
|
||||
value = interrupt(payload)
|
||||
try:
|
||||
reply = ClarificationReply.model_validate(
|
||||
{"answer": value} if isinstance(value, str) else value
|
||||
)
|
||||
if not reply.cancel and reply.option_id is not None:
|
||||
if reply.option_id not in {
|
||||
option.id for option in state["clarification"].options
|
||||
}:
|
||||
raise ValueError("无效选项")
|
||||
break
|
||||
except (ValidationError, ValueError):
|
||||
payload = {
|
||||
**payload,
|
||||
"error": "请选择本题有效选项或填写回答,并提供有效的大屏数据;也可设置 cancel=true 取消",
|
||||
}
|
||||
# 回复的格式与选项有效性已在恢复前由服务层校验,此处只还原为模型
|
||||
reply = ClarificationReply.model_validate(
|
||||
interrupt(state["clarification"].model_dump(mode="json"))
|
||||
)
|
||||
|
||||
updated_state["clarification_reply"] = reply
|
||||
updated_state["status"] = "cancelled" if reply.cancel else "running"
|
||||
|
||||
Reference in New Issue
Block a user