Files
my-v0-server/app/processor/material_agent.py
T

92 lines
3.6 KiB
Python

"""需要物料字段的模型调用:模型按需调用字段查询工具,最后按指定结构输出
动作生成和修正建议共用;字段查询走物料字段 MCP 服务,服务不可用时直接报错
"""
from typing import TypeVar
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call
from langchain.agents.structured_output import ToolStrategy
from langchain_core.messages import AIMessage, HumanMessage
from pydantic import BaseModel
from app.clients.llm import get_llm
from app.clients.material_mcp import get_material_tools
from app.config import settings
from app.processor.state import QueryState
from app.resources.component import get_catalog_cache
from app.resources.material_fields import list_field_groups
OutputModel = TypeVar("OutputModel", bound=BaseModel)
@wrap_model_call
async def _limit_field_queries(request, handler):
"""查询轮数用尽后只保留输出工具,模型必须给出结果"""
rounds = sum(isinstance(message, AIMessage) for message in request.messages)
if rounds >= settings.max_field_tool_rounds:
request = request.override(tools=[])
return await handler(request)
async def invoke_with_field_tools(
schema: type[OutputModel], system_prompt: str, content: str
) -> OutputModel | None:
"""返回结构化结果;模型未给出结果时返回 None
结果不符合 schema 时抛出 StructuredOutputValidationError,由调用方转换为校验错误
"""
agent = create_agent(
get_llm(),
await get_material_tools(),
system_prompt=system_prompt,
# 不在调用内自动重试,结构错误交给后续校验和修正流程,与不查字段时一致
response_format=ToolStrategy(schema, handle_errors=False),
middleware=[_limit_field_queries],
name=f"{schema.__name__}_with_fields",
)
result = await agent.ainvoke(
{"messages": [HumanMessage(content=content)]},
# 每轮模型调用和工具执行各占一步,轮数由中间件限制,这里只防止异常循环
config={"recursion_limit": 2 * settings.max_field_tool_rounds + 6},
)
return result.get("structured_response")
def field_group_directory(state: QueryState) -> dict[str, dict]:
"""本次相关组件的字段分组目录,按物料类型列出,省去模型先查目录的一轮调用"""
nodes = {node.id: node.type for node in state["input_context"].screen.nodes}
plan = state.get("screen_plan")
if plan is None:
types = [
nodes[target]
for change in state["requirement"].changes
if change.target_scope == "nodes"
for target in change.target_ids
if target in nodes
]
else:
types = [
item.component_type
if item.disposition in ("add", "replace")
else nodes.get(item.target_id)
for item in plan.components
if item.disposition in ("add", "replace", "modify")
]
catalog = get_catalog_cache()
directory = {}
for component_type in dict.fromkeys(item for item in types if item):
try:
if catalog is None:
raise ValueError("物料目录未载入")
directory[component_type] = {
key: value
for key, value in list_field_groups(catalog, component_type).items()
if key not in ("component_type", "catalog_version")
}
except ValueError as error:
# 没有物料定义的组件不能修改属性,告诉模型原因
directory[component_type] = {"error": str(error)}
return directory