diff --git a/agent/application/models/__init__.py b/agent/application/models/__init__.py index 270f843..81eaa42 100644 --- a/agent/application/models/__init__.py +++ b/agent/application/models/__init__.py @@ -1,21 +1,25 @@ # -*- coding: utf-8 -*- from application.models.tables import ( - Captchas, - Conversations, - Dialogs, - RunResults, - Users, + CaptchaRecord, + ConversationRecord, + DialogRecord, + RunResultRecord, + UserRecord, +) +from application.models.domains import ( + Conversation, + Dialog, + ThoughtNode, ) -from application.models.domains import Conversation, Dialog, ThoughtNode __all__ = [ - "Captchas", + "CaptchaRecord", "Conversation", - "Conversations", + "ConversationRecord", "Dialog", - "Dialogs", - "RunResults", + "DialogRecord", + "RunResultRecord", "ThoughtNode", - "Users", + "UserRecord", ] diff --git a/agent/application/models/domains.py b/agent/application/models/domains.py index 65285bc..77f477d 100644 --- a/agent/application/models/domains.py +++ b/agent/application/models/domains.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -领域模型 +reflex运行时领域模型 """ from datetime import datetime from typing import Dict diff --git a/agent/application/models/tables.py b/agent/application/models/tables.py index e5324de..a934c74 100644 --- a/agent/application/models/tables.py +++ b/agent/application/models/tables.py @@ -9,21 +9,26 @@ from pydantic_ai._uuid import uuid7 from sqlmodel import Field, JSON, SQLModel -class CaptchaTable(SQLModel, table=True): +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 表示未核验", + description="验证码已核验:True 表示已核验,False 表示未核验", ) expired_at: datetime = Field( default_factory=lambda: datetime.now() + timedelta(minutes=30), @@ -33,7 +38,7 @@ class CaptchaTable(SQLModel, table=True): # 用户表 -class UserTable(SQLModel, table=True): +class UserRecord(SQLModel, table=True): id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, @@ -43,7 +48,7 @@ class UserTable(SQLModel, table=True): # 会话表 -class ConversationTable(SQLModel, table=True): +class ConversationRecord(SQLModel, table=True): id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, @@ -52,18 +57,22 @@ class ConversationTable(SQLModel, table=True): 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 表示未删除") + is_deleted: bool = Field( + default=False, + index=True, + description="已删除:True 表示已删除,False 表示未删除", + ) # 思考节点表(仅定义) -class ThoughtNodes(SQLModel): +class ThoughtNodeRecord(SQLModel): kind: str = Field(description="思考节点类型") content: str = Field(default="", description="思考节点内容") # 对话表 -class Dialogs(SQLModel, table=True): +class DialogRecord(SQLModel, table=True): id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, @@ -71,14 +80,14 @@ class Dialogs(SQLModel, table=True): ) conversation_id: str = Field(..., index=True, description="会话唯一标识") question: str = Field(..., description="问题") - thought_nodes: dict[int, ThoughtNodes] = Field( + thought_nodes: dict[int, ThoughtNodeRecord] = Field( default_factory=dict, sa_type=JSON, description="思考节点字典" ) answer: str = Field(default="", description="回答") # 运行结果表 -class RunResults(SQLModel, table=True): +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 02ebf78..ac71e8c 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -225,9 +225,7 @@ class AuthState(rx.State): # 更新验证码记录 database_state = await self.get_state(DatabaseState) - if not await database_state.update_captcha( - email=self.email, captcha=self.captcha - ): + if not await database_state.get_captcha(email=self.email, captcha=self.captcha): self.is_logging_in = False self.login_error_message = "验证码错误" return diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index c8cd9dd..25a45e8 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -2,9 +2,10 @@ """ 会话状态 """ +from datetime import datetime from typing import Any, AsyncGenerator, Dict -from typing import Dict +from pydantic import BaseModel, Field from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai.messages import ( FunctionToolCallEvent, @@ -24,10 +25,8 @@ from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.run import AgentRunResultEvent import reflex as rx -from application.models import Conversation, Dialog, ThoughtNode from application.states.database import DatabaseState - instructions: str = """ # 角色 专业友好AI助手,结构化解答各类问题。 @@ -60,6 +59,46 @@ 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): """ 会话状态 @@ -68,7 +107,7 @@ class ConversationState(rx.State): # 当前用户唯一标识 user_id: str = "" - # 会话字典 + # 当前用户的会话字典 conversations: Dict[str, Conversation] = {} # 当前会话唯一标识 conversation_id: str = "" @@ -93,7 +132,7 @@ class ConversationState(rx.State): database_state = await self.get_state(DatabaseState) # 获取会话字典 - self.conversations = await database_state.retrieve_conversations( + self.conversations = await database_state.get_conversations( user_id=self.user_id ) # 若会话字典为空则先创建会话记录再在会话字典中添加 diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 32e8706..4cf96c5 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -3,25 +3,27 @@ 数据库状态 """ from datetime import datetime -from typing import Dict, List, cast +from typing import Dict, List from pydantic import TypeAdapter from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter import reflex as rx -from sqlalchemy import ColumnElement, desc +from sqlalchemy import desc from sqlmodel import select, update from application.models import ( - Captchas, + CaptchaRecord, Conversation, - Conversations, + ConversationRecord, Dialog, - Dialogs, - RunResults, + DialogRecord, + RunResultRecord, ThoughtNode, - Users, + UserRecord, ) +from datetime import datetime + # 思考节点领域模型类型适配器 ThoughtNodeTypeAdapter = TypeAdapter(dict[int, ThoughtNode]) @@ -32,26 +34,26 @@ class DatabaseState(rx.State): 数据库状态 """ - async def create_captchas_record(self, email: str) -> str: + async def create_captcha_record(self, email: str) -> str: """ 创建验证码记录 :param email: 邮箱 - :return: 验证码 + :return: 所创建记录的验证码 """ async with rx.asession() as session: - # 前置处理:将该邮箱有效且未使用的验证码记录的是否有效设置为无效 + # 前置操作:将该邮箱有效且未核验的验证码记录设置为无效 await session.exec( - update(Captchas) + update(CaptchaRecord) .where( - Captchas.email == email, # type: ignore - Captchas.is_valid == True, # type: ignore - Captchas.is_unused == True, # type: ignore + CaptchaRecord.email == email, # type: ignore + CaptchaRecord.is_valid == True, # type: ignore + CaptchaRecord.is_verified == False, # type: ignore ) .values(is_valid=False) ) await session.flush() - # 创建验证码记录 - record = Captchas( + # 创建记录 + record = CaptchaRecord( email=email, ) session.add(record) @@ -59,27 +61,27 @@ class DatabaseState(rx.State): await session.refresh(record) return record.captcha - async def update_captcha(self, email: str, captcha: str) -> bool: + async def verify_captcha(self, email: str, captcha: str) -> bool: """ - 更新验证码记录 + 核验验证码 :param email: 邮箱 :param captcha: 验证码 :return: 是否更新成功,True 表示更新成功,False 表示更新失败 """ async with rx.asession() as session: result = await session.exec( - select(Captchas).where( - Captchas.email == email, - Captchas.captcha == captcha, - Captchas.is_valid == True, - Captchas.is_unused == True, - Captchas.expired_at > datetime.now(), + select(CaptchaRecord).where( + CaptchaRecord.email == email, + CaptchaRecord.captcha == captcha, + CaptchaRecord.is_valid == True, + CaptchaRecord.is_verified == False, + CaptchaRecord.expired_at > datetime.now(), # 不区分是否过期 ) ) record = result.first() if not record: return False - record.is_unused = False + record.is_verified = True await session.commit() await session.refresh(record) return True @@ -91,18 +93,20 @@ class DatabaseState(rx.State): :return: 创建用户记录的唯一标识 """ async with rx.asession() as session: - result = await session.exec(select(Users).where(Users.email == email)) + result = await session.exec( + select(UserRecord).where(UserRecord.email == email) + ) record = result.first() # 若用户记录存在则返回用户唯一标识,否则创建新用户记录并返回所创建的用户唯一标识 if record: return record.id - new_user = Users(email=email) + new_user = UserRecord(email=email) session.add(new_user) await session.commit() await session.refresh(new_user) return new_user.id - async def retrieve_conversations(self, user_id: str) -> Dict[str, Conversation]: + async def get_conversations(self, user_id: str) -> Dict[str, Conversation]: """ 获取会话字典 :param user_id: 用户唯一标识 @@ -111,16 +115,16 @@ class DatabaseState(rx.State): records: Dict[str, Conversation] = {} async with rx.asession() as session: result = await session.exec( - select(Conversations, Dialogs) + select(ConversationRecord, DialogRecord) .outerjoin( - Dialogs, - Conversations.id == Dialogs.conversation_id, # type: ignore + DialogRecord, + ConversationRecord.id == DialogRecord.conversation_id, # type: ignore ) .where( - Conversations.user_id == user_id, - Conversations.is_deleted == False, + ConversationRecord.user_id == user_id, + ConversationRecord.is_deleted == False, ) - .order_by(Conversations.id, Dialogs.id) + .order_by(ConversationRecord.id, DialogRecord.id) ) for conversation, dialog in result.all(): record = records.setdefault( @@ -151,7 +155,7 @@ class DatabaseState(rx.State): :return: 创建会话记录的唯一标识和创建时间 """ async with rx.asession() as session: - record = Conversations(user_id=user_id, description=description) + record = ConversationRecord(user_id=user_id, description=description) session.add(record) await session.commit() await session.refresh(record) @@ -164,7 +168,7 @@ class DatabaseState(rx.State): :return: None """ async with rx.asession() as session: - record = await session.get(Conversations, conversation_id) + record = await session.get(ConversationRecord, conversation_id) if not record: return record.is_deleted = True @@ -178,7 +182,7 @@ class DatabaseState(rx.State): :return: 创建对话记录的唯一标识 """ async with rx.asession() as session: - record = Dialogs( + record = DialogRecord( conversation_id=conversation_id, question=question, ) @@ -201,7 +205,7 @@ class DatabaseState(rx.State): :return: None """ async with rx.asession() as session: - record = await session.get(Dialogs, dialog_id) + record = await session.get(DialogRecord, dialog_id) if not record: return record.thought_nodes = ThoughtNodeTypeAdapter.dump_python( @@ -225,7 +229,7 @@ class DatabaseState(rx.State): """ async with rx.asession() as session: session.add( - RunResults( + RunResultRecord( conversation_id=conversation_id, dialog_id=dialog_id, new_messages=ModelMessagesTypeAdapter.dump_json( @@ -248,9 +252,9 @@ class DatabaseState(rx.State): records: List[ModelMessage] = [] async with rx.asession() as session: result = await session.exec( - select(RunResults) - .where(RunResults.conversation_id == conversation_id) - .order_by(desc(RunResults.dialog_id)) + select(RunResultRecord) + .where(RunResultRecord.conversation_id == conversation_id) + .order_by(desc(RunResultRecord.dialog_id)) ) for record in result.all(): records.extend(