385 lines
12 KiB
Python
385 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
数据库状态
|
||
"""
|
||
from datetime import datetime, timedelta
|
||
from random import choices
|
||
from typing import Dict, List
|
||
|
||
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 Field, JSON, SQLModel, select, update
|
||
|
||
from application.domain_models import Conversation, Dialog, Thought
|
||
|
||
|
||
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: Dict[int, ThoughtRecord] = Field(
|
||
default_factory=dict, sa_type=JSON, description="思考列表"
|
||
)
|
||
result_output: str = Field(default="", description="结果输出")
|
||
|
||
|
||
class ResultRecord(SQLModel, table=True):
|
||
"""
|
||
结果记录
|
||
"""
|
||
|
||
conversation_id: str = Field(primary_key=True, description="会话唯一标识")
|
||
dialog_id: str = Field(primary_key=True, description="对话唯一标识")
|
||
new_messages: str = Field(description="新增消息")
|
||
|
||
|
||
def format_at(at: datetime) -> str:
|
||
"""
|
||
格式化日期时间
|
||
:param at: 日期时间
|
||
:return: 格式化后的日期时间字符串
|
||
"""
|
||
match (datetime.now().date() - at.date()).days:
|
||
case 0:
|
||
formatted_at = f"{at.strftime('%H:%M')}"
|
||
case 1:
|
||
formatted_at = f"昨天 {at.strftime('%H:%M')}"
|
||
case _:
|
||
formatted_at = at.strftime("%Y-%m-%d %H:%M")
|
||
return formatted_at
|
||
|
||
|
||
# 思考领域模型类型适配器
|
||
ThoughtTypeAdapter = TypeAdapter(Dict[int, Thought])
|
||
|
||
|
||
class DatabaseState(rx.State):
|
||
"""
|
||
数据库状态
|
||
"""
|
||
|
||
async def create_captcha_record(self, email: str) -> str:
|
||
"""
|
||
创建验证码记录
|
||
:param email: 邮箱
|
||
:return: 所创建记录的验证码
|
||
"""
|
||
async with rx.asession() as session:
|
||
# 前置操作:将该邮箱有效且未核验的验证码记录设置为无效
|
||
await session.exec(
|
||
update(CaptchaRecord)
|
||
.where(
|
||
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 = CaptchaRecord(
|
||
email=email,
|
||
)
|
||
session.add(record)
|
||
await session.commit()
|
||
await session.refresh(record)
|
||
return record.captcha
|
||
|
||
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(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_verified = True
|
||
await session.commit()
|
||
await session.refresh(record)
|
||
return True
|
||
|
||
async def create_user_record(self, email: str) -> str:
|
||
"""
|
||
创建用户记录
|
||
:param email: 邮箱
|
||
:return: 创建用户记录的唯一标识
|
||
"""
|
||
async with rx.asession() as session:
|
||
result = await session.exec(
|
||
select(UserRecord).where(UserRecord.email == email)
|
||
)
|
||
record = result.first()
|
||
# 若记录已存在则返回用户唯一标识,否则创建并返回所创建记录的用户唯一标识
|
||
if record:
|
||
return record.id
|
||
record = UserRecord(email=email)
|
||
session.add(record)
|
||
await session.commit()
|
||
await session.refresh(record)
|
||
return record.id
|
||
|
||
async def get_conversations(self, user_id: str) -> Dict[str, Conversation]:
|
||
"""
|
||
获取会话字典
|
||
:param user_id: 用户唯一标识
|
||
: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
|
||
.where(
|
||
ConversationRecord.user_id == user_id,
|
||
ConversationRecord.is_deleted == False,
|
||
)
|
||
.order_by(ConversationRecord.id, DialogRecord.id)
|
||
)
|
||
for conversation_record, dialog_record in result.all():
|
||
|
||
record = records.setdefault(
|
||
conversation_record.id,
|
||
Conversation(
|
||
id=conversation_record.id,
|
||
description=conversation_record.description,
|
||
created_at=format_at(conversation_record.created_at),
|
||
),
|
||
)
|
||
if not dialog_record:
|
||
continue
|
||
record.dialogs.update(
|
||
{
|
||
dialog_record.id: 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 = "新会话"
|
||
) -> Dict[str, Conversation]:
|
||
"""
|
||
创建会话记录
|
||
:param user_id: 用户唯一标识
|
||
: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: Conversation(
|
||
id=record.id,
|
||
description=record.description,
|
||
created_at=format_at(record.created_at),
|
||
)
|
||
}
|
||
|
||
async def set_conversations_record_deleted(self, conversation_id: str) -> None:
|
||
"""
|
||
设置会话记录为已删除
|
||
:param conversation_id: 指定会话唯一标识
|
||
:return: None
|
||
"""
|
||
async with rx.asession() as session:
|
||
record = await session.get(ConversationRecord, conversation_id)
|
||
if not record:
|
||
return
|
||
record.is_deleted = True
|
||
await session.commit()
|
||
|
||
async def create_dialog_record(
|
||
self, conversation_id: str, user_prompt: str
|
||
) -> Dict[str, Dialog]:
|
||
"""
|
||
创建对话记录
|
||
:param conversation_id: 会话唯一标识
|
||
:param user_prompt: 用户提示词
|
||
:return: 创建对话记录的唯一标识
|
||
"""
|
||
async with rx.asession() as session:
|
||
record = DialogRecord(
|
||
conversation_id=conversation_id,
|
||
user_prompt=user_prompt,
|
||
)
|
||
session.add(record)
|
||
await session.commit()
|
||
await session.refresh(record)
|
||
return {record.id: Dialog(id=record.id, user_prompt=user_prompt)}
|
||
|
||
async def complete_dialog_record(
|
||
self,
|
||
id: str,
|
||
thoughts: Dict[int, Thought],
|
||
result_output: str,
|
||
) -> None:
|
||
"""
|
||
补全对话记录
|
||
:param id: 对话唯一标识
|
||
:param thoughts: 思考列表
|
||
:param result_output: 结果输出
|
||
:return: None
|
||
"""
|
||
async with rx.asession() as session:
|
||
record = await session.get(DialogRecord, id) # 通过主键查询记录
|
||
if not record:
|
||
return
|
||
record.thoughts = ThoughtTypeAdapter.dump_python(thoughts)
|
||
record.result_output = result_output
|
||
await session.commit()
|
||
|
||
async def create_result_record(
|
||
self,
|
||
conversation_id: str,
|
||
dialog_id: str,
|
||
new_messages: List[ModelMessage],
|
||
) -> None:
|
||
"""
|
||
创建结果记录
|
||
:param conversation_id: 会话唯一标识
|
||
:param dialog_id: 对话唯一标识
|
||
:param new_messages: 新增消息
|
||
:return: None
|
||
"""
|
||
async with rx.asession() as session:
|
||
session.add(
|
||
ResultRecord(
|
||
conversation_id=conversation_id,
|
||
dialog_id=dialog_id,
|
||
new_messages=ModelMessagesTypeAdapter.dump_json(
|
||
new_messages
|
||
).decode(
|
||
"utf-8"
|
||
), # 序列化为 JSON 字符串
|
||
)
|
||
)
|
||
await session.commit()
|
||
|
||
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
|
||
"""
|
||
获取消息历史
|
||
:param conversation_id: 会话唯一标识
|
||
:return: 消息历史
|
||
"""
|
||
records: List[ModelMessage] = []
|
||
async with rx.asession() as session:
|
||
result = await session.exec(
|
||
select(ResultRecord)
|
||
.where(ResultRecord.conversation_id == conversation_id)
|
||
.order_by(desc(ResultRecord.dialog_id))
|
||
)
|
||
for record in result.all():
|
||
records.extend(
|
||
ModelMessagesTypeAdapter.validate_json(record.new_messages)
|
||
)
|
||
return records
|