feat:完成重构和测试
This commit is contained in:
@@ -16,6 +16,10 @@ INTENT_QA_MIN_PROBABILITY=0.5
|
|||||||
# screen agent 单次运行的模型调用上限,包括读取 skill、查询物料、提交修正和最终回复
|
# screen agent 单次运行的模型调用上限,包括读取 skill、查询物料、提交修正和最终回复
|
||||||
MAX_AGENT_STEPS=12
|
MAX_AGENT_STEPS=12
|
||||||
|
|
||||||
|
# 会话历史:会话记录完整保存,每次请求最多读取最近 20 轮,再按 token 预算从最近一轮往前保留完整的轮次
|
||||||
|
CONVERSATION_HISTORY_TURNS=20
|
||||||
|
HISTORY_MAX_TOKENS=4096
|
||||||
|
|
||||||
# MongoDB 会话记忆与检查点持久化,不配置则使用进程内存,重启后丢失
|
# MongoDB 会话记忆与检查点持久化,不配置则使用进程内存,重启后丢失
|
||||||
# MONGODB_URI=mongodb://127.0.0.1:27017
|
# MONGODB_URI=mongodb://127.0.0.1:27017
|
||||||
MONGODB_DB=screen_agent
|
MONGODB_DB=screen_agent
|
||||||
|
|||||||
+10
-2
@@ -39,13 +39,21 @@ def merge_config(defaults: dict, overrides: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
|
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
|
||||||
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先,并按物料设置 sample_data"""
|
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先
|
||||||
|
|
||||||
|
sample_data 模型写了就以模型为准(只有它知道是否换成了自己的数据),没写时按物料设置
|
||||||
|
"""
|
||||||
material = find_material(catalog, action.node.type)
|
material = find_material(catalog, action.node.type)
|
||||||
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
|
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
|
||||||
raise ValueError(DATA_BINDING_ERROR)
|
raise ValueError(DATA_BINDING_ERROR)
|
||||||
overrides = action.node.model_dump(mode="json", exclude_unset=True)
|
overrides = action.node.model_dump(mode="json", exclude_unset=True)
|
||||||
node = ScreenNode.model_validate(merge_config(material.template(), overrides))
|
node = ScreenNode.model_validate(merge_config(material.template(), overrides))
|
||||||
return action.model_copy(update={"node": node, "sample_data": material.sample_data})
|
sample_data = (
|
||||||
|
action.sample_data
|
||||||
|
if "sample_data" in action.model_fields_set
|
||||||
|
else material.sample_data
|
||||||
|
)
|
||||||
|
return action.model_copy(update={"node": node, "sample_data": sample_data})
|
||||||
|
|
||||||
|
|
||||||
def run_batch(
|
def run_batch(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Literal, Self
|
|||||||
from langchain.agents import create_agent
|
from langchain.agents import create_agent
|
||||||
from langchain.agents.middleware import ModelCallLimitMiddleware, wrap_model_call
|
from langchain.agents.middleware import ModelCallLimitMiddleware, wrap_model_call
|
||||||
from langchain.agents.structured_output import ToolStrategy
|
from langchain.agents.structured_output import ToolStrategy
|
||||||
from langchain_core.messages import AIMessage
|
from langchain_core.messages import AIMessage, HumanMessage
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
@@ -62,21 +62,36 @@ class AgentReply(BaseModel):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
# 偶发的空回复最多重新调用几次
|
# 模型没有调用工具时最多纠正几次
|
||||||
EMPTY_REPLY_RETRIES = 2
|
NO_TOOL_CALL_RETRIES = 2
|
||||||
|
NO_TOOL_CALL_HINT = (
|
||||||
|
"每一轮都要调用工具。要结束时调用 AgentReply,给用户的话写在 message 里"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@wrap_model_call
|
@wrap_model_call
|
||||||
async def retry_empty_reply(request, handler):
|
async def require_tool_call(request, handler):
|
||||||
"""每一轮都必须调用工具(最后一轮是 AgentReply);模型偶尔返回既没有文本也没有工具调用的空回复,
|
"""ToolStrategy 要求每一轮都调用工具,结束时调用 AgentReply;模型偶尔只回文本(想直接回复用户)或空回复
|
||||||
这是调用失败而不是模型的决定,重新调用即可"""
|
|
||||||
|
原样重试往往得到同样的结果,或者模型转而重复提交 apply_actions 直到调用次数用尽,
|
||||||
|
所以把它的回复和纠正说明加在下一次调用里;只用于这一次调用,不写进 agent 的消息
|
||||||
|
"""
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
for _ in range(EMPTY_REPLY_RETRIES):
|
for _ in range(NO_TOOL_CALL_RETRIES):
|
||||||
message = response.result[0] if response.result else None
|
message = response.result[0] if response.result else None
|
||||||
if not isinstance(message, AIMessage) or message.tool_calls:
|
if not isinstance(message, AIMessage) or message.tool_calls:
|
||||||
break
|
break
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"模型返回了没有工具调用的回复,重新调用: {message.response_metadata}"
|
f"模型没有调用工具,纠正后重新调用: {str(message.content)[:200]}"
|
||||||
|
)
|
||||||
|
# 空回复不放回去,部分接口不接受内容为空的助手消息
|
||||||
|
replied = [message] if message.content else []
|
||||||
|
request = request.override(
|
||||||
|
messages=[
|
||||||
|
*request.messages,
|
||||||
|
*replied,
|
||||||
|
HumanMessage(content=NO_TOOL_CALL_HINT),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
return response
|
return response
|
||||||
@@ -93,12 +108,13 @@ async def get_screen_agent():
|
|||||||
model=get_llm(),
|
model=get_llm(),
|
||||||
tools=[get_nodes, apply_actions, *await get_mcp_tools()],
|
tools=[get_nodes, apply_actions, *await get_mcp_tools()],
|
||||||
system_prompt=SYSTEM_PROMPT,
|
system_prompt=SYSTEM_PROMPT,
|
||||||
|
# 用工具调用给出结论:试过原生结构化输出(ProviderStrategy),歧义时模型更常直接执行而不追问,评测多删了组件
|
||||||
response_format=ToolStrategy(AgentReply),
|
response_format=ToolStrategy(AgentReply),
|
||||||
state_schema=ScreenAgentState,
|
state_schema=ScreenAgentState,
|
||||||
context_schema=ScreenAgentContext,
|
context_schema=ScreenAgentContext,
|
||||||
middleware=[
|
middleware=[
|
||||||
*skill_middleware(),
|
*skill_middleware(),
|
||||||
retry_empty_reply,
|
require_tool_call,
|
||||||
# 超过上限时插入说明并结束,没有结构化结果,由节点按失败处理
|
# 超过上限时插入说明并结束,没有结构化结果,由节点按失败处理
|
||||||
ModelCallLimitMiddleware(
|
ModelCallLimitMiddleware(
|
||||||
run_limit=settings.max_agent_steps, exit_behavior="end"
|
run_limit=settings.max_agent_steps, exit_behavior="end"
|
||||||
@@ -108,4 +124,5 @@ async def get_screen_agent():
|
|||||||
checkpointer=False,
|
checkpointer=False,
|
||||||
name="screen_agent",
|
name="screen_agent",
|
||||||
)
|
)
|
||||||
|
|
||||||
return _agent
|
return _agent
|
||||||
|
|||||||
+7
-1
@@ -10,6 +10,7 @@ from langchain.agents import AgentState
|
|||||||
from langchain.tools import ToolRuntime, tool
|
from langchain.tools import ToolRuntime, tool
|
||||||
from langchain_core.messages import ToolMessage
|
from langchain_core.messages import ToolMessage
|
||||||
from langgraph.types import Command
|
from langgraph.types import Command
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
from app.actions.batch import BatchError, run_batch
|
from app.actions.batch import BatchError, run_batch
|
||||||
from app.schemas.actions import Action, AddAction
|
from app.schemas.actions import Action, AddAction
|
||||||
@@ -63,7 +64,12 @@ def _result_text(actions: list[Action]) -> str:
|
|||||||
|
|
||||||
@tool
|
@tool
|
||||||
def apply_actions(
|
def apply_actions(
|
||||||
actions: list[Action], runtime: ToolRuntime[ScreenAgentContext]
|
actions: Annotated[
|
||||||
|
list[Action],
|
||||||
|
# 空的一批会清掉之前成功的结果;参数校验失败不改状态,之前的结果保留
|
||||||
|
Field(min_length=1, description="至少一个动作;不需要修改时不要调用,直接回复"),
|
||||||
|
],
|
||||||
|
runtime: ToolRuntime[ScreenAgentContext],
|
||||||
) -> Command:
|
) -> Command:
|
||||||
"""提交本次要对大屏做的全部修改
|
"""提交本次要对大屏做的全部修改
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -58,9 +58,13 @@ class Settings:
|
|||||||
conversation_empty_ttl: int = field(
|
conversation_empty_ttl: int = field(
|
||||||
default_factory=lambda: int(os.getenv("CONVERSATION_EMPTY_TTL", "7200"))
|
default_factory=lambda: int(os.getenv("CONVERSATION_EMPTY_TTL", "7200"))
|
||||||
)
|
)
|
||||||
# 每次请求带入模型的最近对话轮数
|
# 每次请求从会话记录读取的最近轮数上限,实际带入多少由 history_max_tokens 决定
|
||||||
conversation_history_turns: int = field(
|
conversation_history_turns: int = field(
|
||||||
default_factory=lambda: int(os.getenv("CONVERSATION_HISTORY_TURNS", "6"))
|
default_factory=lambda: int(os.getenv("CONVERSATION_HISTORY_TURNS", "20"))
|
||||||
|
)
|
||||||
|
# 每次请求带入的会话历史 token 预算(近似计数),从最近一轮往前保留完整的轮次
|
||||||
|
history_max_tokens: int = field(
|
||||||
|
default_factory=lambda: int(os.getenv("HISTORY_MAX_TOKENS", "4096"))
|
||||||
)
|
)
|
||||||
|
|
||||||
# MongoDB 连接串,未配置时会话记录和检查点回退为进程内存实现
|
# MongoDB 连接串,未配置时会话记录和检查点回退为进程内存实现
|
||||||
@@ -107,6 +111,8 @@ class Settings:
|
|||||||
raise ValueError("CONVERSATION_EMPTY_TTL 必须是正整数")
|
raise ValueError("CONVERSATION_EMPTY_TTL 必须是正整数")
|
||||||
if self.conversation_history_turns < 0:
|
if self.conversation_history_turns < 0:
|
||||||
raise ValueError("CONVERSATION_HISTORY_TURNS 必须是非负整数")
|
raise ValueError("CONVERSATION_HISTORY_TURNS 必须是非负整数")
|
||||||
|
if self.history_max_tokens < 1:
|
||||||
|
raise ValueError("HISTORY_MAX_TOKENS 必须是正整数")
|
||||||
if self.max_agent_steps < 1:
|
if self.max_agent_steps < 1:
|
||||||
raise ValueError("MAX_AGENT_STEPS 必须是正整数")
|
raise ValueError("MAX_AGENT_STEPS 必须是正整数")
|
||||||
if self.mcp_tool_timeout <= 0:
|
if self.mcp_tool_timeout <= 0:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""会话历史:数据库保存完整对话,每次请求按 token 预算只带最近的完整轮次
|
||||||
|
|
||||||
|
裁剪在服务层组装请求时做一次,意图判断、问答和 screen agent 看到的是同一段历史
|
||||||
|
"""
|
||||||
|
|
||||||
|
from langchain_core.messages import (
|
||||||
|
AIMessage,
|
||||||
|
BaseMessage,
|
||||||
|
HumanMessage,
|
||||||
|
trim_messages,
|
||||||
|
)
|
||||||
|
from langchain_core.messages.utils import count_tokens_approximately
|
||||||
|
|
||||||
|
from app.schemas.base import JsonObject
|
||||||
|
|
||||||
|
|
||||||
|
def history_messages(history: list[JsonObject]) -> list[BaseMessage]:
|
||||||
|
"""每轮转成一条用户消息和一条助手消息,按时间从早到晚排列"""
|
||||||
|
messages: list[BaseMessage] = []
|
||||||
|
for turn in history:
|
||||||
|
messages.append(HumanMessage(content=turn["user"]))
|
||||||
|
messages.append(AIMessage(content=turn["assistant"]))
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def trim_history(history: list[JsonObject], max_tokens: int) -> list[JsonObject]:
|
||||||
|
"""从最近一轮往前保留,总量不超过 max_tokens;只保留完整的轮次,不从一段回复中间截断
|
||||||
|
|
||||||
|
单独一轮就超过预算时这一轮也不保留
|
||||||
|
"""
|
||||||
|
kept = trim_messages(
|
||||||
|
history_messages(history),
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
token_counter=count_tokens_approximately,
|
||||||
|
strategy="last",
|
||||||
|
# 从用户消息开始,助手回复放不下时连同它的用户消息一起去掉
|
||||||
|
start_on="human",
|
||||||
|
)
|
||||||
|
return history[len(history) - len(kept) // 2 :]
|
||||||
@@ -14,8 +14,10 @@ class IntentNode(BaseNode):
|
|||||||
|
|
||||||
async def process(self, state: QueryState) -> QueryState:
|
async def process(self, state: QueryState) -> QueryState:
|
||||||
updated_state: QueryState = state.copy()
|
updated_state: QueryState = state.copy()
|
||||||
|
context = state["input_context"]
|
||||||
request = DecisionRequest(
|
request = DecisionRequest(
|
||||||
state=request_summary(state["input_context"]),
|
# Jev 只接收 JSON,会话历史作为 history 字段和本次请求放在一起
|
||||||
|
state={"history": context.history, **request_summary(context)},
|
||||||
decisions={"intent": build_intent_question()},
|
decisions={"intent": build_intent_question()},
|
||||||
)
|
)
|
||||||
response = await get_decision_engine().run(request)
|
response = await get_decision_engine().run(request)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from langgraph.config import get_stream_writer
|
|||||||
|
|
||||||
from app.clients.llm import get_llm
|
from app.clients.llm import get_llm
|
||||||
from app.processor.base import BaseNode
|
from app.processor.base import BaseNode
|
||||||
|
from app.processor.history import history_messages
|
||||||
from app.processor.state import QueryState
|
from app.processor.state import QueryState
|
||||||
from app.prompt.qa import QA_SYSTEM_PROMPT
|
from app.prompt.qa import QA_SYSTEM_PROMPT
|
||||||
|
|
||||||
@@ -24,11 +25,16 @@ class QANode(BaseNode):
|
|||||||
|
|
||||||
parser = StrOutputParser()
|
parser = StrOutputParser()
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
|
context = state["input_context"]
|
||||||
async for chunk in get_llm().astream(
|
async for chunk in get_llm().astream(
|
||||||
[
|
[
|
||||||
SystemMessage(content=QA_SYSTEM_PROMPT),
|
SystemMessage(content=QA_SYSTEM_PROMPT),
|
||||||
|
# 会话历史作为此前的对话消息,本次请求是最后一条用户消息
|
||||||
|
*history_messages(context.history),
|
||||||
HumanMessage(
|
HumanMessage(
|
||||||
content=state["input_context"].model_dump_json(exclude_unset=True)
|
content=context.model_dump_json(
|
||||||
|
exclude_unset=True, exclude={"history"}
|
||||||
|
)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from langchain_core.messages import HumanMessage
|
|||||||
from app.agent.screen_agent import AgentReply, get_screen_agent
|
from app.agent.screen_agent import AgentReply, get_screen_agent
|
||||||
from app.agent.tools import ScreenAgentContext
|
from app.agent.tools import ScreenAgentContext
|
||||||
from app.processor.base import BaseNode
|
from app.processor.base import BaseNode
|
||||||
|
from app.processor.history import history_messages
|
||||||
from app.processor.state import QueryState
|
from app.processor.state import QueryState
|
||||||
from app.processor.summary import request_summary
|
from app.processor.summary import request_summary
|
||||||
from app.resources.component import get_catalog_cache
|
from app.resources.component import get_catalog_cache
|
||||||
@@ -55,10 +56,12 @@ class ScreenAgentNode(BaseNode):
|
|||||||
agent = await get_screen_agent()
|
agent = await get_screen_agent()
|
||||||
result = await agent.ainvoke(
|
result = await agent.ainvoke(
|
||||||
{
|
{
|
||||||
|
# 会话历史作为此前的对话消息,本次请求是最后一条用户消息
|
||||||
"messages": [
|
"messages": [
|
||||||
|
*history_messages(context.history),
|
||||||
HumanMessage(
|
HumanMessage(
|
||||||
content=json.dumps(request_summary(context), ensure_ascii=False)
|
content=json.dumps(request_summary(context), ensure_ascii=False)
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
context=ScreenAgentContext(screen=context.screen, catalog=catalog),
|
context=ScreenAgentContext(screen=context.screen, catalog=catalog),
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""请求摘要:意图判断和 screen agent 共用,只给组件的识别信息和布局,完整配置按需读取"""
|
"""请求摘要:意图判断和 screen agent 共用,只给组件的识别信息和布局,完整配置按需读取
|
||||||
|
|
||||||
|
只描述本次请求;会话历史由各使用方另行传入,见 history.py
|
||||||
|
"""
|
||||||
|
|
||||||
from app.schemas.base import JsonObject
|
from app.schemas.base import JsonObject
|
||||||
from app.schemas.input_context import InputContext
|
from app.schemas.input_context import InputContext
|
||||||
|
|
||||||
|
|
||||||
def request_summary(context: InputContext) -> JsonObject:
|
def request_summary(context: InputContext) -> JsonObject:
|
||||||
"""用户输入、会话历史、澄清记录、编辑上下文、附件和大屏摘要"""
|
"""用户输入、澄清记录、编辑上下文、附件和大屏摘要"""
|
||||||
nodes: list[JsonObject] = []
|
nodes: list[JsonObject] = []
|
||||||
for node in context.screen.nodes:
|
for node in context.screen.nodes:
|
||||||
# 图表标题帮助判断用户指的是哪个组件;没有 option 或标题格式不符时按无标题处理
|
# 图表标题帮助判断用户指的是哪个组件;没有 option 或标题格式不符时按无标题处理
|
||||||
@@ -28,7 +31,6 @@ def request_summary(context: InputContext) -> JsonObject:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"prompt": context.prompt,
|
"prompt": context.prompt,
|
||||||
"history": context.history,
|
|
||||||
"clarification_history": context.clarification_history,
|
"clarification_history": context.clarification_history,
|
||||||
"context": context.context.model_dump(mode="json", exclude_unset=True),
|
"context": context.context.model_dump(mode="json", exclude_unset=True),
|
||||||
"screen_summary": {
|
"screen_summary": {
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ QA_SYSTEM_PROMPT = f"""## 任务
|
|||||||
可以简短提示还能继续帮忙,不列举能力清单
|
可以简短提示还能继续帮忙,不列举能力清单
|
||||||
|
|
||||||
## 回答依据
|
## 回答依据
|
||||||
结合当前问题、大屏快照、history(此前各轮的用户输入与助手摘要)和 clarification_history,以用户后续明确补充或纠正为准
|
结合当前问题、大屏快照、此前的对话和 clarification_history,以用户后续明确补充或纠正为准
|
||||||
理解“刚才那个”“再大一点”等指代时参考 history,但当前大屏快照才是最新事实
|
理解“刚才那个”“再大一点”等指代时参考此前的对话,但当前大屏快照才是最新事实
|
||||||
仅在与请求相关时使用大屏快照;一般交流无需强行关联大屏或要求用户补充大屏信息
|
仅在与请求相关时使用大屏快照;一般交流无需强行关联大屏或要求用户补充大屏信息
|
||||||
区分快照事实与待验证的推测,证据不足时说明缺少的信息,不编造故障原因
|
区分快照事实与待验证的推测,证据不足时说明缺少的信息,不编造故障原因
|
||||||
|
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ class AddAction(ActionModel):
|
|||||||
)
|
)
|
||||||
sample_data: bool = Field(
|
sample_data: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="由后端按物料设置,不用填写;true 表示组件使用物料示例数据,不是真实业务数据",
|
description="true 表示组件显示的不是真实业务数据(物料示例数据,或自己编写的数据)。不写时按物料设置;"
|
||||||
|
"只有展示的数据全部来自大屏上已有的组件时写 false,例如更换类型时迁移了原组件的数据",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Turn(BaseModel):
|
|||||||
message_id: str
|
message_id: str
|
||||||
prompt: str # 用户原始输入
|
prompt: str # 用户原始输入
|
||||||
outcome: Literal["completed", "waiting_for_input", "failed", "cancelled"]
|
outcome: Literal["completed", "waiting_for_input", "failed", "cancelled"]
|
||||||
summary: str # 助手侧摘要,只描述后端生成了什么,不代表前端已执行
|
summary: str # 助手一侧的完整回复(回答、追问或失败说明),不截断;不代表前端已执行
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ class InputContext(DesignRequest):
|
|||||||
"""当前复用请求协议,后续可添加解析结果而不改变前端传参"""
|
"""当前复用请求协议,后续可添加解析结果而不改变前端传参"""
|
||||||
|
|
||||||
clarification_history: list[JsonObject] = Field(default_factory=list)
|
clarification_history: list[JsonObject] = Field(default_factory=list)
|
||||||
# 同一会话此前各轮的用户输入与助手摘要,由服务端按 conversation_id 写入,按时间从早到晚排列
|
# 同一会话此前各轮的用户输入与助手回复,由服务端按 conversation_id 读取并按 token 预算裁剪,按时间从早到晚排列
|
||||||
history: list[JsonObject] = Field(default_factory=list)
|
history: list[JsonObject] = Field(default_factory=list)
|
||||||
|
|||||||
+19
-18
@@ -10,19 +10,13 @@ from loguru import logger
|
|||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.core.exceptions import BizException
|
from app.core.exceptions import BizException
|
||||||
from app.observability.tracing import astream_screen
|
from app.observability.tracing import astream_screen
|
||||||
|
from app.processor.history import trim_history
|
||||||
from app.schemas.agent import AgentResult, ResumeRequest
|
from app.schemas.agent import AgentResult, ResumeRequest
|
||||||
from app.schemas.conversation import Turn
|
from app.schemas.conversation import Turn
|
||||||
from app.schemas.request import DesignRequest
|
from app.schemas.request import DesignRequest
|
||||||
from app.services.conversation import ConversationStore
|
from app.services.conversation import ConversationStore
|
||||||
|
|
||||||
|
|
||||||
SUMMARY_MAX_CHARS = 500
|
|
||||||
|
|
||||||
|
|
||||||
def _clip(text: str) -> str:
|
|
||||||
return text if len(text) <= SUMMARY_MAX_CHARS else text[:SUMMARY_MAX_CHARS] + "…"
|
|
||||||
|
|
||||||
|
|
||||||
class AgentService:
|
class AgentService:
|
||||||
def __init__(self, graph, store: ConversationStore):
|
def __init__(self, graph, store: ConversationStore):
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
@@ -51,11 +45,18 @@ class AgentService:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
payload = request.model_dump(mode="json")
|
payload = request.model_dump(mode="json")
|
||||||
# 历史只信任服务端记录,覆盖前端可能携带的同名扩展字段
|
# 历史只信任服务端记录,覆盖前端可能携带的同名扩展字段;记录是完整的,这里按 token 预算裁剪
|
||||||
payload["history"] = [
|
payload["history"] = trim_history(
|
||||||
{"user": turn.prompt, "assistant": turn.summary, "outcome": turn.outcome}
|
[
|
||||||
for turn in turns
|
{
|
||||||
]
|
"user": turn.prompt,
|
||||||
|
"assistant": turn.summary,
|
||||||
|
"outcome": turn.outcome,
|
||||||
|
}
|
||||||
|
for turn in turns
|
||||||
|
],
|
||||||
|
settings.history_max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
thread_id,
|
thread_id,
|
||||||
@@ -144,17 +145,17 @@ class AgentService:
|
|||||||
self._conversation_of[thread_id] = conversation_id
|
self._conversation_of[thread_id] = conversation_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _summarize(state, result: AgentResult) -> str:
|
def _summarize(result: AgentResult) -> str:
|
||||||
"""用一句话描述本轮结果,只陈述后端生成了什么,不代表前端已执行"""
|
"""本轮助手一侧的完整文本,只陈述后端生成了什么,不代表前端已执行;不截断,带入请求时再按预算裁剪"""
|
||||||
if result.status == "waiting_for_input":
|
if result.status == "waiting_for_input":
|
||||||
question = result.clarification.question if result.clarification else ""
|
question = result.clarification.question if result.clarification else ""
|
||||||
return _clip(f"向用户提问:{question}")
|
return f"向用户提问:{question}"
|
||||||
if result.status == "cancelled":
|
if result.status == "cancelled":
|
||||||
return "用户取消了本次补充"
|
return "用户取消了本次补充"
|
||||||
if result.status == "failed":
|
if result.status == "failed":
|
||||||
return _clip(f"处理失败:{result.answer or '未知原因'}")
|
return f"处理失败:{result.answer or '未知原因'}"
|
||||||
if result.answer:
|
if result.answer:
|
||||||
return _clip(result.answer)
|
return result.answer
|
||||||
return f"已生成 {len(result.actions)} 个画布动作"
|
return f"已生成 {len(result.actions)} 个画布动作"
|
||||||
|
|
||||||
async def _save_turn(self, state, result: AgentResult) -> None:
|
async def _save_turn(self, state, result: AgentResult) -> None:
|
||||||
@@ -167,7 +168,7 @@ class AgentService:
|
|||||||
message_id=result.message_id,
|
message_id=result.message_id,
|
||||||
prompt=state["request"]["prompt"],
|
prompt=state["request"]["prompt"],
|
||||||
outcome=result.status,
|
outcome=result.status,
|
||||||
summary=self._summarize(state, result),
|
summary=self._summarize(result),
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user