143 lines
5.8 KiB
Python
143 lines
5.8 KiB
Python
"""判断需求涉及的范围、规划和资源需求"""
|
||
|
||
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.nodes.action_node import action_block_reason
|
||
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:
|
||
try:
|
||
return await self._generate(state)
|
||
except ValueError as error:
|
||
# 结构化输出解析失败或引用校验不通过(含 ValidationError、OutputParserException)
|
||
return self.fail_on_invalid_output(
|
||
state, error, "未能确定需要修改的范围,请描述得更具体一些"
|
||
)
|
||
|
||
async def _generate(self, state: QueryState) -> QueryState:
|
||
# 浅拷贝状态
|
||
updated_state: QueryState = state.copy()
|
||
# 获取需求节点提取的结构化需求,作为范围判断的依据
|
||
requirement = state["requirement"]
|
||
|
||
# 获取当前大屏和用户输入,帮助模型结合已有组件判断影响范围
|
||
context = state["input_context"]
|
||
|
||
llm_client = get_llm(ChangeScope)
|
||
# 异步调用模型,判断涉及哪些组件、是否需要规划以及需要补充哪些资源
|
||
scope = await llm_client.ainvoke(
|
||
[
|
||
# 系统消息提供稳定的范围、规划和资源判断规则
|
||
SystemMessage(content=SCOPE_SYSTEM_PROMPT),
|
||
# 用户消息承载本次结构化需求和输入上下文,序列化为 JSON 供模型读取
|
||
HumanMessage(
|
||
content=json.dumps(
|
||
{
|
||
"requirement": requirement.model_dump(mode="json"),
|
||
"input_context": context.model_dump(
|
||
mode="json", exclude_unset=True
|
||
),
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
),
|
||
]
|
||
)
|
||
|
||
# 收集当前大屏中真实存在的节点 ID,用于检查模型是否引用了不存在的组件
|
||
existing_ids = {node.id for node in context.screen.nodes}
|
||
# 如果范围中的目标 ID 不在当前大屏中,则抛出异常并列出无效 ID
|
||
if unknown_ids := set(scope.target_ids) - existing_ids:
|
||
raise ValueError(f"范围引用了不存在的节点: {sorted(unknown_ids)}")
|
||
|
||
# 汇总需求中明确指定的目标 ID,使用集合去除多项变更之间重复引用的节点
|
||
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)}")
|
||
|
||
# 保存校验后的范围,下面的动作能力检查也需要读取该结果
|
||
updated_state["change_scope"] = scope
|
||
# 需要规划时先交给后续规划流程,不在此处判断能否直接生成动作
|
||
# 无需规划时检查资源和操作能力,无法执行则记录阻塞原因,通过则返回 None
|
||
reason = None if scope.needs_planning else action_block_reason(updated_state)
|
||
# 写入本次检查结果,没有阻塞原因时同时清空旧的失败说明
|
||
updated_state["failure_reason"] = reason
|
||
# 有阻塞原因时标记失败,否则保持运行状态,继续后续处理
|
||
updated_state["status"] = "failed" if reason else "running"
|
||
|
||
# 返回包含范围、失败原因和处理状态的更新结果
|
||
return updated_state
|
||
|
||
|
||
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())
|