"""调用 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