157 lines
6.1 KiB
Python
157 lines
6.1 KiB
Python
"""将需要规划的需求整理为 ScreenPlan"""
|
|
|
|
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.planner import PLANNER_SYSTEM_PROMPT
|
|
from app.schemas.screen_plan import ScreenPlan
|
|
|
|
|
|
class PlannerNode(BaseNode):
|
|
name = "planner_node"
|
|
|
|
async def process(self, state: QueryState) -> QueryState:
|
|
updated_state: QueryState = state.copy()
|
|
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("组件目录尚未就绪,不能继续规划")
|
|
result = await get_llm(ScreenPlan).ainvoke(
|
|
[
|
|
SystemMessage(content=PLANNER_SYSTEM_PROMPT),
|
|
HumanMessage(
|
|
content=json.dumps(
|
|
{
|
|
"input_context": context.model_dump(mode="json"),
|
|
"requirement": requirement.model_dump(mode="json"),
|
|
"change_scope": scope.model_dump(mode="json"),
|
|
"component_resources": resources.model_dump(mode="json")
|
|
if resources is not None
|
|
else None,
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
),
|
|
]
|
|
)
|
|
plan = ScreenPlan.model_validate(result.model_dump())
|
|
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}"
|
|
)
|
|
existing_ids = {node.id for node in context.screen.nodes}
|
|
planned_ids = {
|
|
component.target_id
|
|
for component in plan.components
|
|
if component.target_id is not None
|
|
}
|
|
if unknown := planned_ids - existing_ids:
|
|
raise ValueError(f"规划引用了不存在的组件: {sorted(unknown)}")
|
|
required_ids = {
|
|
target for change in requirement.changes for target in change.target_ids
|
|
}
|
|
if missing := required_ids - planned_ids:
|
|
raise ValueError(f"规划遗漏了需求中的组件: {sorted(missing)}")
|
|
if scope.scope == "local":
|
|
changed_ids = {
|
|
component.target_id
|
|
for component in plan.components
|
|
if component.disposition in ("modify", "remove")
|
|
}
|
|
if outside := changed_ids - set(scope.target_ids):
|
|
raise ValueError(f"局部规划修改了范围外的组件: {sorted(outside)}")
|
|
# 原始约束和假设不能在规划阶段被无声丢弃
|
|
plan.constraints = list(
|
|
dict.fromkeys([*requirement.constraints, *plan.constraints])
|
|
)
|
|
plan.assumptions = list(
|
|
dict.fromkeys([*requirement.assumptions, *plan.assumptions])
|
|
)
|
|
updated_state["screen_plan"] = plan
|
|
return updated_state
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import asyncio
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from app.processor.nodes.component_resource_node import ComponentResourceNode
|
|
from app.processor.nodes.input_node import InputNode
|
|
from app.schemas.change_scope import ChangeScope
|
|
from app.schemas.requirement import Requirement
|
|
|
|
async def main():
|
|
# 1. 创建 state,直接提供需求和范围,单独测试规划
|
|
state = InputNode().process(
|
|
{
|
|
"request": {
|
|
"prompt": "创建销售大屏,上方展示销售总额,下方展示月度趋势",
|
|
"screen": {"canvas": {"width": 1920, "height": 1080}, "nodes": []},
|
|
}
|
|
}
|
|
)
|
|
state["requirement"] = Requirement(
|
|
intent="create_screen",
|
|
goal="创建销售总览大屏",
|
|
needs_clarification=False,
|
|
changes=[
|
|
{
|
|
"target_scope": "screen",
|
|
"operation": "add",
|
|
"parameters": {"metrics": ["销售总额", "月度销售额"]},
|
|
}
|
|
],
|
|
constraints=["总额在上,趋势在下"],
|
|
)
|
|
state["change_scope"] = ChangeScope(
|
|
scope="screen",
|
|
needs_planning=True,
|
|
resource_needs=["销售总额和月度销售数据,用于指标与趋势展示"],
|
|
reason="需要安排区域和组件组合",
|
|
)
|
|
# 2. 执行节点并打印完整 state,仅调用 LLM
|
|
node = PlannerNode()
|
|
state = ComponentResourceNode().process(state)
|
|
final_state = await node.process(state)
|
|
print(
|
|
json.dumps(
|
|
final_state,
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
default=lambda value: (
|
|
value.model_dump(mode="json")
|
|
if isinstance(value, BaseModel)
|
|
else str(value)
|
|
),
|
|
)
|
|
)
|
|
|
|
asyncio.run(main())
|