From 01eb3befb0c356f4f89e2e6d9ee04f6a41f8d84e Mon Sep 17 00:00:00 2001 From: liubiren Date: Thu, 2 Jul 2026 22:17:07 +0800 Subject: [PATCH] 1 --- 智能体/application/models.py | 24 +- 智能体/application/state/conversation.py | 334 +++++++++++++++- .../state/create_conversation_modal.py | 4 +- 智能体/application/utils/agent.py | 363 ------------------ 4 files changed, 325 insertions(+), 400 deletions(-) delete mode 100644 智能体/application/utils/agent.py diff --git a/智能体/application/models.py b/智能体/application/models.py index d1e5782..9bc686b 100644 --- a/智能体/application/models.py +++ b/智能体/application/models.py @@ -20,7 +20,7 @@ class MessageHistory(SQLModel, table=True): primary_key=True, description="消息唯一标识", ) - conversation_id: str = SqlField(index=True, description="会话唯一标识") + conversation_id: str = SqlField(index=True, description="对话唯一标识") run_id: str = SqlField(index=True, description="运行唯一标识") new_message: str = SqlField(description="新增消息") @@ -30,7 +30,7 @@ class MessageHistory(SQLModel, table=True): ) -> "MessageHistory": """ 适配新增消息为消息历史记录 - :param conversation_id: 会话唯一标识 + :param conversation_id: 对话唯一标识 :param run_id: 运行唯一标识 :param new_message: 新增消息 :return: 消息历史记录 @@ -69,26 +69,6 @@ class PartKind(StrEnum): RUN_RETURN = "run-return" -class Event(BaseModel): - """ - 自定义分片生命周期事件类 - """ - - run_id: str = Field(..., description="运行唯一标识") - part_index: Optional[int] = Field(default=None, description="分片索引") - previous_part_kind: Optional[PartKind] = Field( - default=None, description="上个分片类型" - ) - part_kind: Optional[PartKind] = Field(default=None, description="分片类型") - next_part_kind: Optional[PartKind] = Field(default=None, description="下个分片类型") - event_kind: EventKind = Field(..., description="事件类型") - event_content: str = Field(default="", description="事件内容") - tool_name: Optional[str] = Field(default=None, description="工具名称") - run_new_messages: List[ModelMessage] = Field( - default=[], description="运行新增消息列表" - ) - - class Reasoning(BaseModel): """推理类""" diff --git a/智能体/application/state/conversation.py b/智能体/application/state/conversation.py index 36ca304..d229677 100644 --- a/智能体/application/state/conversation.py +++ b/智能体/application/state/conversation.py @@ -4,13 +4,47 @@ """ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast +from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai._uuid import uuid7 +from pydantic_ai.capabilities import AgentCapability +from pydantic_ai.messages import ( + FunctionToolCallEvent, + FunctionToolResultEvent, + LoadCapabilityCallPart, + ModelMessage, + PartDeltaEvent, + PartEndEvent, + PartStartEvent, + TextPart, + TextPartDelta, + ThinkingPart, + ToolCallPart, + ToolSearchCallPart, +) +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider import reflex as rx -from application.models import Conversation, EventKind, PartKind, Run +from application.models import Conversation, EventKind, PartKind, Run, Event, EventKind, PartKind from application.state.create_conversation_modal import CreateConversationModalState from application.state.database import DatabaseState -from application.utils.agent import AIAgent + + +instructions: str = """ +# 角色 +专业友好AI助手,结构化解答各类问题。 + +# 输出硬性规则 +1. 全文强制标准Markdown,禁止纯文本;不要额外说明排版格式,直接输出内容; +2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表; +3. 代码块用 ```语言名``` 包裹; +4. 重点内容标注 **粗体**/*斜体*; +5. 思考、工具日志仅输出文本,适配前端折叠面板,禁止输出HTML标签; +6. 内容分点拆分,排版整洁适配前端Markdown渲染。 + +# 行文要求 +语言通俗,逻辑完整简洁,无多余废话。 +""" class ConversationState(rx.State): @@ -18,22 +52,36 @@ class ConversationState(rx.State): 对话状态 """ - # 当前对话唯一标识 + # 初始化当前对话唯一标识 conversation_id: str = str(uuid7()) - # 对话字典 + # 初始化对话字典 conversations: Dict[str, Conversation] = {conversation_id: Conversation()} - # 当前运行唯一标识 + # 初始化当前运行唯一标识 run_id: Optional[str] = None + # 初始化当前运行工具调用唯一标识与片段索引映射表 + run_mapping: dict[str, int] = {} # 初始化智能体 - agent: AIAgent = AIAgent() + agent: Agent = Agent( + model=OpenAIChatModel( + model_name="deepseek-v4-flash", + provider=OpenAIProvider( + base_url="https://tokenhub.tencentmaas.com/v1", + api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq", + ), + ), + instructions=instructions, + capabilities=None, + output_type=str, + retries=1, + ) @rx.var def get_conversations(self) -> Dict[str, Conversation]: """ 获取对话字典,用于前端按照创建倒序渲染对话历史 - :return: 对话字典 + :return: 对话字典(倒序) """ return dict(reversed(self.conversations.items())) @@ -49,9 +97,12 @@ class ConversationState(rx.State): if not description: description = "新对话" - # 创建对话唯一标识 + # 生成并设置为当前对话唯一标识 self.conversation_id = str(uuid7()) + # 创建对话 self.conversations[self.conversation_id] = Conversation(description=description) + # 初始化当前运行唯一标识 + self.run_id = None # 获取创建对话模态窗状态 create_conversation_modal_state = await self.get_state( @@ -71,13 +122,19 @@ class ConversationState(rx.State): return del self.conversations[conversation_id] - # 删除后,若对话字典为空则创建对话 if not self.conversations: - self.conversations[str(uuid7())] = Conversation() + # 生成并设置为当前对话唯一标识 + self.conversation_id = str(uuid7()) + # 创建对话 + self.conversations[self.conversation_id] = Conversation() + # 初始化当前运行唯一标识 + self.run_id = None - # 删除后,若当前对话唯一标识不存在则将末位对话唯一标识设置为当前对话唯一标识 if self.conversation_id not in self.conversations: - self.conversation_id = next(reversed(self.conversations)) + # 将末位对话唯一标识设置为当前对话唯一标识 + self.conversation_id = next(reversed(self.conversations.keys())) + # 初始化当前运行唯一标识 + self.run_id = next(reversed(self.conversations[self.conversation_id].runs.keys()), None) @rx.event def switch_conversation(self, conversation_id: str) -> None: @@ -137,6 +194,9 @@ class ConversationState(rx.State): if not conversation: return + # 更新当前运行唯一标识 + self.run_id = str(uuid7()) + # 获取用户提示词 user_prompt = form_data.get("user_prompt", "").strip() if not user_prompt: @@ -149,8 +209,256 @@ class ConversationState(rx.State): conversation_id=self.conversation_id ) + # 重置当前运行工具调用唯一标识与片段索引映射表 + self.run_mapping.clear() + + async for event in self.agent.run_stream_events( + conversation_id=self.conversation_id, + user_prompt=user_prompt, + message_history=message_history, + ): + match event: + # ========== 分片开始事件 ========== + case PartStartEvent( + event_kind=event_kind, + index=index, + part=part, + previous_part_kind=previous_part_kind, + ): + match part: + # 思考分片开始事件 + case ThinkingPart( + part_kind=part_kind, content=content + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content, + part_index=index, + previous_part_kind=PartKind( + previous_part_kind + ), + part_kind=PartKind(part_kind), + ) + # 检索工具分片开始事件 + case ToolSearchCallPart( + tool_kind=tool_kind, + tool_call_id=tool_call_id, + tool_name=tool_name, + ): + # 记录工具调用唯一标识与片段索引映射 + self.tool_call_ids[tool_call_id] = index + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + previous_part_kind=PartKind( + previous_part_kind + ), + part_kind=PartKind(tool_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 加载能力分片开始事件 + case LoadCapabilityCallPart( + tool_kind=tool_kind, + tool_call_id=tool_call_id, + tool_name=tool_name, + ): + # 记录工具调用唯一标识与片段索引映射 + self.tool_call_ids[tool_call_id] = index + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + previous_part_kind=PartKind( + previous_part_kind + ), + part_kind=PartKind(tool_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 调用工具分片开始事件 + case ToolCallPart( + part_kind=part_kind, + tool_call_id=tool_call_id, + tool_name=tool_name, + ): + # 记录工具调用唯一标识与片段索引映射 + self.tool_call_ids[tool_call_id] = index + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + previous_part_kind=PartKind( + previous_part_kind + ), + part_kind=PartKind(part_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 文本分片开始事件 + case TextPart( + part_kind=part_kind, content=content + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content, + part_index=index, + previous_part_kind=PartKind( + previous_part_kind + ), + part_kind=PartKind(part_kind), + run_id=current_run_id, + ) + + # ========== 分片增量事件(前端仅就文本片段实现打字机效果) ========== + case PartDeltaEvent( + event_kind=event_kind, index=index, delta=delta + ): + match delta: + # 思考分片增量事件 + case ThinkingPartDelta( + part_delta_kind=part_delta_kind, + content_delta=content_delta, + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content_delta or "", + part_index=index, + part_kind=PartKind(part_delta_kind), + run_id=current_run_id, + ) + # 文本分片增量事件 + case TextPartDelta( + part_delta_kind=part_delta_kind, + content_delta=content_delta, + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content_delta, + part_index=index, + part_kind=PartKind(part_delta_kind), + run_id=current_run_id, + ) + + # ========== 分片结束事件 ========== + case PartEndEvent( + event_kind=event_kind, + index=index, + part=part, + next_part_kind=next_part_kind, + ): + match part: + # 思考分片结束事件 + case ThinkingPart( + part_kind=part_kind, content=content + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content, + part_index=index, + part_kind=PartKind(part_kind), + next_part_kind=PartKind(next_part_kind), + run_id=current_run_id, + ) + # 检索工具分片结束事件 + case ToolSearchCallPart( + tool_kind=tool_kind, tool_name=tool_name + ): + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + part_kind=PartKind(tool_kind), + next_part_kind=PartKind(next_part_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 加载能力分片结束事件 + case LoadCapabilityCallPart( + tool_kind=tool_kind, tool_name=tool_name + ): + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + part_kind=PartKind(tool_kind), + next_part_kind=PartKind(next_part_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 调用工具分片结束事件 + case ToolCallPart( + part_kind=part_kind, tool_name=tool_name + ): + yield Event( + event_kind=EventKind(event_kind), + part_index=index, + part_kind=PartKind(part_kind), + next_part_kind=PartKind(next_part_kind), + tool_name=tool_name, + run_id=current_run_id, + ) + # 文本分片结束事件 + case TextPart( + part_kind=part_kind, content=content + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=content, + part_index=index, + part_kind=PartKind(part_kind), + next_part_kind=PartKind(next_part_kind), + run_id=current_run_id, + ) + + # ========== 函数工具调用事件 ========== + case FunctionToolCallEvent( + event_kind=event_kind, + part=part, + ): + yield Event( + event_kind=EventKind(event_kind), + part_index=self.tool_call_ids.get( + part.tool_call_id + ), + part_kind=PartKind(part.part_kind), + tool_name=part.tool_name, + run_id=current_run_id, + ) + + # ========== 函数工具回调事件 ========== + case FunctionToolResultEvent( + event_kind=event_kind, + content=content, + part=part, + ): + yield Event( + event_kind=EventKind(event_kind), + event_content=( + content if isinstance(content, str) else "" + ), # 暂仅处理文本类型,后续处理多模态和缓存 + part_index=self.tool_call_ids.get( + part.tool_call_id + ), + part_kind=PartKind(part.part_kind), + tool_name=part.tool_name, + run_id=current_run_id, + ) + +yield Event( + event_kind=EventKind.RUN_END, + run_id=current_run_id, + run_new_messages=agent_run.new_messages(), +) + + + + + + + + + + + # 运行,处理分片生命周期事件 - async with self.agent.run( + async for self.agent.run( conversation_id=self.conversation_id, user_prompt=user_prompt, message_history=message_history, diff --git a/智能体/application/state/create_conversation_modal.py b/智能体/application/state/create_conversation_modal.py index b2f9a90..43885a0 100644 --- a/智能体/application/state/create_conversation_modal.py +++ b/智能体/application/state/create_conversation_modal.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """ -创建会话模态窗状态 +创建对话模态窗状态 """ import reflex as rx class CreateConversationModalState(rx.State): """ - 创建会话模态窗状态 + 创建对话模态窗状态 """ # 打开状态,True表示打开,False表示关闭 diff --git a/智能体/application/utils/agent.py b/智能体/application/utils/agent.py deleted file mode 100644 index 4ff5353..0000000 --- a/智能体/application/utils/agent.py +++ /dev/null @@ -1,363 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Pydantic AI 智能体和相关模块 -""" - -# 列举导入模块 -from typing import AsyncGenerator, List, Optional -from pydantic_ai import _agent_graph as agent_graph -from pydantic_ai import Agent, ThinkingPartDelta -from pydantic_ai.capabilities import AgentCapability -from pydantic_ai.messages import ( - FunctionToolCallEvent, - FunctionToolResultEvent, - LoadCapabilityCallPart, - ModelMessage, - PartDeltaEvent, - PartEndEvent, - PartStartEvent, - TextPart, - TextPartDelta, - ThinkingPart, - ToolCallPart, - ToolSearchCallPart, -) -from pydantic_ai.models.openai import OpenAIChatModel -from pydantic_ai.output import OutputSpec -from pydantic_ai.providers.openai import OpenAIProvider -from application.models import Event, EventKind, PartKind - - -DEFAULT_INSTRUCTIONS: str = """ -# 角色 -专业友好AI助手,结构化解答各类问题。 - -# 输出硬性规则 -1. 全文强制标准Markdown,禁止纯文本;不要额外说明排版格式,直接输出内容; -2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表; -3. 代码块用 ```语言名``` 包裹; -4. 重点内容标注 **粗体**/*斜体*; -5. 思考、工具日志仅输出文本,适配前端折叠面板,禁止输出HTML标签; -6. 内容分点拆分,排版整洁适配前端Markdown渲染。 - -# 行文要求 -语言通俗,逻辑完整简洁,无多余废话。 -""" - - -class AIAgent: - """ - 基于 Pydantic AI 封装的智能体 - """ - - def __init__( - self, - instructions: Optional[str] = None, - capabilities: Optional[List[AgentCapability]] = None, - output_type: OutputSpec = str, - retries: int = 1, - ): - """ - 初始化 - :param instructions: 指令 - :param capabilities: 技能列表,默认为不使用技能 - :param output_type: 输出类型 - :param retries: 重试次数,默认为1次 - :return: 智能体实例 - """ - # 一次会话(conversation)包含若干次运行(run),每次运行包含用户提示词(user_prompt)和输出(output)。其中,输出包含若干片分片(Part) - - # 本次运行工具调用唯一标识与片段索引映射表 - self.tool_call_ids: dict[str, int] = {} - - # 若指令为空则使用默认指令 - if not instructions: - instructions = DEFAULT_INSTRUCTIONS - - # 初始化智能体 - self.agent = Agent( - model=OpenAIChatModel( - model_name="deepseek-v4-flash", - provider=OpenAIProvider( - base_url="https://tokenhub.tencentmaas.com/v1", - api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq", - ), - ), - instructions=instructions, - capabilities=capabilities, - output_type=output_type, - retries=retries, - ) - - async def run( - self, - conversation_id: str, - user_prompt: str | List[str], - message_history: Optional[List[ModelMessage]] = None, - ) -> AsyncGenerator[Event]: - """ - 运行 - :param conversation_id: 会话唯一标识 - :param user_prompt: 用户提示词(用户提示词) - :param message_history: 消息历史,默认为None - :yield: AsyncGenerator[Event] - """ - # 重置工具调用唯一标识与片段索引映射表 - self.tool_call_ids.clear() - - self.agent.run() - - async with self.agent.iter( - conversation_id=conversation_id, - user_prompt=user_prompt, - message_history=message_history, - ) as agent_run: - # 当前运行唯一标识 - current_run_id = agent_run.ctx.state.run_id - yield Event( - event_kind=EventKind.RUN_START, - run_id=current_run_id, - ) - async for node in agent_run: - if self.agent.is_model_request_node( - node=node - ) or self.agent.is_call_tools_node(node=node): - async with node.stream(agent_run.ctx) as stream: - # 将原始流转为事件流 - stream = ( - agent_run.ctx.deps.root_capability.wrap_run_event_stream( - ctx=agent_graph.build_run_context(ctx=agent_run.ctx), - stream=stream, - ) - ) - async for event in stream: - match event: - # ========== 分片开始事件 ========== - case PartStartEvent( - event_kind=event_kind, - index=index, - part=part, - previous_part_kind=previous_part_kind, - ): - match part: - # 思考分片开始事件 - case ThinkingPart( - part_kind=part_kind, content=content - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content, - part_index=index, - previous_part_kind=PartKind( - previous_part_kind - ), - part_kind=PartKind(part_kind), - run_id=current_run_id, - ) - # 检索工具分片开始事件 - case ToolSearchCallPart( - tool_kind=tool_kind, - tool_call_id=tool_call_id, - tool_name=tool_name, - ): - # 记录工具调用唯一标识与片段索引映射 - self.tool_call_ids[tool_call_id] = index - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - previous_part_kind=PartKind( - previous_part_kind - ), - part_kind=PartKind(tool_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 加载能力分片开始事件 - case LoadCapabilityCallPart( - tool_kind=tool_kind, - tool_call_id=tool_call_id, - tool_name=tool_name, - ): - # 记录工具调用唯一标识与片段索引映射 - self.tool_call_ids[tool_call_id] = index - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - previous_part_kind=PartKind( - previous_part_kind - ), - part_kind=PartKind(tool_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 调用工具分片开始事件 - case ToolCallPart( - part_kind=part_kind, - tool_call_id=tool_call_id, - tool_name=tool_name, - ): - # 记录工具调用唯一标识与片段索引映射 - self.tool_call_ids[tool_call_id] = index - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - previous_part_kind=PartKind( - previous_part_kind - ), - part_kind=PartKind(part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 文本分片开始事件 - case TextPart( - part_kind=part_kind, content=content - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content, - part_index=index, - previous_part_kind=PartKind( - previous_part_kind - ), - part_kind=PartKind(part_kind), - run_id=current_run_id, - ) - - # ========== 分片增量事件(前端仅就文本片段实现打字机效果) ========== - case PartDeltaEvent( - event_kind=event_kind, index=index, delta=delta - ): - match delta: - # 思考分片增量事件 - case ThinkingPartDelta( - part_delta_kind=part_delta_kind, - content_delta=content_delta, - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content_delta or "", - part_index=index, - part_kind=PartKind(part_delta_kind), - run_id=current_run_id, - ) - # 文本分片增量事件 - case TextPartDelta( - part_delta_kind=part_delta_kind, - content_delta=content_delta, - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content_delta, - part_index=index, - part_kind=PartKind(part_delta_kind), - run_id=current_run_id, - ) - - # ========== 分片结束事件 ========== - case PartEndEvent( - event_kind=event_kind, - index=index, - part=part, - next_part_kind=next_part_kind, - ): - match part: - # 思考分片结束事件 - case ThinkingPart( - part_kind=part_kind, content=content - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content, - part_index=index, - part_kind=PartKind(part_kind), - next_part_kind=PartKind(next_part_kind), - run_id=current_run_id, - ) - # 检索工具分片结束事件 - case ToolSearchCallPart( - tool_kind=tool_kind, tool_name=tool_name - ): - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - part_kind=PartKind(tool_kind), - next_part_kind=PartKind(next_part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 加载能力分片结束事件 - case LoadCapabilityCallPart( - tool_kind=tool_kind, tool_name=tool_name - ): - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - part_kind=PartKind(tool_kind), - next_part_kind=PartKind(next_part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 调用工具分片结束事件 - case ToolCallPart( - part_kind=part_kind, tool_name=tool_name - ): - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - part_kind=PartKind(part_kind), - next_part_kind=PartKind(next_part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 文本分片结束事件 - case TextPart( - part_kind=part_kind, content=content - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=content, - part_index=index, - part_kind=PartKind(part_kind), - next_part_kind=PartKind(next_part_kind), - run_id=current_run_id, - ) - - # ========== 函数工具调用事件 ========== - case FunctionToolCallEvent( - event_kind=event_kind, - part=part, - ): - yield Event( - event_kind=EventKind(event_kind), - part_index=self.tool_call_ids.get( - part.tool_call_id - ), - part_kind=PartKind(part.part_kind), - tool_name=part.tool_name, - run_id=current_run_id, - ) - - # ========== 函数工具回调事件 ========== - case FunctionToolResultEvent( - event_kind=event_kind, - content=content, - part=part, - ): - yield Event( - event_kind=EventKind(event_kind), - event_content=( - content if isinstance(content, str) else "" - ), # 暂仅处理文本类型,后续处理多模态和缓存 - part_index=self.tool_call_ids.get( - part.tool_call_id - ), - part_kind=PartKind(part.part_kind), - tool_name=part.tool_name, - run_id=current_run_id, - ) - - yield Event( - event_kind=EventKind.RUN_END, - run_id=current_run_id, - run_new_messages=agent_run.new_messages(), - )