Files
my-v0-server/app/processor/nodes/screen_agent_node.py
T
2026-10-14 08:19:52 -07:00

152 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""调用 screen agent,按它的回复和成功提交的动作写回状态
kind 由模型决定;代码只在"回复 done 但最后一次提交失败"时不采纳:最后想做的修改没有写入草稿,
模型却说已完成。这是保守做法,last_write_failed 只看工具结果,任务是否完成仍以模型的结论和评测为准
agent 正常结束却没有结论时(调用次数用尽、只回了文本或空回复)补一次只读收尾;取消和异常照常抛出,不收尾
"""
import json
from langchain_core.messages import HumanMessage
from app.actions.batch import mark_sample_data
from app.agent.screen_agent import (
AgentReply,
finalize_reply,
get_screen_agent,
reached_call_limit,
)
from app.agent.tools import ScreenAgentContext, last_write_failed
from app.processor.base import BaseNode
from app.processor.history import history_messages, history_records
from app.processor.state import QueryState, run_data
from app.processor.summary import request_summary
from app.resources.component import get_catalog_cache
from app.resources.material_fields import identifying_texts
from app.schemas.base import JsonObject
from app.schemas.component_resource import ComponentCatalog
from app.schemas.conversation import TurnChange
from app.schemas.screen import Screen, ScreenNode
# 评测按这句话识别"草稿因最后一次提交失败没有交付",单独核对草稿本身是否满足要求
DRAFT_REJECTED = "修改没有通过校验,这次没有生效,可以换个说法再试"
def reply_update(
result: dict, *, limit_hit: bool = False, catalog: ComponentCatalog | None = None
) -> QueryState:
"""把 agent 的运行结果转换成 QueryState 的更新
交付的新增组件按交付后的大屏定 sample_data:留着物料示例数据的也标为示例数据(mark_sample_data)
"""
reply: AgentReply | None = result.get("structured_response")
if reply is None:
# 收尾也没有给出合法的结论;按真实原因说明,不再一律说超出上限
answer = (
"处理步骤超出上限,这次没有完成,修改没有生效,可以换个说法再试"
if limit_hit
else "这次没有得出结论,修改没有生效,可以换个说法再试"
)
return {"status": "failed", "answer": answer}
if reply.kind == "clarify":
return {"status": "waiting_for_input", "clarification": reply.clarification}
if reply.kind == "unable":
return {"status": "failed", "answer": reply.message}
if last_write_failed(result.get("messages", [])):
return {"status": "failed", "answer": DRAFT_REJECTED}
# 各次成功提交的动作按顺序拼接;clarify、unable 不交付,下次请求从原始大屏重新执行
actions = result.get("actions") or []
if actions:
actions = mark_sample_data(actions, result["working_screen"], catalog)
return {"status": "completed", "actions": actions, "answer": reply.message}
def screen_changes(
before: Screen, after: Screen, catalog: ComponentCatalog | None
) -> list[JsonObject]:
"""原始大屏和交付后的大屏按组件做净差,同一组件的多个动作合并成一条
不重放动作:新增后又删掉的组件不出现,布局展开产生的大量移动、缩放合并成一条 modified;
交付后的大屏就是草稿,交付记录在原始大屏上重放得到的也是它
"""
def change(node: ScreenNode, kind: str) -> JsonObject:
texts = identifying_texts(catalog, node.model_dump(mode="json"))
return TurnChange(
node_id=node.id,
type=node.type,
title=texts[0] if texts else node.name,
change=kind,
).model_dump(mode="json")
old = {node.id: node for node in before.nodes}
new = {node.id: node for node in after.nodes}
changes: list[JsonObject] = []
if before.canvas.model_dump(mode="json") != after.canvas.model_dump(mode="json"):
changes.append(
TurnChange(node_id="canvas", type="canvas", change="modified").model_dump(
mode="json"
)
)
for node_id, node in new.items():
if node_id not in old:
changes.append(change(node, "added"))
elif node.model_dump(mode="json") != old[node_id].model_dump(mode="json"):
changes.append(change(node, "modified"))
changes.extend(
change(node, "deleted") for node_id, node in old.items() if node_id not in new
)
return changes
class ScreenAgentNode(BaseNode):
name = "screen_agent_node"
async def process(self, state: QueryState) -> QueryState:
updated_state: QueryState = state.copy()
catalog = get_catalog_cache()
if catalog is None:
updated_state["status"] = "failed"
updated_state["answer"] = "物料目录未载入,暂时无法新建或修改大屏"
return updated_state
context = state["input_context"]
agent = await get_screen_agent(with_data=bool(context.data_files))
request = request_summary(context)
# 此前各轮的修改、查询和澄清记录跟着本次请求,不拼进历史消息
if records := history_records(context.history, context.screen):
request["history_records"] = records
result = await agent.ainvoke(
{
# 会话历史作为此前的对话消息,本次请求是最后一条用户消息
"messages": [
*history_messages(context.history),
HumanMessage(content=json.dumps(request, ensure_ascii=False)),
]
},
context=ScreenAgentContext(
screen=context.screen,
catalog=catalog,
data=run_data(),
),
)
limit_hit = False
if result.get("structured_response") is None:
limit_hit = reached_call_limit(result["messages"])
last = result["messages"][-1]
self.logger.warning(
f"agent 没有给出结论({'调用次数用尽' if limit_hit else '没有调用 AgentReply'}),收尾一次: "
f"{last.type} {str(last.content)[:500]} "
f"metadata={getattr(last, 'response_metadata', None)}"
)
reply = await finalize_reply(result, limit_hit=limit_hit)
result = {**result, "structured_response": reply}
update = reply_update(result, limit_hit=limit_hit, catalog=catalog)
if update.get("actions"):
# reply_update 拿不到原始大屏,修改记录在这里按原始大屏和草稿算
update["changes"] = screen_changes(
context.screen, result["working_screen"], catalog
)
updated_state.update(update)
return updated_state