This commit is contained in:
liubiren 2026-07-01 20:22:34 +08:00
parent 11a4505db1
commit 2af7ffd493
5 changed files with 124 additions and 160 deletions

View File

@ -2,6 +2,7 @@
""" """
数据模型 数据模型
""" """
from datetime import datetime
from enum import StrEnum from enum import StrEnum
from typing import Dict, List, Optional from typing import Dict, List, Optional
@ -12,7 +13,7 @@ from sqlmodel import Field as SqlField, SQLModel
# 消息历史数据表模型 # 消息历史数据表模型
# 需使用 reflex db init 初始化数据库表,若重新初始化需手动删除 alembic 相关配置和文件夹 # 重新初始化:先手动删除 alembic 相关配置和文件夹,再使用 reflex db init 初始化数据库表
class MessageHistory(SQLModel, table=True): class MessageHistory(SQLModel, table=True):
id: str = SqlField( id: str = SqlField(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
@ -21,36 +22,30 @@ class MessageHistory(SQLModel, table=True):
) )
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="运行唯一标识")
run_new_message: str = SqlField(description="运行新增消息") new_message: str = SqlField(description="新增消息")
@staticmethod @staticmethod
def adapt( def adapt(
conversation_id: str, run_id: str, run_new_message: List[ModelMessage] conversation_id: str, run_id: str, new_message: List[ModelMessage]
) -> "MessageHistory": ) -> "MessageHistory":
""" """
适配运行新增消息为消息历史 适配新增消息为消息历史记录
:param conversation_id: 会话唯一标识 :param conversation_id: 会话唯一标识
:param run_id: 运行唯一标识 :param run_id: 运行唯一标识
:param run_new_message: 运行新增消息 :param new_message: 新增消息
:return: 消息历史 :return: 消息历史记录
""" """
return MessageHistory( return MessageHistory(
conversation_id=conversation_id, conversation_id=conversation_id,
run_id=run_id, run_id=run_id,
run_new_message=ModelMessagesTypeAdapter.dump_json(run_new_message).decode( new_message=ModelMessagesTypeAdapter.dump_json(new_message).decode(
"utf-8" "utf-8"
), # 序列化为 JSON 字符串 ), # 序列化为 JSON 字符串
) )
"""
会话运行和消息关系
一次会话conversation包含若干次运行run每次运行包含用户提示词user_prompt和输出output其中输出包含推理和回答
"""
class EventKind(StrEnum): class EventKind(StrEnum):
"""事件类型枚举""" """事件类型"""
PART_START = "part_start" PART_START = "part_start"
PART_DELTA = "part_delta" PART_DELTA = "part_delta"
@ -62,7 +57,7 @@ class EventKind(StrEnum):
class PartKind(StrEnum): class PartKind(StrEnum):
"""分片类型枚举""" """分片类型"""
THINKING = "thinking" THINKING = "thinking"
TOOL_SEARCH = "tool-search" TOOL_SEARCH = "tool-search"
@ -76,41 +71,27 @@ class PartKind(StrEnum):
class Event(BaseModel): class Event(BaseModel):
""" """
事件类 自定义分片生命周期事件类
""" """
event_kind: EventKind = Field(..., description="事件类型") run_id: str = Field(..., description="运行唯一标识")
event_content: str = Field(default="", description="事件内容")
part_index: Optional[int] = Field(default=None, description="分片索引") part_index: Optional[int] = Field(default=None, description="分片索引")
previous_part_kind: Optional[PartKind] = Field( previous_part_kind: Optional[PartKind] = Field(
default=None, description="上个分片类型" default=None, description="上个分片类型"
) )
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="下个分片类型") 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="工具名称") tool_name: Optional[str] = Field(default=None, description="工具名称")
run_id: str = Field(..., description="运行唯一标识")
run_new_messages: List[ModelMessage] = Field( run_new_messages: List[ModelMessage] = Field(
default=[], description="运行新增消息列表" default=[], description="运行新增消息列表"
) )
class ReasoningKind(StrEnum):
"""推理类型枚举"""
THINKING = "thinking"
TOOL_SEARCH = "tool-search"
CAPABILITY_LOAD = "capability-load"
TOOL_CALL = "tool-call"
TEXT = "text"
TOOL_RETURN = "tool-return"
RETRY_PROMPT = "retry-prompt"
RUN_RETURN = "run-return"
class Reasoning(BaseModel): class Reasoning(BaseModel):
"""推理类""" """推理类"""
reasoning_kind: ReasoningKind = Field(..., description="推理类型")
content: str = Field(default="", description="推理内容") content: str = Field(default="", description="推理内容")
@ -123,21 +104,22 @@ class Run(BaseModel):
) )
is_reasoning: bool = Field( is_reasoning: bool = Field(
default=False, default=False,
description="运行推理状态True 表示正在推理False 表示非正在推理", description="推理状态True 表示正在推理False 表示非正在推理",
) )
is_expanded: bool = Field( is_collapsed: bool = Field(
default=False, default=False,
description="推理折叠面板展开状态True 表示展开False 表示折叠", description="推理面板折叠状态True 表示折叠False 表示展开",
) )
answer: str = Field(default="", description="") assistant_content: str = Field(default="", description="复正文")
is_streaming: bool = Field( is_streaming: bool = Field(
default=False, default=False,
description="运行流式输出状态True 表示正在流式输出False 表示非正在流式输出", description="流式输出状态True 表示正在流式输出False 表示非正在流式输出",
) )
class Conversation(BaseModel): class Conversation(BaseModel):
"""话类""" """话类"""
description: str = Field(default="", description="会话描述") description: str = Field(default="", description="描述")
runs: Dict[str, Run] = Field(default_factory=dict, description="运行字典") runs: Dict[str, Run] = Field(default_factory=dict, description="运行字典")
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
会话页面状态 对话状态
""" """
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast
@ -8,23 +8,23 @@ from pydantic_ai._uuid import uuid7
import reflex as rx import reflex as rx
from application.models import Conversation, EventKind, PartKind, Run from application.models import Conversation, EventKind, PartKind, Run
from application.state.create_conversation import CreateConversationState 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 from application.utils.agent import AIAgent
class ConversationState(rx.State): class ConversationState(rx.State):
""" """
话状态 话状态
""" """
# 当前话唯一标识 # 当前话唯一标识
current_conversation_id: str = str(uuid7()) conversation_id: str = str(uuid7())
# 话字典 # 话字典
conversations: Dict[str, Conversation] = {current_conversation_id: Conversation()} conversations: Dict[str, Conversation] = {conversation_id: Conversation()}
# 当前运行唯一标识 # 当前运行唯一标识
current_run_id: Optional[str] = None run_id: Optional[str] = None
# 初始化智能体 # 初始化智能体
agent: AIAgent = AIAgent() agent: AIAgent = AIAgent()
@ -32,117 +32,98 @@ class ConversationState(rx.State):
@rx.var @rx.var
def get_conversations(self) -> Dict[str, Conversation]: def get_conversations(self) -> Dict[str, Conversation]:
""" """
获取会话字典用于前端渲染会话列表 获取对话字典用于前端按照创建倒序渲染对话历史
:return: 话字典 :return: 话字典
""" """
return self.conversations return dict(reversed(self.conversations.items()))
@rx.var
def get_current_conversation_description(self) -> str:
"""
获取当前会话描述用于前端渲染导航栏中当前会话描述
:return: 当前会话描述
"""
# 若当前会话唯一标识不存在则创建会话
if self.current_conversation_id not in self.conversations:
self.conversations[self.current_conversation_id] = Conversation()
# 当前会话
current_conversation = self.conversations[self.current_conversation_id]
return current_conversation.description
@rx.event
def set_current_conversation_id(self, conversation_id: str) -> None:
"""
将指定会话唯一标识设置为当前会话唯一标识
:param conversation_id: 指定会话唯一标识
:return: None
"""
# 若指定会话唯一标识不存在则创建会话
if conversation_id not in self.conversations:
self.conversations[conversation_id] = Conversation()
# 将指定会话唯一标识设置为当前会话唯一标识
self.current_conversation_id = conversation_id
@rx.event @rx.event
async def create_conversation(self, form_data: Dict[str, Any]) -> None: async def create_conversation(self, form_data: Dict[str, Any]) -> None:
""" """
创建 创建对话
:param form_data: 表单数据 :param form_data: 表单数据
:return: None :return: None
""" """
# 获取描述 # 获取描述
description = form_data["conversation_description"].strip() description = form_data["description"].strip()
if not description: if not description:
description = "" description = "新对话"
# 创建会话唯一标识 # 创建对话唯一标识
self.current_conversation_id = str(uuid7()) self.conversation_id = str(uuid7())
self.conversations[self.current_conversation_id] = Conversation( self.conversations[self.conversation_id] = Conversation(description=description)
description=description
# 获取创建对话模态窗状态
create_conversation_modal_state = await self.get_state(
CreateConversationModalState
) )
# 关闭创建对话模态窗
# 跨状态读取创建会话模态窗状态 create_conversation_modal_state.is_open = False
create_conversation_state = await self.get_state(CreateConversationState)
create_conversation_state.is_open = False
@rx.event @rx.event
def delete_conversation(self, conversation_id: str) -> None: def delete_conversation(self, conversation_id: str) -> None:
""" """
删除 删除对话
:param conversation_id: 话唯一标识 :param conversation_id: 话唯一标识
:return: None :return: None
""" """
# 若指定会话唯一标识不存在则直接返回
if conversation_id not in self.conversations: if conversation_id not in self.conversations:
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.conversations[str(uuid7())] = Conversation()
# 删除后若当前会话唯一标识不存在则将第一个会话唯一标识设置为当前会话唯一标识
if self.current_conversation_id not in self.conversations: # 删除后,若当前对话唯一标识不存在则将末位对话唯一标识设置为当前对话唯一标识
self.current_conversation_id = next(iter(self.conversations)) if self.conversation_id not in self.conversations:
self.conversation_id = next(reversed(self.conversations))
@rx.event
def switch_conversation(self, conversation_id: str) -> None:
"""
设置为当前对话唯一标识
:param conversation_id: 指定对话唯一标识
:return: None
"""
if conversation_id not in self.conversations:
return
self.conversation_id = conversation_id
@rx.var
def get_conversation_description(self) -> str:
"""
获取当前对话描述用于前端渲染对话标题
:return: 当前对话描述
"""
# 当前对话
conversation = self.conversations.get(self.conversation_id)
return conversation.description if conversation else "新对话"
@rx.var @rx.var
def get_runs(self) -> Dict[str, Run]: def get_runs(self) -> Dict[str, Run]:
""" """
获取当前会话运行字典用于前端渲染运行列表 获取运行字典用于前端渲染运行历史
:return: 当前会话运行字典 :return: 运行字典
""" """
# 若当前会话唯一标识不存在则创建会话 # 当前对话
if self.current_conversation_id not in self.conversations: conversation = self.conversations.get(self.conversation_id)
self.conversations[self.current_conversation_id] = Conversation() return conversation.runs if conversation else {}
# 当前会话
current_conversation = self.conversations[self.current_conversation_id]
return current_conversation.runs
@rx.var @rx.var
def get_current_run_streaming_status(self) -> bool: def get_run_streaming_status(self) -> bool:
""" """
获取当前运行流式输出状态 获取当前运行流式输出状态
:return: 当前运行流式输出状态True 表示正在流式输出False 表示非正在流式输出 :return: 当前运行流式输出状态True 表示正在流式输出False 表示非正在流式输出
""" """
# 若当前会话唯一标识不存在则创建会话 # 当前对话
if self.current_conversation_id not in self.conversations: conversation = self.conversations.get(self.conversation_id)
self.conversations[self.current_conversation_id] = Conversation() if not conversation or not conversation.runs:
# 当前会话
current_conversation = self.conversations[self.current_conversation_id]
# 若当前运行唯一标识为空或不存在则返回 False
if (
not self.current_run_id
or self.current_run_id not in current_conversation.runs
):
return False return False
# 当前运行 # 末位运行
current_run = current_conversation.runs[self.current_run_id] run = next(reversed(conversation.runs.values()))
return current_run.is_streaming return run.is_streaming
@rx.event @rx.event
async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]: async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]:
@ -151,32 +132,29 @@ class ConversationState(rx.State):
:param form_data: 表单数据 :param form_data: 表单数据
:return: AsyncGenerator :return: AsyncGenerator
""" """
# 若当前会话唯一标识不存在则创建会话 # 当前对话
if self.current_conversation_id not in self.conversations: conversation = self.conversations.get(self.conversation_id)
self.conversations[self.current_conversation_id] = Conversation() if not conversation:
return
# 当前会话
current_conversation = self.conversations[self.current_conversation_id]
# 获取用户提示词 # 获取用户提示词
user_prompt = form_data["user_prompt"].strip() user_prompt = form_data.get("user_prompt", "").strip()
if not user_prompt: if not user_prompt:
return return
# 数据库状态 # 获取数据库状态
database_state = await self.get_state(DatabaseState) database_state = await self.get_state(DatabaseState)
# 获取当前会话消息历史 # 获取消息历史
message_history = await database_state.get_message_history( message_history = await database_state.get_message_history(
conversation_id=self.current_conversation_id conversation_id=self.conversation_id
) )
async for event in self.agent.run( # 运行,处理分片生命周期事件
conversation_id=self.current_conversation_id, async with self.agent.run(
conversation_id=self.conversation_id,
user_prompt=user_prompt, user_prompt=user_prompt,
message_history=message_history, message_history=message_history,
): ) as async_event:
# 若事件为空或当前运行唯一标识为空或当前运行唯一标识与事件运行唯一标识不相同则跳过
if ( if (
not event not event
or not self.current_run_id or not self.current_run_id
@ -189,7 +167,7 @@ class ConversationState(rx.State):
# 将事件运行唯一标识设置为当前运行唯一标识 # 将事件运行唯一标识设置为当前运行唯一标识
self.current_run_id = event.run_id self.current_run_id = event.run_id
# 创建运行 # 创建运行
current_conversation.runs[self.current_run_id] = Run( conversation.runs[self.current_run_id] = Run(
user_prompt=user_prompt, is_streaming=True user_prompt=user_prompt, is_streaming=True
) )
yield # 通知前端渲染 yield # 通知前端渲染

View File

@ -5,17 +5,17 @@
import reflex as rx import reflex as rx
class CreateConversationState(rx.State): class CreateConversationModalState(rx.State):
""" """
创建会话状态 创建会话模态窗状态
""" """
# 创建会话模态窗打开状态True表示打开False表示关闭 # 打开状态True表示打开False表示关闭
is_open: bool = False is_open: bool = False
@rx.event @rx.event
def toggle(self, is_open: bool) -> None: def toggle(self, is_open: bool) -> None:
""" """
打开/关闭模态窗 打开/关闭
""" """
self.is_open = is_open self.is_open = is_open

View File

@ -17,9 +17,9 @@ class DatabaseState(rx.State):
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]: async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
""" """
获取会话消息历史 获取消息历史
:param conversation_id: 话唯一标识 :param conversation_id: 话唯一标识
:return: 会话消息历史 :return: 消息历史
""" """
message_history = [] message_history = []
async with rx.asession() as session: async with rx.asession() as session:
@ -30,25 +30,25 @@ class DatabaseState(rx.State):
) )
for record in records.all(): for record in records.all():
message_history.extend( message_history.extend(
ModelMessagesTypeAdapter.validate_json(record.run_new_message) ModelMessagesTypeAdapter.validate_json(record.new_message)
) )
return message_history return message_history
async def save_new_message( async def save_new_message(
self, conversation_id: str, run_id: str, run_new_message: List[ModelMessage] self, conversation_id: str, run_id: str, new_message: List[ModelMessage]
) -> None: ) -> None:
""" """
保存运行新增消息 保存新增消息
:param conversation_id: 话唯一标识 :param conversation_id: 话唯一标识
:param run_id: 运行唯一标识 :param run_id: 运行唯一标识
:param run_new_message: 运行新增消息 :param new_message: 新增消息
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = MessageHistory.adapt( record = MessageHistory.adapt(
conversation_id=conversation_id, conversation_id=conversation_id,
run_id=run_id, run_id=run_id,
run_new_message=run_new_message, new_message=new_message,
) )
session.add(record) session.add(record)
await session.commit() await session.commit()

