This commit is contained in:
liubiren 2026-07-24 15:59:48 +08:00
parent 345b443975
commit b2d78e73b3
6 changed files with 127 additions and 73 deletions

View File

@ -1,21 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from application.models.tables import ( from application.models.tables import (
Captchas, CaptchaRecord,
Conversations, ConversationRecord,
Dialogs, DialogRecord,
RunResults, RunResultRecord,
Users, UserRecord,
)
from application.models.domains import (
Conversation,
Dialog,
ThoughtNode,
) )
from application.models.domains import Conversation, Dialog, ThoughtNode
__all__ = [ __all__ = [
"Captchas", "CaptchaRecord",
"Conversation", "Conversation",
"Conversations", "ConversationRecord",
"Dialog", "Dialog",
"Dialogs", "DialogRecord",
"RunResults", "RunResultRecord",
"ThoughtNode", "ThoughtNode",
"Users", "UserRecord",
] ]

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
领域模型 reflex运行时领域模型
""" """
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict

View File

@ -9,21 +9,26 @@ from pydantic_ai._uuid import uuid7
from sqlmodel import Field, JSON, SQLModel from sqlmodel import Field, JSON, SQLModel
class CaptchaTable(SQLModel, table=True): class CaptchaRecord(SQLModel, table=True):
""" """
验证码表 验证码记录表
邮箱和创建时间联合作为主键
""" """
email: str = Field(..., primary_key=True, description="邮箱") email: str = Field(..., primary_key=True, description="邮箱")
captcha: str = Field( captcha: str = Field(
default_factory=lambda: "".join(choices("0123456789", k=6)), default_factory=lambda: "".join(choices("0123456789", k=6)),
primary_key=True,
description="验证码", description="验证码",
) )
is_valid: bool = Field(
default=True,
index=True,
description="验证码有效True 表示有效False 表示无效",
)
is_verified: bool = Field( is_verified: bool = Field(
default=False, default=False,
index=True, index=True,
description="已核验True 表示已核验False 表示未核验", description="验证码已核验True 表示已核验False 表示未核验",
) )
expired_at: datetime = Field( expired_at: datetime = Field(
default_factory=lambda: datetime.now() + timedelta(minutes=30), 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( id: str = Field(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
primary_key=True, 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( id: str = Field(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
primary_key=True, primary_key=True,
@ -52,18 +57,22 @@ class ConversationTable(SQLModel, table=True):
user_id: str = Field(..., index=True, description="用户唯一标识") user_id: str = Field(..., index=True, description="用户唯一标识")
description: str = Field(..., description="会话描述") description: str = Field(..., description="会话描述")
created_at: datetime = Field(default_factory=datetime.now, 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="思考节点类型") kind: str = Field(description="思考节点类型")
content: str = Field(default="", description="思考节点内容") content: str = Field(default="", description="思考节点内容")
# 对话表 # 对话表
class Dialogs(SQLModel, table=True): class DialogRecord(SQLModel, table=True):
id: str = Field( id: str = Field(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
primary_key=True, primary_key=True,
@ -71,14 +80,14 @@ class Dialogs(SQLModel, table=True):
) )
conversation_id: str = Field(..., index=True, description="会话唯一标识") conversation_id: str = Field(..., index=True, description="会话唯一标识")
question: str = Field(..., description="问题") question: str = Field(..., description="问题")
thought_nodes: dict[int, ThoughtNodes] = Field( thought_nodes: dict[int, ThoughtNodeRecord] = Field(
default_factory=dict, sa_type=JSON, description="思考节点字典" default_factory=dict, sa_type=JSON, description="思考节点字典"
) )
answer: str = Field(default="", description="回答") answer: str = Field(default="", description="回答")
# 运行结果表 # 运行结果表
class RunResults(SQLModel, table=True): class RunResultRecord(SQLModel, table=True):
conversation_id: str = Field(primary_key=True, description="会话唯一标识") conversation_id: str = Field(primary_key=True, description="会话唯一标识")
dialog_id: str = Field(primary_key=True, description="对话唯一标识") dialog_id: str = Field(primary_key=True, description="对话唯一标识")
new_messages: str = Field(description="新增消息") new_messages: str = Field(description="新增消息")

View File

@ -225,9 +225,7 @@ class AuthState(rx.State):
# 更新验证码记录 # 更新验证码记录
database_state = await self.get_state(DatabaseState) database_state = await self.get_state(DatabaseState)
if not await database_state.update_captcha( if not await database_state.get_captcha(email=self.email, captcha=self.captcha):
email=self.email, captcha=self.captcha
):
self.is_logging_in = False self.is_logging_in = False
self.login_error_message = "验证码错误" self.login_error_message = "验证码错误"
return return

View File

@ -2,9 +2,10 @@
""" """
会话状态 会话状态
""" """
from datetime import datetime
from typing import Any, AsyncGenerator, Dict from typing import Any, AsyncGenerator, Dict
from typing import Dict
from pydantic import BaseModel, Field
from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai import Agent, ThinkingPartDelta
from pydantic_ai.messages import ( from pydantic_ai.messages import (
FunctionToolCallEvent, FunctionToolCallEvent,
@ -24,10 +25,8 @@ from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
import reflex as rx import reflex as rx
from application.models import Conversation, Dialog, ThoughtNode
from application.states.database import DatabaseState from application.states.database import DatabaseState
instructions: str = """ instructions: str = """
# 角色 # 角色
专业友好AI助手结构化解答各类问题 专业友好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): class ConversationState(rx.State):
""" """
会话状态 会话状态
@ -68,7 +107,7 @@ class ConversationState(rx.State):
# 当前用户唯一标识 # 当前用户唯一标识
user_id: str = "" user_id: str = ""
# 会话字典 # 当前用户的会话字典
conversations: Dict[str, Conversation] = {} conversations: Dict[str, Conversation] = {}
# 当前会话唯一标识 # 当前会话唯一标识
conversation_id: str = "" conversation_id: str = ""
@ -93,7 +132,7 @@ class ConversationState(rx.State):
database_state = await self.get_state(DatabaseState) 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 user_id=self.user_id
) )
# 若会话字典为空则先创建会话记录再在会话字典中添加 # 若会话字典为空则先创建会话记录再在会话字典中添加

View File

@ -3,25 +3,27 @@
数据库状态 数据库状态
""" """
from datetime import datetime from datetime import datetime
from typing import Dict, List, cast from typing import Dict, List
from pydantic import TypeAdapter from pydantic import TypeAdapter
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter
import reflex as rx import reflex as rx
from sqlalchemy import ColumnElement, desc from sqlalchemy import desc
from sqlmodel import select, update from sqlmodel import select, update
from application.models import ( from application.models import (
Captchas, CaptchaRecord,
Conversation, Conversation,
Conversations, ConversationRecord,
Dialog, Dialog,
Dialogs, DialogRecord,
RunResults, RunResultRecord,
ThoughtNode, ThoughtNode,
Users, UserRecord,
) )
from datetime import datetime
# 思考节点领域模型类型适配器 # 思考节点领域模型类型适配器
ThoughtNodeTypeAdapter = TypeAdapter(dict[int, ThoughtNode]) 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: 邮箱 :param email: 邮箱
:return: 验证码 :return: 所创建记录的验证码
""" """
async with rx.asession() as session: async with rx.asession() as session:
# 前置处理:将该邮箱有效且未使用的验证码记录的是否有效设置为无效 # 前置操作:将该邮箱有效且未核验的验证码记录设置为无效
await session.exec( await session.exec(
update(Captchas) update(CaptchaRecord)
.where( .where(
Captchas.email == email, # type: ignore CaptchaRecord.email == email, # type: ignore
Captchas.is_valid == True, # type: ignore CaptchaRecord.is_valid == True, # type: ignore
Captchas.is_unused == True, # type: ignore CaptchaRecord.is_verified == False, # type: ignore
) )
.values(is_valid=False) .values(is_valid=False)
) )
await session.flush() await session.flush()
# 创建验证码记录 # 创建记录
record = Captchas( record = CaptchaRecord(
email=email, email=email,
) )
session.add(record) session.add(record)
@ -59,27 +61,27 @@ class DatabaseState(rx.State):
await session.refresh(record) await session.refresh(record)
return record.captcha 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 email: 邮箱
:param captcha: 验证码 :param captcha: 验证码
:return: 是否更新成功True 表示更新成功False 表示更新失败 :return: 是否更新成功True 表示更新成功False 表示更新失败
""" """
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(Captchas).where( select(CaptchaRecord).where(
Captchas.email == email, CaptchaRecord.email == email,
Captchas.captcha == captcha, CaptchaRecord.captcha == captcha,
Captchas.is_valid == True, CaptchaRecord.is_valid == True,
Captchas.is_unused == True, CaptchaRecord.is_verified == False,
Captchas.expired_at > datetime.now(), CaptchaRecord.expired_at > datetime.now(), # 不区分是否过期
) )
) )
record = result.first() record = result.first()
if not record: if not record:
return False return False
record.is_unused = False record.is_verified = True
await session.commit() await session.commit()
await session.refresh(record) await session.refresh(record)
return True return True
@ -91,18 +93,20 @@ class DatabaseState(rx.State):
:return: 创建用户记录的唯一标识 :return: 创建用户记录的唯一标识
""" """
async with rx.asession() as session: 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() record = result.first()
# 若用户记录存在则返回用户唯一标识,否则创建新用户记录并返回所创建的用户唯一标识 # 若用户记录存在则返回用户唯一标识,否则创建新用户记录并返回所创建的用户唯一标识
if record: if record:
return record.id return record.id
new_user = Users(email=email) new_user = UserRecord(email=email)
session.add(new_user) session.add(new_user)
await session.commit() await session.commit()
await session.refresh(new_user) await session.refresh(new_user)
return new_user.id 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: 用户唯一标识 :param user_id: 用户唯一标识
@ -111,16 +115,16 @@ class DatabaseState(rx.State):
records: Dict[str, Conversation] = {} records: Dict[str, Conversation] = {}
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(Conversations, Dialogs) select(ConversationRecord, DialogRecord)
.outerjoin( .outerjoin(
Dialogs, DialogRecord,
Conversations.id == Dialogs.conversation_id, # type: ignore ConversationRecord.id == DialogRecord.conversation_id, # type: ignore
) )
.where( .where(
Conversations.user_id == user_id, ConversationRecord.user_id == user_id,
Conversations.is_deleted == False, ConversationRecord.is_deleted == False,
) )
.order_by(Conversations.id, Dialogs.id) .order_by(ConversationRecord.id, DialogRecord.id)
) )
for conversation, dialog in result.all(): for conversation, dialog in result.all():
record = records.setdefault( record = records.setdefault(
@ -151,7 +155,7 @@ class DatabaseState(rx.State):
:return: 创建会话记录的唯一标识和创建时间 :return: 创建会话记录的唯一标识和创建时间
""" """
async with rx.asession() as session: 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) session.add(record)
await session.commit() await session.commit()
await session.refresh(record) await session.refresh(record)
@ -164,7 +168,7 @@ class DatabaseState(rx.State):
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = await session.get(Conversations, conversation_id) record = await session.get(ConversationRecord, conversation_id)
if not record: if not record:
return return
record.is_deleted = True record.is_deleted = True
@ -178,7 +182,7 @@ class DatabaseState(rx.State):
:return: 创建对话记录的唯一标识 :return: 创建对话记录的唯一标识
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = Dialogs( record = DialogRecord(
conversation_id=conversation_id, conversation_id=conversation_id,
question=question, question=question,
) )
@ -201,7 +205,7 @@ class DatabaseState(rx.State):
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = await session.get(Dialogs, dialog_id) record = await session.get(DialogRecord, dialog_id)
if not record: if not record:
return return
record.thought_nodes = ThoughtNodeTypeAdapter.dump_python( record.thought_nodes = ThoughtNodeTypeAdapter.dump_python(
@ -225,7 +229,7 @@ class DatabaseState(rx.State):
""" """
async with rx.asession() as session: async with rx.asession() as session:
session.add( session.add(
RunResults( RunResultRecord(
conversation_id=conversation_id, conversation_id=conversation_id,
dialog_id=dialog_id, dialog_id=dialog_id,
new_messages=ModelMessagesTypeAdapter.dump_json( new_messages=ModelMessagesTypeAdapter.dump_json(
@ -248,9 +252,9 @@ class DatabaseState(rx.State):
records: List[ModelMessage] = [] records: List[ModelMessage] = []
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(RunResults) select(RunResultRecord)
.where(RunResults.conversation_id == conversation_id) .where(RunResultRecord.conversation_id == conversation_id)
.order_by(desc(RunResults.dialog_id)) .order_by(desc(RunResultRecord.dialog_id))
) )
for record in result.all(): for record in result.all():
records.extend( records.extend(