This commit is contained in:
liubiren 2026-07-02 22:17:07 +08:00
parent 2af7ffd493
commit 01eb3befb0
4 changed files with 325 additions and 400 deletions

View File

@ -20,7 +20,7 @@ class MessageHistory(SQLModel, table=True):
primary_key=True, primary_key=True,
description="消息唯一标识", description="消息唯一标识",
) )
conversation_id: str = SqlField(index=True, description="话唯一标识") conversation_id: str = SqlField(index=True, description="话唯一标识")
run_id: str = SqlField(index=True, description="运行唯一标识") run_id: str = SqlField(index=True, description="运行唯一标识")
new_message: str = SqlField(description="新增消息") new_message: str = SqlField(description="新增消息")
@ -30,7 +30,7 @@ class MessageHistory(SQLModel, table=True):
) -> "MessageHistory": ) -> "MessageHistory":
""" """
适配新增消息为消息历史记录 适配新增消息为消息历史记录
:param conversation_id: 话唯一标识 :param conversation_id: 话唯一标识
:param run_id: 运行唯一标识 :param run_id: 运行唯一标识
:param new_message: 新增消息 :param new_message: 新增消息
:return: 消息历史记录 :return: 消息历史记录
@ -69,26 +69,6 @@ class PartKind(StrEnum):
RUN_RETURN = "run-return" 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): class Reasoning(BaseModel):
"""推理类""" """推理类"""

View File

@ -4,13 +4,47 @@
""" """
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast 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._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 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.create_conversation_modal import CreateConversationModalState
from application.state.database import DatabaseState 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): class ConversationState(rx.State):
@ -18,22 +52,36 @@ class ConversationState(rx.State):
对话状态 对话状态
""" """
# 当前对话唯一标识 # 初始化当前对话唯一标识
conversation_id: str = str(uuid7()) conversation_id: str = str(uuid7())
# 对话字典 # 初始化对话字典
conversations: Dict[str, Conversation] = {conversation_id: Conversation()} conversations: Dict[str, Conversation] = {conversation_id: Conversation()}
# 当前运行唯一标识 # 初始化当前运行唯一标识
run_id: Optional[str] = None 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 @rx.var
def get_conversations(self) -> Dict[str, Conversation]: def get_conversations(self) -> Dict[str, Conversation]:
""" """
获取对话字典用于前端按照创建倒序渲染对话历史 获取对话字典用于前端按照创建倒序渲染对话历史
:return: 对话字典 :return: 对话字典倒序
""" """
return dict(reversed(self.conversations.items())) return dict(reversed(self.conversations.items()))
@ -49,9 +97,12 @@ class ConversationState(rx.State):
if not description: if not description:
description = "新对话" description = "新对话"
# 创建对话唯一标识 # 生成并设置为当前对话唯一标识
self.conversation_id = str(uuid7()) self.conversation_id = str(uuid7())
# 创建对话
self.conversations[self.conversation_id] = Conversation(description=description) self.conversations[self.conversation_id] = Conversation(description=description)
# 初始化当前运行唯一标识
self.run_id = None
# 获取创建对话模态窗状态 # 获取创建对话模态窗状态
create_conversation_modal_state = await self.get_state( create_conversation_modal_state = await self.get_state(
@ -71,13 +122,19 @@ class ConversationState(rx.State):
return return
del self.conversations[conversation_id] del self.conversations[conversation_id]
# 删除后,若对话字典为空则创建对话
if not self.conversations: 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: 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 @rx.event
def switch_conversation(self, conversation_id: str) -> None: def switch_conversation(self, conversation_id: str) -> None:
@ -137,6 +194,9 @@ class ConversationState(rx.State):
if not conversation: if not conversation:
return return
# 更新当前运行唯一标识
self.run_id = str(uuid7())
# 获取用户提示词 # 获取用户提示词
user_prompt = form_data.get("user_prompt", "").strip() user_prompt = form_data.get("user_prompt", "").strip()
if not user_prompt: if not user_prompt:
@ -149,8 +209,256 @@ class ConversationState(rx.State):
conversation_id=self.conversation_id 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, conversation_id=self.conversation_id,
user_prompt=user_prompt, user_prompt=user_prompt,
message_history=message_history, message_history=message_history,

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
创建话模态窗状态 创建话模态窗状态
""" """
import reflex as rx import reflex as rx
class CreateConversationModalState(rx.State): class CreateConversationModalState(rx.State):
""" """
创建话模态窗状态 创建话模态窗状态
""" """
# 打开状态True表示打开False表示关闭 # 打开状态True表示打开False表示关闭

View File

@ -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(),
)