View File

@ -105,27 +105,31 @@ class AIAgent:
# 重置工具调用唯一标识与片段索引映射表 # 重置工具调用唯一标识与片段索引映射表
self.tool_call_ids.clear() self.tool_call_ids.clear()
self.agent.run()
async with self.agent.iter( async with self.agent.iter(
conversation_id=conversation_id, conversation_id=conversation_id,
user_prompt=user_prompt, user_prompt=user_prompt,
message_history=message_history, message_history=message_history,
) as run: ) as agent_run:
# 当前运行唯一标识 # 当前运行唯一标识
current_run_id = run.ctx.state.run_id current_run_id = agent_run.ctx.state.run_id
yield Event( yield Event(
event_kind=EventKind.RUN_START, event_kind=EventKind.RUN_START,
run_id=current_run_id, run_id=current_run_id,
) )
async for node in run: async for node in agent_run:
if self.agent.is_model_request_node( if self.agent.is_model_request_node(
node=node node=node
) or self.agent.is_call_tools_node(node=node): ) or self.agent.is_call_tools_node(node=node):
async with node.stream(run.ctx) as stream: async with node.stream(agent_run.ctx) as stream:
# 将原始流转为事件流 # 将原始流转为事件流
stream = run.ctx.deps.root_capability.wrap_run_event_stream( stream = (
ctx=agent_graph.build_run_context(ctx=run.ctx), agent_run.ctx.deps.root_capability.wrap_run_event_stream(
ctx=agent_graph.build_run_context(ctx=agent_run.ctx),
stream=stream, stream=stream,
) )
)
async for event in stream: async for event in stream:
match event: match event:
# ========== 分片开始事件 ========== # ========== 分片开始事件 ==========
@ -355,5 +359,5 @@ class AIAgent:
yield Event( yield Event(
event_kind=EventKind.RUN_END, event_kind=EventKind.RUN_END,
run_id=current_run_id, run_id=current_run_id,
run_new_messages=run.new_messages(), run_new_messages=agent_run.new_messages(),
) )