diff --git a/智能体/application/models.py b/智能体/application/models.py index d35a1cf..d1e5782 100644 --- a/智能体/application/models.py +++ b/智能体/application/models.py @@ -2,6 +2,7 @@ """ 数据模型 """ +from datetime import datetime from enum import StrEnum 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): id: str = SqlField( default_factory=lambda: str(uuid7()), @@ -21,36 +22,30 @@ class MessageHistory(SQLModel, table=True): ) conversation_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 def adapt( - conversation_id: str, run_id: str, run_new_message: List[ModelMessage] + conversation_id: str, run_id: str, new_message: List[ModelMessage] ) -> "MessageHistory": """ - 适配运行新增消息为消息历史 + 适配新增消息为消息历史记录 :param conversation_id: 会话唯一标识 :param run_id: 运行唯一标识 - :param run_new_message: 运行新增消息 - :return: 消息历史 + :param new_message: 新增消息 + :return: 消息历史记录 """ return MessageHistory( conversation_id=conversation_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" ), # 序列化为 JSON 字符串 ) -""" -会话、运行和消息关系: -一次会话(conversation)包含若干次运行(run),每次运行包含用户提示词(user_prompt)和输出(output)。其中,输出包含推理和回答 -""" - - class EventKind(StrEnum): - """事件类型枚举""" + """事件类型""" PART_START = "part_start" PART_DELTA = "part_delta" @@ -62,7 +57,7 @@ class EventKind(StrEnum): class PartKind(StrEnum): - """分片类型枚举""" + """分片类型""" THINKING = "thinking" TOOL_SEARCH = "tool-search" @@ -76,41 +71,27 @@ class PartKind(StrEnum): class Event(BaseModel): """ - 事件类 + 自定义分片生命周期事件类 """ - event_kind: EventKind = Field(..., description="事件类型") - event_content: str = Field(default="", description="事件内容") + 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_id: str = Field(..., description="运行唯一标识") run_new_messages: List[ModelMessage] = Field( 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): """推理类""" - reasoning_kind: ReasoningKind = Field(..., description="推理类型") content: str = Field(default="", description="推理内容") @@ -123,21 +104,22 @@ class Run(BaseModel): ) is_reasoning: bool = Field( default=False, - description="运行推理状态,True 表示正在推理,False 表示非正在推理", + description="推理状态,True 表示正在推理,False 表示非正在推理", ) - is_expanded: bool = Field( + is_collapsed: bool = Field( default=False, - description="推理折叠面板展开状态,True 表示展开,False 表示折叠", + description="推理面板折叠状态,True 表示折叠,False 表示展开", ) - answer: str = Field(default="", description="回答") + assistant_content: str = Field(default="", description="回复正文") is_streaming: bool = Field( default=False, - description="运行流式输出状态,True 表示正在流式输出,False 表示非正在流式输出", + description="流式输出状态,True 表示正在流式输出,False 表示非正在流式输出", ) class Conversation(BaseModel): - """会话类""" + """对话类""" - description: str = Field(default="新会话", description="会话描述") + description: str = Field(default="新对话", description="描述") runs: Dict[str, Run] = Field(default_factory=dict, description="运行字典") + created_at: datetime = Field(default_factory=datetime.now, description="创建时间") diff --git a/智能体/application/state/conversation.py b/智能体/application/state/conversation.py index 539e683..36ca304 100644 --- a/智能体/application/state/conversation.py +++ b/智能体/application/state/conversation.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -会话页面状态 +对话状态 """ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast @@ -8,23 +8,23 @@ from pydantic_ai._uuid import uuid7 import reflex as rx 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.utils.agent import AIAgent class ConversationState(rx.State): """ - 会话状态 + 对话状态 """ - # 当前会话唯一标识 - current_conversation_id: str = str(uuid7()) - # 会话字典 - conversations: Dict[str, Conversation] = {current_conversation_id: Conversation()} + # 当前对话唯一标识 + conversation_id: str = str(uuid7()) + # 对话字典 + conversations: Dict[str, Conversation] = {conversation_id: Conversation()} # 当前运行唯一标识 - current_run_id: Optional[str] = None + run_id: Optional[str] = None # 初始化智能体 agent: AIAgent = AIAgent() @@ -32,117 +32,98 @@ class ConversationState(rx.State): @rx.var def get_conversations(self) -> Dict[str, Conversation]: """ - 获取会话字典,用于前端渲染会话列表 - :return: 会话字典 + 获取对话字典,用于前端按照创建倒序渲染对话历史 + :return: 对话字典 """ - return self.conversations - - @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 + return dict(reversed(self.conversations.items())) @rx.event async def create_conversation(self, form_data: Dict[str, Any]) -> None: """ - 创建会话 + 创建对话 :param form_data: 表单数据 :return: None """ # 获取描述 - description = form_data["conversation_description"].strip() + description = form_data["description"].strip() if not description: - description = "新会话" + description = "新对话" - # 创建会话唯一标识 - self.current_conversation_id = str(uuid7()) - self.conversations[self.current_conversation_id] = Conversation( - description=description + # 创建对话唯一标识 + self.conversation_id = str(uuid7()) + self.conversations[self.conversation_id] = Conversation(description=description) + + # 获取创建对话模态窗状态 + create_conversation_modal_state = await self.get_state( + CreateConversationModalState ) - - # 跨状态读取创建会话模态窗状态 - create_conversation_state = await self.get_state(CreateConversationState) - create_conversation_state.is_open = False + # 关闭创建对话模态窗 + create_conversation_modal_state.is_open = False @rx.event def delete_conversation(self, conversation_id: str) -> None: """ - 删除会话 - :param conversation_id: 会话唯一标识 + 删除对话 + :param conversation_id: 对话唯一标识 :return: None """ - # 若指定会话唯一标识不存在则直接返回 if conversation_id not in self.conversations: return - del self.conversations[conversation_id] - # 删除后若会话字典为空则后创建会话 + + # 删除后,若对话字典为空则创建对话 if not self.conversations: 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 def get_runs(self) -> Dict[str, Run]: """ - 获取当前会话运行字典,用于前端渲染运行列表 - :return: 当前会话运行字典 + 获取运行字典,用于前端渲染运行历史 + :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.runs + # 当前对话 + conversation = self.conversations.get(self.conversation_id) + return conversation.runs if conversation else {} @rx.var - def get_current_run_streaming_status(self) -> bool: + def get_run_streaming_status(self) -> bool: """ 获取当前运行流式输出状态 :return: 当前运行流式输出状态(True 表示正在流式输出,False 表示非正在流式输出) """ - # 若当前会话唯一标识不存在则创建会话 - if self.current_conversation_id not in self.conversations: - self.conversations[self.current_conversation_id] = Conversation() - - # 当前会话 - 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 - ): + # 当前对话 + conversation = self.conversations.get(self.conversation_id) + if not conversation or not conversation.runs: return False - # 当前运行 - current_run = current_conversation.runs[self.current_run_id] - return current_run.is_streaming + # 末位运行 + run = next(reversed(conversation.runs.values())) + return run.is_streaming @rx.event async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]: @@ -151,32 +132,29 @@ class ConversationState(rx.State): :param form_data: 表单数据 :return: AsyncGenerator """ - # 若当前会话唯一标识不存在则创建会话 - if self.current_conversation_id not in self.conversations: - self.conversations[self.current_conversation_id] = Conversation() - - # 当前会话 - current_conversation = self.conversations[self.current_conversation_id] + # 当前对话 + conversation = self.conversations.get(self.conversation_id) + if not conversation: + return # 获取用户提示词 - user_prompt = form_data["user_prompt"].strip() + user_prompt = form_data.get("user_prompt", "").strip() if not user_prompt: return - # 数据库状态 + # 获取数据库状态 database_state = await self.get_state(DatabaseState) - # 获取当前会话消息历史 + # 获取消息历史 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, message_history=message_history, - ): - - # 若事件为空或当前运行唯一标识为空或当前运行唯一标识与事件运行唯一标识不相同则跳过 + ) as async_event: if ( not event or not self.current_run_id @@ -189,7 +167,7 @@ class ConversationState(rx.State): # 将事件运行唯一标识设置为当前运行唯一标识 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 ) yield # 通知前端渲染 diff --git a/智能体/application/state/create_conversation.py b/智能体/application/state/create_conversation_modal.py similarity index 57% rename from 智能体/application/state/create_conversation.py rename to 智能体/application/state/create_conversation_modal.py index 07736d4..b2f9a90 100644 --- a/智能体/application/state/create_conversation.py +++ b/智能体/application/state/create_conversation_modal.py @@ -5,17 +5,17 @@ import reflex as rx -class CreateConversationState(rx.State): +class CreateConversationModalState(rx.State): """ - 创建会话状态 + 创建会话模态窗状态 """ - # 创建会话模态窗打开状态,True表示打开,False表示关闭 + # 打开状态,True表示打开,False表示关闭 is_open: bool = False @rx.event def toggle(self, is_open: bool) -> None: """ - 打开/关闭模态窗 + 打开/关闭 """ self.is_open = is_open diff --git a/智能体/application/state/database.py b/智能体/application/state/database.py index 6bf1bd0..b894944 100644 --- a/智能体/application/state/database.py +++ b/智能体/application/state/database.py @@ -17,9 +17,9 @@ class DatabaseState(rx.State): async def get_message_history(self, conversation_id: str) -> List[ModelMessage]: """ - 获取会话消息历史 - :param conversation_id: 会话唯一标识 - :return: 会话消息历史 + 获取消息历史 + :param conversation_id: 对话唯一标识 + :return: 消息历史 """ message_history = [] async with rx.asession() as session: @@ -30,25 +30,25 @@ class DatabaseState(rx.State): ) for record in records.all(): message_history.extend( - ModelMessagesTypeAdapter.validate_json(record.run_new_message) + ModelMessagesTypeAdapter.validate_json(record.new_message) ) return message_history 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: """ - 保存运行新增消息 - :param conversation_id: 会话唯一标识 + 保存新增消息 + :param conversation_id: 对话唯一标识 :param run_id: 运行唯一标识 - :param run_new_message: 运行新增消息 + :param new_message: 新增消息 :return: None """ async with rx.asession() as session: record = MessageHistory.adapt( conversation_id=conversation_id, run_id=run_id, - run_new_message=run_new_message, + new_message=new_message, ) session.add(record) await session.commit() diff --git a/智能体/application/utils/agent.py b/智能体/application/utils/agent.py index 547137b..4ff5353 100644 --- a/智能体/application/utils/agent.py +++ b/智能体/application/utils/agent.py @@ -105,26 +105,30 @@ class AIAgent: # 重置工具调用唯一标识与片段索引映射表 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 run: + ) as agent_run: # 当前运行唯一标识 - current_run_id = run.ctx.state.run_id + 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 run: + 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(run.ctx) as stream: + async with node.stream(agent_run.ctx) as stream: # 将原始流转为事件流 - stream = run.ctx.deps.root_capability.wrap_run_event_stream( - ctx=agent_graph.build_run_context(ctx=run.ctx), - stream=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: @@ -355,5 +359,5 @@ class AIAgent: yield Event( event_kind=EventKind.RUN_END, run_id=current_run_id, - run_new_messages=run.new_messages(), + run_new_messages=agent_run.new_messages(), )