From 7ee474044ee6d18e590f734bfed3cfa667026bef Mon Sep 17 00:00:00 2001 From: liubiren Date: Thu, 11 Jun 2026 21:04:13 +0800 Subject: [PATCH] 1 --- utils/agent.py | 48 ++++++++++------- utils/agent_memory.db | Bin 90112 -> 90112 bytes .../application/components/session.py | 49 ++++++++++++++++-- 产品需求文档AI生成/application/state.py | 49 ++++++++++++++---- 4 files changed, 114 insertions(+), 32 deletions(-) diff --git a/utils/agent.py b/utils/agent.py index 4ede8cf..bcabeae 100644 --- a/utils/agent.py +++ b/utils/agent.py @@ -8,10 +8,10 @@ from pathlib import Path import time from typing import AsyncGenerator, List, Optional from uuid import uuid4 - +from pydantic_ai.messages import ModelMessage from pydantic_ai import Agent as PydanticAIAgent, ModelMessage from pydantic_ai.capabilities import AgentCapability -from pydantic_ai.messages import ModelMessagesTypeAdapter +from pydantic_ai.messages import ModelMessagesTypeAdapter, ModelResponse from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.output import OutputSpec from pydantic_ai.providers.openai import OpenAIProvider @@ -172,24 +172,36 @@ class Agent: user_prompt=user_prompt, message_history=message_history, ) as result: - async for part in result.stream_output(): # 全量结构化输出分片 - match part.part_kind: - case "text": # 就文本分片拆分 content 和 thinking - if not (part_content := part.content.strip()): - continue - if not part.is_reasoning: - yield f"0:{part_content}" # content - else: - yield f"1:{part_content}" # thinking - case "tool-call": # 技能调用分片 - yield f"2:技能名称:{part.tool_name},调用参数:{part.args}" - case "tool-return": # 技能返回分片 - yield f"3:技能结果:{part.content}" - case "error": # 错误分片 - yield f"4:{str(part.error)}" - case _: + async for response in result.stream_response(): # 完整结构化相应对象 + if not isinstance(response, ModelResponse): + continue + + if not hasattr(response, "parts") or not response.parts: + continue + + for part in response.parts: + if isinstance(part, str): + content = part.strip() + if content: + yield f"00:{content}" continue + match part.part_kind: + case "text": + content = part.content.strip() + if content: + yield f"00:{content}" + case "thinking": + content = part.content.strip() + if content: + yield f"01:{content}" + case "tool-call" | "builtin-tool-call": + yield f"02:技能名称:{part.tool_name},调用参数:{part.args}" + case "tool-return" | "builtin-tool-return": + yield f"03:技能结果:{part.content}" + case _: + continue + self.agent_memory.create_new_messages( session_id=self.session_id, new_messages=result.new_messages(), diff --git a/utils/agent_memory.db b/utils/agent_memory.db index 6ad6cd47d4089a6eef100853b58cba02d8002b10..bcc6c61c6750f4c2d90dc81a836d5cf3ee5f63ea 100644 GIT binary patch delta 4110 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 delta 1983 zcmb7FUrbt87)LFxZKS$(agA*)-Jhl*!(QNW|CnrD-`ZqmS<>vuWCek3*=FLF%w#WI zL3%YiVMnm5=c+W+y@h`S(y1p9Fwf?4Q)Fm~#Wb6L=fI(MC=v<90 zIjRk+w_-gF%{@J9XSDz2#|jp! z$n;q)Hm98x`#8I;&&-<{8*g!$7zdB81kasNeJGsy z=abcH7SKWPi%!O2`ElMN2KLyJX>9ue!4rZ<`!U00+GU#UzO8^A8(rGwA!Rb zn_M4=Xir7$EzLcxD_?5S?Jj-%?Q0FR3d0$$n-RNNzDqRoR+9xPV9%PEJIt}qqJ@ng zoNM^}GGpe9c3yBAd5e=b+U*Q$WEjT6vkum2GqF%d2YP3@9liU#xmAg-kifRGwk8*Y zWNjY(GI$={eW}^4q{h_EZ8Ez=QbA>Cr|J*k#Sr8!S4QNWG!EzF@{%$+qAuT;i=}!H z;w_NSf?Qlxm+#?HMlLOqO%D(Oh2oI{&L-sjSu*ay;W4~aQ4+&M@_-q{r0&TL63f$% zu^qe{#_oIL^}W0|h968q9ymFNOVPt{{t>+BSF#e>@Q~>Vxio@Pc^rsie+j^7l&_0; z->Z5ncr*zBcxOv4mhtFHo%zFbr*n8R14*l%QK%66w&e-}Ux0Y!4fOs+H{}3;<;vG& zI;W)4aEA8+;Gm?JsvZPL6u^^^%aXhk02mU8sge(eSHT|4s#2VmzL-#R8}#w7;_daP zmAPl>>EYh~-hT3M9{UPPp>-D|;dOdi^C1_0gvE+d@_HC$J_hF3#BXa3mh$@#y@!~hM zrdWs1l1ni-|X4Fx2D9<+5Cvs!JK?D@t}A)=+?4o_Hjs zh&15_E)-uDS}YeOWg%XjU3etT;y@mbadeWTW&u-Kj>yG59M}Q@z-bkDtSRakI3Agi zD=A#elHk}8IP4&EI3cMqDx*|@#QbDF0CkbAj2e7c9m=}p@)qos-5BtQ^IA@nl6xu6*V+T1IK;p{J6S2j05*5fBq6k3H<0f z9K(|#3KmVM{z5%FcMVypW@2hQj2EHv@#C2JFy3AQtsMcX*?A=k79LoTIL(}-mq;AO zj&=^z1uDm(DcYw%T%BJbMIUUBM}%%bx>Fl4jdF25~R4>X>`;@TL{JIICb5Ipmy+yimVoMf69f;6XivaOyFM`648IrWKK>hWhY|n) diff --git a/产品需求文档AI生成/application/components/session.py b/产品需求文档AI生成/application/components/session.py index dfd4606..40b1100 100644 --- a/产品需求文档AI生成/application/components/session.py +++ b/产品需求文档AI生成/application/components/session.py @@ -5,12 +5,12 @@ import reflex from reflex.constants.colors import ColorType -from ..state import State, Turn +from ..state import State, MessageBlockType, MessageBlock, Turn -def message_bubble(message: str, color: ColorType) -> reflex.Component: +def input_bubble(message: str, color: ColorType) -> reflex.Component: """ - 对话组件中一个消息气泡组件 + 输入气泡组件 :param message: 消息 :param color: 颜色 :return: Component @@ -25,6 +25,45 @@ def message_bubble(message: str, color: ColorType) -> reflex.Component: ) +def output_bubble(message_block: MessageBlock) -> reflex.Component: + """ + 输出气泡组件 + :param message_block: 消息块 + :return: 气泡组件 + """ + color = reflex.cond( + message_block.type == MessageBlockType.content, + "accent", + reflex.cond( + message_block.type == MessageBlockType.thinking, + "iris", + reflex.cond( + message_block.type == MessageBlockType.tool_call, + "orange", + reflex.cond( + message_block.type == MessageBlockType.tool_result, + "teal", + reflex.cond( + message_block.type == MessageBlockType.error, + "red", + "mauve", # 兜底 + ), + ), + ), + ), + ) + return reflex.markdown( + message_block.content, + color=reflex.color(color=color, shade=12), + background_color=reflex.color(color=color, shade=4), + display="inline-block", + padding_inline="1em", + padding_block="0.5em", + border_radius="8px", + margin_bottom="4px", + ) + + def turn(turn: Turn) -> reflex.Component: """ 对话组件 @@ -33,12 +72,12 @@ def turn(turn: Turn) -> reflex.Component: """ return reflex.box( reflex.box( - message_bubble(message=turn.input, color="mauve"), + input_bubble(message=turn.input, color="mauve"), text_align="right", margin_bottom="8px", ), reflex.box( - message_bubble(message=turn.output, color="accent"), + reflex.foreach(turn.output, output_bubble), text_align="left", margin_bottom="8px", ), diff --git a/产品需求文档AI生成/application/state.py b/产品需求文档AI生成/application/state.py index 9ae30fd..05f0fd1 100644 --- a/产品需求文档AI生成/application/state.py +++ b/产品需求文档AI生成/application/state.py @@ -5,7 +5,7 @@ from typing import Any, AsyncGenerator, Dict, List, Literal from uuid import uuid4 - +from enum import StrEnum from pydantic import BaseModel, Field import reflex from pathlib import Path @@ -27,17 +27,29 @@ def retrieve_agent(state) -> Agent: if current_session_name not in agents: agents[current_session_name] = Agent( session_id=state.sessions[current_session_name].id, - instructions="You are a friendly chatbot named Reflex. Respond in markdown.", + instructions="You are a friendly chatbot", ) return agents[current_session_name] +# 消息块类型 +class MessageBlockType(StrEnum): + + content = "content" + thinking = "thinking" + tool_call = "tool_call" + tool_result = "tool_result" + error = "error" + + +# 消息块类型前缀映射 +MESSAGE_BLOCK_TYPE_PREFIX_MAP = {f"{i:02d}:": m for i, m in enumerate(MessageBlockType)} + + class MessageBlock(BaseModel): """消息块数据模型,包含类型和内容""" - type: Literal[ - "thinking", "content", "skill_call", "skill_result", "skill_error" - ] = Field(..., description="类型") + type: MessageBlockType = Field(..., description="类型") content: str = Field(default="", description="内容") @@ -170,7 +182,7 @@ class State(reflex.State): :param form_data: 对话表单数据 :return: AsyncGenerator """ - input = form_data["input_message"].strip() + input = form_data["input"].strip() if not input: return @@ -193,17 +205,36 @@ class State(reflex.State): input=input, ) ) + yield # 通知前端更新状态(显示用户输入) + # 当前对话 current_turn = current_session.turns[-1] - yield + # 获取当前会话绑定的智能体 agent = retrieve_agent(self) async for chunk in agent.output_message_streamed(user_prompt=input): + # 跳过空分块 if not chunk: + yield continue - current_session.turns[-1].output_message += chunk - yield + # 匹配消息块类型 + prefix_matched = next( + (t for t in MESSAGE_BLOCK_TYPE_PREFIX_MAP if chunk.startswith(t)), None + ) + # 跳过未匹配分块 + if not prefix_matched: + yield + continue + + # 消息块类型 + type = MESSAGE_BLOCK_TYPE_PREFIX_MAP[prefix_matched] + # 若当前对话输出为空或当前消息块类型和上一个消息块类型不一致则创建消息块 + if not current_turn.output or current_turn.output[-1].type != type: + current_turn.output.append(MessageBlock(type=type)) + current_turn.output[-1].content += chunk.removeprefix(prefix_matched) + + yield # 通知前端更新状态(打字机效果显示输出) # 当前会话处理完成 current_session.is_processing = False