From b0323d256af33930ece43e72487396e84a2cf171 Mon Sep 17 00:00:00 2001 From: liubiren Date: Sat, 25 Jul 2026 18:54:46 +0800 Subject: [PATCH] 1 --- agent/application/domain_models.py | 61 +++++++ agent/application/models/__init__.py | 25 --- agent/application/models/domains.py | 48 ------ agent/application/models/tables.py | 94 ----------- agent/application/states/auth.py | 7 +- agent/application/states/conversation.py | 152 ++++++++--------- agent/application/states/database.py | 199 +++++++++++++++++------ 7 files changed, 283 insertions(+), 303 deletions(-) create mode 100644 agent/application/domain_models.py delete mode 100644 agent/application/models/__init__.py delete mode 100644 agent/application/models/domains.py delete mode 100644 agent/application/models/tables.py diff --git a/agent/application/domain_models.py b/agent/application/domain_models.py new file mode 100644 index 0000000..58847aa --- /dev/null +++ b/agent/application/domain_models.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +""" +领域模型 +""" +from datetime import datetime +from typing import Dict, List +from pydantic_ai._uuid import uuid7 +from pydantic import BaseModel, Field + + +class Thought(BaseModel): + """ + 思考节点领域模型 + """ + + id: str = Field(..., description="思考唯一标识") + type: str = Field(..., description="思考类型") + content: str = Field(..., description="思考内容") + + +class Dialog(BaseModel): + """ + 对话领域模型 + """ + + id: str = Field(..., description="对话唯一标识") + user_prompt: str = Field(..., description="用户提示词") + thoughts: List[Thought] = Field(default_factory=list, description="思考列表") + result_output: str = Field(default="", description="结果输出") + is_thinking: bool = Field( + default=False, description="正在思考,True 表示正在思考,False 表示未正在思考" + ) + is_expanded: bool = Field( + default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" + ) + + +class Conversation(BaseModel): + """ + 会话领域模型 + """ + + id: str = Field(default_factory=lambda: str(uuid7()), description="会话唯一标识") + description: str = Field(default="新会话", description="会话描述") + is_actived: bool = Field( + default=False, description="已激活,True 表示激活,False 表示不激活" + ) + is_running: bool = Field( + default=False, description="正在运行,True 表示正在运行,False 表示未正在运行" + ) + dialogs: List[Dialog] = Field(default_factory=list, description="对话列表") + created_at: datetime = Field(default_factory=datetime.now, description="创建时间") + + +class ConversationHistoryItem(BaseModel): + """ + 会话历史项领域模型 + """ + id: str = Field(..., description="会话唯一标识") + description: str = Field(..., description="会话描述") + created_at: str = Field(..., description="会话创建时间") diff --git a/agent/application/models/__init__.py b/agent/application/models/__init__.py deleted file mode 100644 index 81eaa42..0000000 --- a/agent/application/models/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from application.models.tables import ( - CaptchaRecord, - ConversationRecord, - DialogRecord, - RunResultRecord, - UserRecord, -) -from application.models.domains import ( - Conversation, - Dialog, - ThoughtNode, -) - - -__all__ = [ - "CaptchaRecord", - "Conversation", - "ConversationRecord", - "Dialog", - "DialogRecord", - "RunResultRecord", - "ThoughtNode", - "UserRecord", -] diff --git a/agent/application/models/domains.py b/agent/application/models/domains.py deleted file mode 100644 index 77f477d..0000000 --- a/agent/application/models/domains.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -""" -reflex运行时领域模型 -""" -from datetime import datetime -from typing import Dict - -from pydantic import BaseModel, Field - - -class ThoughtNode(BaseModel): - """ - 思考节点领域模型 - """ - - kind: str - content: str - - -class Dialog(BaseModel): - """ - 对话领域模型 - """ - - question: str = Field(..., description="问题") - is_thinking: bool = Field( - default=False, description="思考状态,True 表示思考中,False 表示思考完成" - ) - is_collapse_expanded: bool = Field( - default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" - ) - thought_nodes: dict[int, ThoughtNode] = Field( - default_factory=dict, description="思考节点字典" - ) - answer: str = Field(default="", description="回答") - - -class Conversation(BaseModel): - """ - 会话领域模型 - """ - - description: str = Field(default="新会话", description="会话描述") - is_running: bool = Field( - default=False, description="运行状态,True 表示运行中,False 表示运行完成" - ) - dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话字典") - created_at: datetime = Field(default_factory=datetime.now, description="创建时间") diff --git a/agent/application/models/tables.py b/agent/application/models/tables.py deleted file mode 100644 index 4706e01..0000000 --- a/agent/application/models/tables.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -""" -数据表模型 -""" -from datetime import datetime, timedelta -from random import choices - -from pydantic_ai._uuid import uuid7 -from sqlmodel import Field, JSON, SQLModel - - -class CaptchaRecord(SQLModel, table=True): - """ - 验证码记录 - 邮箱、验证码和失效时间联合作为主键 - """ - - email: str = Field(..., primary_key=True, description="邮箱") - captcha: str = Field( - default_factory=lambda: "".join(choices("0123456789", k=6)), - primary_key=True, - description="验证码", - ) - is_valid: bool = Field( - default=True, - index=True, - description="验证码有效:True 表示有效,False 表示无效", - ) - is_verified: bool = Field( - default=False, - index=True, - description="验证码已核验:True 表示已核验,False 表示未核验", - ) - expired_at: datetime = Field( - default_factory=lambda: datetime.now() + timedelta(minutes=30), - primary_key=True, - description="失效时间", - ) - - -# 用户记录 -class UserRecord(SQLModel, table=True): - id: str = Field( - default_factory=lambda: str(uuid7()), - primary_key=True, - description="用户唯一标识", - ) - email: str = Field(..., index=True, description="邮箱") - - -# 会话表 -class ConversationRecord(SQLModel, table=True): - id: str = Field( - default_factory=lambda: str(uuid7()), - primary_key=True, - description="会话唯一标识", - ) - user_id: str = Field(..., index=True, description="用户唯一标识") - description: str = Field(..., description="会话描述") - created_at: datetime = Field(default_factory=datetime.now, description="创建时间") - is_deleted: bool = Field( - default=False, - index=True, - description="已删除:True 表示已删除,False 表示未删除", - ) - - -# 思考节点表(仅定义) -class ThoughtNodeRecord(SQLModel): - - kind: str = Field(description="思考节点类型") - content: str = Field(default="", description="思考节点内容") - - -# 对话表 -class DialogRecord(SQLModel, table=True): - id: str = Field( - default_factory=lambda: str(uuid7()), - primary_key=True, - description="对话唯一标识", - ) - conversation_id: str = Field(..., index=True, description="会话唯一标识") - question: str = Field(..., description="问题") - thought_nodes: dict[int, ThoughtNodeRecord] = Field( - default_factory=dict, sa_type=JSON, description="思考节点字典" - ) - answer: str = Field(default="", description="回答") - - -# 运行结果表 -class RunResultRecord(SQLModel, table=True): - conversation_id: str = Field(primary_key=True, description="会话唯一标识") - dialog_id: str = Field(primary_key=True, description="对话唯一标识") - new_messages: str = Field(description="新增消息") diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 82a31dc..48996d9 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -235,10 +235,9 @@ class AuthState(rx.State): # 创建用户记录 user_id = await database_state.create_user_record(email=self.email) - # 获取会话状态 + # 就会话状态设置用户唯一标识 conversation_state = await self.get_state(ConversationState) - # 加载 - await conversation_state.load(user_id=user_id) + await conversation_state.set_user_id(user_id=user_id) self.user_id = user_id @@ -269,7 +268,7 @@ class AuthState(rx.State): conversation_state = await self.get_state(ConversationState) conversation_state.user_id = "" - conversation_state.conversations = {} + conversation_state.conversations = [] conversation_state.conversation_id = "" conversation_state.is_conversation_history_shown = False conversation_state.is_conversation_creating = False diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 009f0fb..48172e0 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -3,9 +3,7 @@ 会话状态 """ from datetime import datetime -from typing import Any, AsyncGenerator, Dict - -from pydantic import BaseModel, Field +from typing import Any, AsyncGenerator, Dict, List from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai.messages import ( FunctionToolCallEvent, @@ -26,6 +24,7 @@ from pydantic_ai.run import AgentRunResultEvent import reflex as rx from application.states.database import DatabaseState +from application.domain_models import Conversation, Dialog, ConversationHistoryItem instructions: str = """ # 角色 @@ -59,46 +58,6 @@ agent: Agent = Agent( ) -class ThoughtNode(BaseModel): - """ - 思考节点领域模型 - """ - - kind: str - content: str - - -class Dialog(BaseModel): - """ - 对话领域模型 - """ - - question: str = Field(..., description="问题") - is_thinking: bool = Field( - default=False, description="思考状态,True 表示思考中,False 表示思考完成" - ) - is_collapse_expanded: bool = Field( - default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" - ) - thought_nodes: dict[int, ThoughtNode] = Field( - default_factory=dict, description="思考节点字典" - ) - answer: str = Field(default="", description="回答") - - -class Conversation(BaseModel): - """ - 会话领域模型 - """ - - description: str = Field(default="新会话", description="会话描述") - is_running: bool = Field( - default=False, description="运行状态,True 表示运行中,False 表示运行完成" - ) - dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话字典") - created_at: datetime = Field(default_factory=datetime.now, description="创建时间") - - class ConversationState(rx.State): """ 会话状态 @@ -106,9 +65,8 @@ class ConversationState(rx.State): # 当前用户唯一标识 user_id: str = "" - - # 当前用户的会话字典 - conversations: Dict[str, Conversation] = {} + # 会话列表 + conversations: Dict[str, Conversation] = {} # 按照会话唯一标识顺序排序 # 当前会话唯一标识 conversation_id: str = "" @@ -118,9 +76,9 @@ class ConversationState(rx.State): # 会话创建状态,True表示正在创建,False表示未正在创建 is_conversation_creating: bool = False - async def load(self, user_id: str) -> None: + async def set_user_id(self, user_id: str) -> None: """ - 加载 + 设置用户唯一标识 :param user_id: 用户唯一标识 :return: None """ @@ -128,37 +86,68 @@ class ConversationState(rx.State): if not self.user_id: return - # 数据库状态 database_state = await self.get_state(DatabaseState) - # 获取会话字典 + # 获取会话列表 self.conversations = await database_state.get_conversations( user_id=self.user_id ) - # 若会话字典为空则先创建会话记录再在会话字典中添加 + # 若会话列表为空则先创建会话记录再添加会话实例 if not self.conversations: - conversation_id, created_at = ( + self.conversations.append( await database_state.create_conversations_record(user_id=self.user_id) ) - self.conversations[conversation_id] = Conversation(created_at=created_at) - - # 将最后一个会话的唯一标识设置为当前会话唯一标识 - self.conversation_id = next(reversed(self.conversations.keys())) + # 将最后一个会话作为当前会话 + self.idx = -1 + self.conversation = self.conversations[self.idx] + # 将当前会话设置为已激活 + self.conversation.is_actived = True + # 获取对话列表 + self.conversation.dialogs = await database_state.get_dialogs( + conversation_id=self.conversation.id + ) @rx.event def toggle_conversation_history_shown(self) -> None: """ - 切换会话历史展示状态 + 切换会话历史展示状态,用于前端是否渲染会话历史 :return: None """ self.is_conversation_history_shown = not self.is_conversation_history_shown + @staticmethod + def format_created_at(created_at: datetime) -> str: + """ + 格式化创建时间 + :param created_at: 创建时间 + :return: 格式化后的创建时间 + """ + match (datetime.now().date() - created_at.date()).days: + case 0: + formated_created_at = f"今天 {created_at.strftime('%H:%M')}" + case 1: + formated_created_at = f"昨天 {created_at.strftime('%H:%M')}" + case _: + formated_created_at = created_at.strftime("%Y-%m-%d %H:%M") + return formated_created_at + @rx.var - def conversation_history(self) -> Dict[str, Conversation]: + def conversation_history_items(self) -> List[ConversationHistoryItem]: """ - 获取会话历史,用于前端渲染会话历史 - :return: 会话历史 + 获取会话历史项列表,用于前端渲染会话历史 + :return: 会话历史项列表 """ - return dict(reversed(self.conversations.items())) + items = [] + for conversation in reversed(self.conversations): # 按照创建顺序倒序排序 + items.append( + ConversationHistoryItem( + id=conversation.id, + description=conversation.description, + created_at=self.format_created_at( + created_at=conversation.created_at + ), + ) + ) + return items @rx.event async def create_conversation(self, form_data: Dict[str, Any]) -> None: @@ -167,43 +156,44 @@ class ConversationState(rx.State): :param form_data: 表单数据 :return: None """ - # 获取会话描述 + # 解析会话描述 description = form_data["description"].strip() or "新会话" - # 先创建会话记录再在会话字典中添加 + # 创建会话记录再添加会话实例 database_state = await self.get_state(DatabaseState) - conversation_id, created_at = await database_state.create_conversations_record( - user_id=self.user_id, description=description + self.conversations.append( + await database_state.create_conversations_record( + user_id=self.user_id, description=description + ) ) - self.conversations[conversation_id] = Conversation( - description=description, created_at=created_at - ) - self.conversation_id = conversation_id + # 将当前会话设置为未激活 + self.conversation.is_actived = False + # 将最后一个会话作为当前会话并设置为已激活 + self.conversation = self.conversations[-1] + self.conversation.is_actived = True + + # 会话创建状态设置为未正在创建 self.is_conversation_creating = False @rx.event - async def delete_conversation(self, conversation_id: str) -> None: + async def delete_conversation(self, idx: int) -> None: """ - 删除指定会话 - :param conversation_id: 指定会话唯一标识 + 删除指定索引的会话 + :param idx: 指定索引 :return: None """ - if conversation_id not in self.conversations: - return - - # 先逻辑删除会话记录再在会话字典中删除会话 + # 先逻辑删除会话记录再在会话列表删除会话实例 database_state = await self.get_state(DatabaseState) await database_state.delete_conversations_record( - conversation_id=conversation_id + conversation_id=self.conversations[idx].id ) - del self.conversations[conversation_id] + del self.conversations[idx] - # 删除后,若会话字典为空则先创建会话记录再在会话字典中添加默认会话 + # 删除后,若会话列表为空则先创建会话记录再添加会话实例 if not self.conversations: - conversation_id, created_at = ( + self.conversations.append( await database_state.create_conversations_record(user_id=self.user_id) ) - self.conversations[conversation_id] = Conversation(created_at=created_at) # 删除后,若当前会话唯一标识不存在则将最后一个会话的唯一标识设置为当前会话唯一标识 if self.conversation_id not in self.conversations: diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 76956b4..cc9d8d7 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -2,31 +2,125 @@ """ 数据库状态 """ -from datetime import datetime -from typing import Dict, List +from datetime import datetime, timedelta +from random import choices +from typing import Dict, List, Tuple from pydantic import TypeAdapter from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter +from pydantic_ai._uuid import uuid7 import reflex as rx from sqlalchemy import desc -from sqlmodel import select, update +from sqlmodel import Field, JSON, SQLModel, select, update -from application.models import ( - CaptchaRecord, - Conversation, - ConversationRecord, - Dialog, - DialogRecord, - RunResultRecord, - ThoughtNode, - UserRecord, -) - -from datetime import datetime +from application.domain_models import Conversation, Dialog, Thought -# 思考节点领域模型类型适配器 -ThoughtNodeTypeAdapter = TypeAdapter(dict[int, ThoughtNode]) +class CaptchaRecord(SQLModel, table=True): + """ + 验证码记录 + 邮箱、验证码和失效时间联合作为主键 + """ + + email: str = Field(..., primary_key=True, description="邮箱") + captcha: str = Field( + default_factory=lambda: "".join(choices("0123456789", k=6)), + primary_key=True, + description="验证码", + ) + is_valid: bool = Field( + default=True, + index=True, + description="验证码有效:True 表示有效,False 表示无效", + ) + is_verified: bool = Field( + default=False, + index=True, + description="验证码已核验:True 表示已核验,False 表示未核验", + ) + expired_at: datetime = Field( + default_factory=lambda: datetime.now() + timedelta(minutes=30), + primary_key=True, + description="失效时间", + ) + + +class UserRecord(SQLModel, table=True): + """ + 用户记录 + """ + + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="用户唯一标识", + ) + email: str = Field(..., index=True, description="邮箱") + + +class ConversationRecord(SQLModel, table=True): + """ + 会话记录 + """ + + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="会话记录唯一标识", + ) + user_id: str = Field(..., index=True, description="用户唯一标识") + description: str = Field(..., description="会话描述") + is_deleted: bool = Field( + default=False, + index=True, + description="会话已删除:True 表示已删除,False 表示未删除", + ) + created_at: datetime = Field( + default_factory=datetime.now, description="会话创建时间" + ) + + +class ThoughtRecord(SQLModel): + """ + 思考记录(不创建数据表) + """ + + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="思考唯一标识", + ) + type: str = Field(description="思考类型") + content: str = Field(default="", description="思考内容") + + +class DialogRecord(SQLModel, table=True): + """ + 对话记录 + """ + + id: str = Field( + default_factory=lambda: str(uuid7()), + primary_key=True, + description="对话唯一标识", + ) + conversation_id: str = Field(..., index=True, description="会话唯一标识") + user_prompt: str = Field(..., description="用户提示词") + thoughts: List[ThoughtRecord] = Field( + default_factory=List, sa_type=JSON, description="思考列表" + ) + result_output: str = Field(default="", description="结果输出") + + +# 运行结果表 +class RunResultRecord(SQLModel, table=True): + conversation_id: str = Field(primary_key=True, description="会话唯一标识") + dialog_id: str = Field(primary_key=True, description="对话唯一标识") + new_messages: str = Field(description="新增消息") + + +# 思考领域模型类型适配器 +ThoughtTypeAdapter = TypeAdapter(List[Thought]) class DatabaseState(rx.State): @@ -66,7 +160,7 @@ class DatabaseState(rx.State): 核验验证码 :param email: 邮箱 :param captcha: 验证码 - :return: 是否核验成功,True 表示核验成功,False 表示核验失败(根据邮箱和验证码未找到有效且未核验的记录,或失效) + :return: 是否核验成功,True 表示核验成功,False 表示核验失败(根据邮箱和验证码未查询到有效且未核验的记录,或失效) """ async with rx.asession() as session: result = await session.exec( @@ -75,7 +169,7 @@ class DatabaseState(rx.State): CaptchaRecord.captcha == captcha, CaptchaRecord.is_valid == True, CaptchaRecord.is_verified == False, - CaptchaRecord.expired_at > datetime.now(), # 不区分是否过期 + CaptchaRecord.expired_at > datetime.now(), ) ) record = result.first() @@ -97,7 +191,7 @@ class DatabaseState(rx.State): select(UserRecord).where(UserRecord.email == email) ) record = result.first() - # 若记录存在则返回用户唯一标识,否则创建并返回所创建记录的用户唯一标识 + # 若记录已存在则返回用户唯一标识,否则创建并返回所创建记录的用户唯一标识 if record: return record.id record = UserRecord(email=email) @@ -108,63 +202,68 @@ class DatabaseState(rx.State): async def get_conversations(self, user_id: str) -> Dict[str, Conversation]: """ - 获取会话字典 + 获取会话列表 :param user_id: 用户唯一标识 - :return: 会话字典 + :return: 会话列表 """ records: Dict[str, Conversation] = {} async with rx.asession() as session: result = await session.exec( select(ConversationRecord, DialogRecord) - .outerjoin( - DialogRecord, - ConversationRecord.id == DialogRecord.conversation_id, # type: ignore - ) + .outerjoin(DialogRecord, ConversationRecord.id == DialogRecord.conversation_id) # type: ignore .where( ConversationRecord.user_id == user_id, ConversationRecord.is_deleted == False, ) .order_by(ConversationRecord.id, DialogRecord.id) ) - for conversation, dialog in result.all(): + for conversation_record, dialog_record in result.all(): record = records.setdefault( - conversation.id, + conversation_record.id, Conversation( - description=conversation.description, - created_at=conversation.created_at, + id=conversation_record.id, + description=conversation_record.description, + created_at=conversation_record.created_at, ), ) - if not dialog: + if not dialog_record: continue - record.dialogs[dialog.id] = Dialog( - question=dialog.question, - thought_nodes=ThoughtNodeTypeAdapter.validate_python( - dialog.thought_nodes - ), - answer=dialog.answer, + record.dialogs.append( + Dialog( + id=dialog_record.id, + user_prompt=dialog_record.user_prompt, + thoughts=ThoughtTypeAdapter.validate_python( + dialog_record.thoughts + ), + result_output=dialog_record.result_output, + ) ) return records async def create_conversations_record( self, user_id: str, description: str = "新会话" - ) -> tuple[str, datetime]: + ) -> Conversation: """ 创建会话记录 :param user_id: 用户唯一标识 - :param description: 会话描述 - :return: 创建会话记录的唯一标识和创建时间 + :param description: 会话描述,默认为"新会话" + :return: 会话实例 """ async with rx.asession() as session: record = ConversationRecord(user_id=user_id, description=description) session.add(record) await session.commit() await session.refresh(record) - return record.id, record.created_at + return Conversation( + id=record.id, + description=record.description, + created_at=record.created_at, + ) async def delete_conversations_record(self, conversation_id: str) -> None: """ 逻辑删除会话记录 - :param conversation_id: 指定会话记录唯一标识 + :param conversation_id: 指定会话唯一标识 :return: None """ async with rx.asession() as session: @@ -184,7 +283,7 @@ class DatabaseState(rx.State): async with rx.asession() as session: record = DialogRecord( conversation_id=conversation_id, - question=question, + user_prompt=question, ) session.add(record) await session.commit() @@ -194,24 +293,22 @@ class DatabaseState(rx.State): async def update_dialog_record( self, dialog_id: str, - thought_nodes: dict[int, ThoughtNode], - answer: str, + thoughts: List[Thought], + result_output: str, ) -> None: """ 更新对话记录 :param dialog_id: 对话唯一标识 - :param thought_nodes: 思考节点列表 - :param answer: 回答 + :param thoughts: 思考列表 + :param result_output: 结果输出 :return: None """ async with rx.asession() as session: record = await session.get(DialogRecord, dialog_id) if not record: return - record.thought_nodes = ThoughtNodeTypeAdapter.dump_python( - {k: v for k, v in thought_nodes.items()} - ) - record.answer = answer + record.thoughts = ThoughtTypeAdapter.dump_python(thoughts) + record.result_output = result_output await session.commit() async def create_run_result_record(