152 lines
6.8 KiB
Python
152 lines
6.8 KiB
Python
"""调用 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
|