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 -*-
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",
]

View File

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

View File

@ -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="新增消息")

View File

@ -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

View File

@ -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
)
# 若会话字典为空则先创建会话记录再在会话字典中添加

View File

@ -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(