This commit is contained in:
parent
026c573556
commit
483b7dc7ea
|
|
@ -25,7 +25,7 @@ def conversation_history_item_showing(
|
||||||
|
|
||||||
# 高亮:若为当前会话或显示更多则高亮
|
# 高亮:若为当前会话或显示更多则高亮
|
||||||
highlight: bool = (
|
highlight: bool = (
|
||||||
conversation.id == ConversationState.shown_more_conversation_id
|
conversation.id == ConversationState.is_conversation_history_item_more_button_shown
|
||||||
) | (conversation.id == ConversationState.conversation_id)
|
) | (conversation.id == ConversationState.conversation_id)
|
||||||
|
|
||||||
return rx.list.item(
|
return rx.list.item(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
会话状态
|
会话状态
|
||||||
"""
|
"""
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
FunctionToolCallEvent,
|
FunctionToolCallEvent,
|
||||||
|
|
@ -24,15 +25,28 @@ import reflex as rx
|
||||||
from application.states.database import DatabaseState
|
from application.states.database import DatabaseState
|
||||||
from application.states.models import (
|
from application.states.models import (
|
||||||
Conversation,
|
Conversation,
|
||||||
Dialog,
|
ConversationHistoryItem,
|
||||||
TaskNodeResultEvent,
|
|
||||||
Thought,
|
|
||||||
thoughts_to_dict,
|
|
||||||
usage_to_dict,
|
usage_to_dict,
|
||||||
)
|
)
|
||||||
from application.tasks import run_stream_events
|
from application.tasks import run_stream_events
|
||||||
|
|
||||||
|
|
||||||
|
def format_conversation_history_item_created_at(created_at: datetime) -> str:
|
||||||
|
"""
|
||||||
|
格式化会话历史项创建日期时间
|
||||||
|
:param created_at: 创建日期时间
|
||||||
|
:return: 格式化后的日期时间字符串
|
||||||
|
"""
|
||||||
|
match (datetime.now().date() - created_at.date()).days:
|
||||||
|
case 0:
|
||||||
|
formatted_created_at = f"{created_at.strftime('%H:%M')}"
|
||||||
|
case 1:
|
||||||
|
formatted_created_at = f"昨天 {created_at.strftime('%H:%M')}"
|
||||||
|
case _:
|
||||||
|
formatted_created_at = created_at.strftime("%Y-%m-%d %H:%M")
|
||||||
|
return formatted_created_at
|
||||||
|
|
||||||
|
|
||||||
class ConversationState(rx.State):
|
class ConversationState(rx.State):
|
||||||
"""
|
"""
|
||||||
会话状态
|
会话状态
|
||||||
|
|
@ -40,70 +54,86 @@ class ConversationState(rx.State):
|
||||||
|
|
||||||
# 当前用户唯一标识
|
# 当前用户唯一标识
|
||||||
user_id: str = ""
|
user_id: str = ""
|
||||||
# 键为会话唯一标识,值为会话实例的会话字典
|
# 当前用户的会话字典(私有变量)
|
||||||
conversations: dict[str, Conversation] = {} # 按照会话唯一标识顺序排序
|
# 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用
|
||||||
# 当前会话唯一标识
|
_conversations: dict[str, Conversation] = {} # 键为会话唯一标识,值为会话实例
|
||||||
conversation_id: str = ""
|
# 激活会话唯一标识
|
||||||
|
actived_conversation_id: str = ""
|
||||||
|
|
||||||
# 会话历史展示状态,True表示展示,False表示隐藏
|
# 显示会话历史,True 表示显示,False 表示隐藏
|
||||||
is_conversation_history_shown: bool = False
|
is_conversation_history_shown: bool = False
|
||||||
# 显示更多的会话唯一标识
|
# 会话历史项显示悬停气泡,True 表示显示,False 表示隐藏
|
||||||
shown_more_conversation_id: str = ""
|
is_conversation_history_item_popover_shown: bool = False
|
||||||
|
|
||||||
# 用户提示词
|
# 用户提示词
|
||||||
user_prompt: str = ""
|
user_prompt: str = ""
|
||||||
|
|
||||||
# 数据库状态(私有变量,不予序列化)
|
# 当前数据库状态(私有变量)
|
||||||
_db_state: DatabaseState | None = None
|
_db_state: DatabaseState | None = None
|
||||||
|
|
||||||
async def get_db_state(self) -> DatabaseState:
|
async def get_db_state(self) -> DatabaseState:
|
||||||
"""
|
"""
|
||||||
获取数据库状态
|
获取当前数据库状态
|
||||||
:return: 数据库状态
|
:return: 当前数据库状态
|
||||||
"""
|
"""
|
||||||
|
# 若数据库状态为 None 则获取数据库状态,否则直接返回当前数据库状态
|
||||||
if not self._db_state:
|
if not self._db_state:
|
||||||
self._db_state = await self.get_state(DatabaseState)
|
self._db_state = await self.get_state(DatabaseState)
|
||||||
return self._db_state
|
return self._db_state
|
||||||
|
|
||||||
async def resume(self, user_id: str) -> None:
|
async def resume_conversation_state(self, user_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
恢复当前用户会话状态
|
恢复当前用户的会话状态
|
||||||
:param user_id: 用户唯一标识
|
:param user_id: 用户唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.user_id = user_id
|
self.user_id = user_id.strip()
|
||||||
if not self.user_id:
|
if not self.user_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 获取数据库状态
|
# 获取当前数据库状态
|
||||||
db_state = await self.get_db_state()
|
db_state = await self.get_db_state()
|
||||||
# 获取会话字典
|
# 获取当前用户的会话字典
|
||||||
self.conversations = await db_state.get_conversations(user_id=self.user_id)
|
self._conversations = await db_state.get_conversations(self.user_id)
|
||||||
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
|
# 若当前用户的会话字典为空则先创建会话记录再添加会话实例
|
||||||
if not self.conversations:
|
if not self._conversations:
|
||||||
self.conversations.update(
|
self._conversations.update(
|
||||||
await db_state.create_conversations_record(user_id=self.user_id)
|
await db_state.create_conversation_record(self.user_id)
|
||||||
)
|
)
|
||||||
# 将最后一个会话作为当前会话并更新会话唯一标识
|
# 将最后一个会话的唯一标识作为激活会话唯一标识
|
||||||
self.conversation_id = next(reversed(self.conversations.keys()))
|
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def toggle_conversation_history_shown(self) -> None:
|
def toggle_conversation_history_shown(self) -> None:
|
||||||
"""
|
"""
|
||||||
切换会话历史展示状态,用于前端是否渲染会话历史
|
显示 / 隐藏会话历史
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.is_conversation_history_shown = not self.is_conversation_history_shown
|
self.is_conversation_history_shown = not self.is_conversation_history_shown
|
||||||
|
|
||||||
@rx.event
|
@rx.var
|
||||||
def set_shown_more_conversation_id(
|
def conversation_history_items(self) -> list[ConversationHistoryItem]:
|
||||||
self, conversation_id: str, is_shown: bool
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
设置显示更多的会话唯一标识
|
获取会话历史项列表(按照会话创建日期时间降序排序)
|
||||||
|
:return: 会话历史项列表
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
ConversationHistoryItem(
|
||||||
|
description=conversation.description,
|
||||||
|
created_at=format_conversation_history_item_created_at(
|
||||||
|
conversation.created_at
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for conversation in reversed(self._conversations.values())
|
||||||
|
]
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def set_conversation_history_item_popover_shown(self, is_shown: bool) -> None:
|
||||||
|
"""
|
||||||
|
设置会话历史项悬停气泡显示 / 隐藏
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.shown_more_conversation_id = conversation_id if is_shown else ""
|
self.is_conversation_history_item_popover_shown = is_shown
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
async def delete_conversation(self, conversation_id: str) -> None:
|
async def delete_conversation(self, conversation_id: str) -> None:
|
||||||
|
|
@ -112,30 +142,29 @@ class ConversationState(rx.State):
|
||||||
:param conversation_id: 需删除的会话唯一标识
|
:param conversation_id: 需删除的会话唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 获取数据库状态
|
# 获取当前数据库状态
|
||||||
db_state = await self.get_db_state()
|
db_state = await self.get_db_state()
|
||||||
# 先设置会话记录为已删除再在会话字典中删除会话实例
|
await db_state.delete_conversation_record(conversation_id)
|
||||||
await db_state.delete_conversations_record(conversation_id=conversation_id)
|
del self._conversations[conversation_id]
|
||||||
del self.conversations[conversation_id]
|
|
||||||
|
|
||||||
# 删除后,若会话字典为空则先创建会话记录再添加会话实例
|
# 删除后,若当前用户的会话字典为空则创建会话
|
||||||
if not self.conversations:
|
if not self._conversations:
|
||||||
self.conversations.update(
|
self._conversations.update(
|
||||||
await db_state.create_conversations_record(user_id=self.user_id)
|
await db_state.create_conversation_record(self.user_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识
|
# 删除后,若激活会话唯一标识不存在则将最后一个会话的唯一标识作为激活会话唯一标识
|
||||||
if self.conversation_id not in self.conversations:
|
if self.actived_conversation_id not in self._conversations:
|
||||||
self.conversation_id = next(reversed(self.conversations.keys()))
|
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def switch_conversation(self, conversation_id: str) -> None:
|
def set_actived_conversation(self, conversation_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
切换会话
|
设置激活会话
|
||||||
:param conversation_id: 需切换的会话唯一标识
|
:param conversation_id: 会话唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.conversation_id = conversation_id
|
self.actived_conversation_id = conversation_id
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
async def create_conversation(self) -> None:
|
async def create_conversation(self) -> None:
|
||||||
|
|
@ -143,16 +172,14 @@ class ConversationState(rx.State):
|
||||||
创建会话
|
创建会话
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# 获取数据库状态
|
# 获取当前数据库状态
|
||||||
db_state = await self.get_db_state()
|
db_state = await self.get_db_state()
|
||||||
# 创建会话记录再添加会话实例
|
# 创建会话记录再添加会话实例
|
||||||
self.conversations.update(
|
self._conversations.update(
|
||||||
await db_state.create_conversations_record(
|
await db_state.create_conversation_record(self.user_id)
|
||||||
user_id=self.user_id, description="新会话"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# 将最后一个会话作为当前会话并更新会话唯一标识
|
# 将最后一个会话的唯一标识作为激活会话唯一标识
|
||||||
self.conversation_id = next(reversed(self.conversations.keys()))
|
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def set_user_prompt(self, user_prompt: str) -> None:
|
def set_user_prompt(self, user_prompt: str) -> None:
|
||||||
|
|
@ -164,13 +191,13 @@ class ConversationState(rx.State):
|
||||||
self.user_prompt = user_prompt.strip()
|
self.user_prompt = user_prompt.strip()
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def is_user_prompt_sending_disabled(self) -> bool:
|
def is_user_prompt_send_button_disabled(self) -> bool:
|
||||||
"""
|
"""
|
||||||
用户提示词发送禁用状态
|
用户提示词发送按钮不可点击
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
# 当前会话
|
# 当前会话
|
||||||
conversation = self.conversations.get(self.conversation_id)
|
conversation = self._conversations.get(self.actived_conversation_id)
|
||||||
if not conversation:
|
if not conversation:
|
||||||
return True
|
return True
|
||||||
return conversation.is_running or not self.user_prompt
|
return conversation.is_running or not self.user_prompt
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,29 @@ import reflex as rx
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
from sqlmodel import Field, JSON, SQLModel, select, update
|
from sqlmodel import Field, JSON, SQLModel, select, update
|
||||||
|
|
||||||
from application.states.models import Conversation, Dialog
|
from application.states.models import (
|
||||||
|
Conversation,
|
||||||
|
TaskType,
|
||||||
|
TaskStatus,
|
||||||
|
RunStatus,
|
||||||
|
MessageType,
|
||||||
|
Task,
|
||||||
|
Run,
|
||||||
|
Message,
|
||||||
|
deps_to_object,
|
||||||
|
usage_to_object,
|
||||||
|
usage_limits_to_object,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CaptchaRecord(SQLModel, table=True):
|
class VerificationCodeTable(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
验证码记录
|
验证码表
|
||||||
邮箱、验证码和失效时间联合作为主键
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
email: str = Field(..., primary_key=True, description="邮箱")
|
email: str = Field(..., primary_key=True, description="邮箱")
|
||||||
captcha: str = Field(
|
verification_code: str = Field(
|
||||||
default_factory=lambda: "".join(choices("0123456789", k=6)),
|
default_factory=lambda: "".join(choices("0123456789", k=6)),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
description="验证码",
|
description="验证码",
|
||||||
|
|
@ -40,13 +52,13 @@ class CaptchaRecord(SQLModel, table=True):
|
||||||
expired_at: datetime = Field(
|
expired_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now() + timedelta(minutes=30),
|
default_factory=lambda: datetime.now() + timedelta(minutes=30),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
description="失效时间",
|
description="过期时间",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserRecord(SQLModel, table=True):
|
class UserTable(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
用户记录
|
用户表
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
|
|
@ -57,18 +69,18 @@ class UserRecord(SQLModel, table=True):
|
||||||
email: str = Field(..., index=True, description="邮箱")
|
email: str = Field(..., index=True, description="邮箱")
|
||||||
|
|
||||||
|
|
||||||
class ConversationRecord(SQLModel, table=True):
|
class ConversationTable(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,
|
||||||
description="会话记录唯一标识",
|
description="会话唯一标识",
|
||||||
)
|
)
|
||||||
user_id: str = Field(..., index=True, description="用户唯一标识")
|
user_id: str = Field(..., index=True, description="用户唯一标识")
|
||||||
description: str = Field(..., description="会话描述")
|
description: str = Field(default="新会话", description="会话描述")
|
||||||
is_deleted: bool = Field(
|
is_deleted: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
index=True,
|
index=True,
|
||||||
|
|
@ -79,51 +91,67 @@ class ConversationRecord(SQLModel, table=True):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DialogRecord(SQLModel, table=True):
|
class TaskTable(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
对话记录
|
任务表
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
...,
|
default_factory=lambda: str(uuid7()),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
description="对话唯一标识",
|
description="任务唯一标识",
|
||||||
)
|
)
|
||||||
conversation_id: str = Field(..., index=True, description="会话唯一标识")
|
conversation_id: str = Field(..., index=True, description="会话唯一标识")
|
||||||
user_prompt: str = Field(..., description="用户提示词")
|
type: TaskType = Field(default=TaskType.CHAT, description="任务类型")
|
||||||
thoughts: dict[int, Any] = Field(
|
status: TaskStatus = Field(default=TaskStatus.NONE, description="任务状态")
|
||||||
default_factory=dict, sa_type=JSON, description="思考列表"
|
deps: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, sa_type=JSON, description="任务依赖项"
|
||||||
)
|
)
|
||||||
result_output: str = Field(default="", description="结果输出")
|
|
||||||
usage: dict[str, Any] = Field(
|
usage: dict[str, Any] = Field(
|
||||||
default_factory=dict, sa_type=JSON, description="使用量"
|
default_factory=dict, sa_type=JSON, description="任务使用量"
|
||||||
|
)
|
||||||
|
usage_limits: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, sa_type=JSON, description="任务使用量限制"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResultRecord(SQLModel, table=True):
|
class RunTable(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
结果记录
|
运行表
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conversation_id: str = Field(primary_key=True, description="会话唯一标识")
|
id: str = Field(
|
||||||
dialog_id: str = Field(primary_key=True, description="对话唯一标识")
|
default_factory=lambda: str(uuid7()),
|
||||||
new_messages: str = Field(description="新增消息")
|
primary_key=True,
|
||||||
|
description="运行唯一标识",
|
||||||
|
)
|
||||||
|
task_id: str = Field(..., index=True, description="任务唯一标识")
|
||||||
|
status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态")
|
||||||
|
usage: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, sa_type=JSON, description="运行使用量"
|
||||||
|
)
|
||||||
|
usage_limits: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, sa_type=JSON, description="运行使用量限制"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_at(at: datetime) -> str:
|
class MessageTable(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
格式化日期时间
|
消息表
|
||||||
:param at: 日期时间
|
|
||||||
:return: 格式化后的日期时间字符串
|
|
||||||
"""
|
"""
|
||||||
match (datetime.now().date() - at.date()).days:
|
|
||||||
case 0:
|
id: str = Field(
|
||||||
formatted_at = f"{at.strftime('%H:%M')}"
|
default_factory=lambda: str(uuid7()),
|
||||||
case 1:
|
primary_key=True,
|
||||||
formatted_at = f"昨天 {at.strftime('%H:%M')}"
|
description="消息唯一标识",
|
||||||
case _:
|
)
|
||||||
formatted_at = at.strftime("%Y-%m-%d %H:%M")
|
run_id: str = Field(..., index=True, description="运行唯一标识")
|
||||||
return formatted_at
|
type: MessageType = Field(default=MessageType.USER_PROMPT, description="消息类型")
|
||||||
|
title: str = Field(default="", description="消息标题")
|
||||||
|
content: str = Field(default="", description="消息内容")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseState(rx.State):
|
class DatabaseState(rx.State):
|
||||||
|
|
@ -131,48 +159,51 @@ class DatabaseState(rx.State):
|
||||||
数据库状态
|
数据库状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def create_captcha_record(self, email: str) -> str:
|
async def create_verification_code_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(CaptchaRecord)
|
update(VerificationCodeTable)
|
||||||
.where(
|
.where(
|
||||||
CaptchaRecord.email == email, # type: ignore
|
VerificationCodeTable.email == email, # type: ignore
|
||||||
CaptchaRecord.is_valid == True, # type: ignore
|
VerificationCodeTable.is_valid == True, # type: ignore
|
||||||
CaptchaRecord.is_verified == False, # type: ignore
|
VerificationCodeTable.is_verified == False, # type: ignore
|
||||||
)
|
)
|
||||||
.values(is_valid=False)
|
.values(is_valid=False)
|
||||||
)
|
)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
# 创建记录
|
# 创建验证码记录
|
||||||
record = CaptchaRecord(
|
record = VerificationCodeTable(
|
||||||
email=email,
|
email=email,
|
||||||
)
|
)
|
||||||
session.add(record)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(record)
|
await session.refresh(record)
|
||||||
return record.captcha
|
return record.verification_code
|
||||||
|
|
||||||
async def verify_captcha(self, email: str, captcha: str) -> bool:
|
async def verify_verification_code(
|
||||||
|
self, email: str, verification_code: str
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
核验验证码
|
核验验证码
|
||||||
:param email: 邮箱
|
:param email: 邮箱
|
||||||
:param captcha: 验证码
|
:param verification_code: 验证码
|
||||||
: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(CaptchaRecord).where(
|
select(VerificationCodeTable).where(
|
||||||
CaptchaRecord.email == email,
|
VerificationCodeTable.email == email,
|
||||||
CaptchaRecord.captcha == captcha,
|
VerificationCodeTable.verification_code == verification_code,
|
||||||
CaptchaRecord.is_valid == True,
|
VerificationCodeTable.is_valid == True,
|
||||||
CaptchaRecord.is_verified == False,
|
VerificationCodeTable.is_verified == False,
|
||||||
CaptchaRecord.expired_at > datetime.now(),
|
VerificationCodeTable.expired_at > datetime.now(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
record = result.first()
|
record = result.first()
|
||||||
|
|
@ -187,17 +218,17 @@ class DatabaseState(rx.State):
|
||||||
"""
|
"""
|
||||||
创建用户记录
|
创建用户记录
|
||||||
:param email: 邮箱
|
:param email: 邮箱
|
||||||
:return: 创建用户记录的唯一标识
|
:return: 用户唯一标识
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
result = await session.exec(
|
result = await session.exec(
|
||||||
select(UserRecord).where(UserRecord.email == email)
|
select(UserTable).where(UserTable.email == email)
|
||||||
)
|
)
|
||||||
record = result.first()
|
record = result.first()
|
||||||
# 若记录已存在则返回用户唯一标识,否则创建并返回所创建记录的用户唯一标识
|
# 若用户记录已存在则返回用户唯一标识,否则先创建用户记录再返回用户唯一标识
|
||||||
if record:
|
if record:
|
||||||
return record.id
|
return record.id
|
||||||
record = UserRecord(email=email)
|
record = UserTable(email=email)
|
||||||
session.add(record)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(record)
|
await session.refresh(record)
|
||||||
|
|
@ -205,56 +236,69 @@ class DatabaseState(rx.State):
|
||||||
|
|
||||||
async def get_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: 用户唯一标识
|
||||||
:return: 会话字典
|
:return: 当前用户的会话字典
|
||||||
"""
|
"""
|
||||||
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(ConversationRecord, DialogRecord)
|
select(ConversationTable, RunTable, MessageTable)
|
||||||
.outerjoin(DialogRecord, ConversationRecord.id == DialogRecord.conversation_id) # type: ignore
|
.outerjoin(RunTable, RunTable.conversation_id == ConversationTable.id) # type: ignore
|
||||||
|
.outerjoin(MessageTable, MessageTable.run_id == RunTable.id) # type: ignore
|
||||||
.where(
|
.where(
|
||||||
ConversationRecord.user_id == user_id,
|
ConversationTable.user_id == user_id,
|
||||||
ConversationRecord.is_deleted == False,
|
ConversationTable.is_deleted == False,
|
||||||
|
)
|
||||||
|
.order_by(
|
||||||
|
ConversationTable.id, TaskTable.id, RunTable.id, MessageTable.id
|
||||||
)
|
)
|
||||||
.order_by(ConversationRecord.id, DialogRecord.id)
|
|
||||||
)
|
)
|
||||||
for conversation_record, dialog_record in result.all():
|
for (
|
||||||
record = records.setdefault(
|
conversation_result,
|
||||||
conversation_record.id,
|
run_result,
|
||||||
|
message_result,
|
||||||
|
) in result.all():
|
||||||
|
conversation_record = records.setdefault(
|
||||||
|
conversation_result.id,
|
||||||
Conversation(
|
Conversation(
|
||||||
id=conversation_record.id,
|
id=conversation_result.id,
|
||||||
description=conversation_record.description,
|
description=conversation_result.description,
|
||||||
created_at=format_at(conversation_record.created_at),
|
created_at=conversation_result.created_at,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if not dialog_record:
|
if not run_result:
|
||||||
continue
|
continue
|
||||||
record.dialogs.update(
|
run_record = conversation_record.runs.setdefault(
|
||||||
{
|
run_result.id,
|
||||||
dialog_record.id: Dialog(
|
Run(
|
||||||
id=dialog_record.id,
|
id=run_result.id,
|
||||||
user_prompt=dialog_record.user_prompt,
|
status=run_result.status,
|
||||||
thoughts=dialog_record.thoughts,
|
usage=usage_to_object(run_result.usage),
|
||||||
result_output=dialog_record.result_output,
|
usage_limits=usage_limits_to_object(run_result.usage_limits),
|
||||||
usage=dialog_record.usage,
|
),
|
||||||
)
|
)
|
||||||
}
|
if not message_result:
|
||||||
|
continue
|
||||||
|
run_record.messages.setdefault(
|
||||||
|
message_result.id,
|
||||||
|
Message(
|
||||||
|
id=message_result.id,
|
||||||
|
type=message_result.type,
|
||||||
|
title=message_result.title,
|
||||||
|
content=message_result.content,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return records
|
return records
|
||||||
|
|
||||||
async def create_conversations_record(
|
async def create_conversation_record(self, user_id: str) -> dict[str, Conversation]:
|
||||||
self, user_id: str, description: str = "新会话"
|
|
||||||
) -> dict[str, Conversation]:
|
|
||||||
"""
|
"""
|
||||||
创建会话记录
|
创建会话记录
|
||||||
:param user_id: 用户唯一标识
|
:param user_id: 用户唯一标识
|
||||||
:param description: 会话描述,默认为"新会话"
|
:return: 会话实例
|
||||||
:return: 键为会话唯一标识,值为会话实例的会话字典
|
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
record = ConversationRecord(user_id=user_id, description=description)
|
record = ConversationTable(user_id=user_id)
|
||||||
session.add(record)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(record)
|
await session.refresh(record)
|
||||||
|
|
@ -262,18 +306,18 @@ class DatabaseState(rx.State):
|
||||||
record.id: Conversation(
|
record.id: Conversation(
|
||||||
id=record.id,
|
id=record.id,
|
||||||
description=record.description,
|
description=record.description,
|
||||||
created_at=format_at(record.created_at),
|
created_at=record.created_at,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def delete_conversations_record(self, conversation_id: str) -> None:
|
async def delete_conversation_record(self, conversation_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
删除会话记录(逻辑删除)
|
删除会话记录(逻辑删除)
|
||||||
:param conversation_id: 指定会话唯一标识
|
:param conversation_id: 指定会话唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
record = await session.get(ConversationRecord, conversation_id)
|
record = await session.get(ConversationTable, conversation_id)
|
||||||
if not record:
|
if not record:
|
||||||
return
|
return
|
||||||
record.is_deleted = True
|
record.is_deleted = True
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"""
|
"""
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, TypeAdapter
|
from pydantic import BaseModel, Field, TypeAdapter
|
||||||
from pydantic_ai import RunUsage
|
from pydantic_ai import RunUsage
|
||||||
|
|
@ -11,44 +12,136 @@ from pydantic_ai._uuid import uuid7
|
||||||
from pydantic_ai.usage import UsageLimits
|
from pydantic_ai.usage import UsageLimits
|
||||||
|
|
||||||
|
|
||||||
class Thought(BaseModel):
|
class MessageType(StrEnum):
|
||||||
"""
|
"""
|
||||||
思考类
|
消息类型枚举
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: str = Field(..., description="思考类型")
|
USER_PROMPT = "user_prompt"
|
||||||
content: str = Field(..., description="思考内容")
|
THINKING = "thinking"
|
||||||
|
AGENT_RUN_RESULT_OUTPUT = "agent_run_result_output"
|
||||||
|
|
||||||
|
|
||||||
# 思考领域模型类型适配器
|
class Message(BaseModel):
|
||||||
ThoughtsAdapter = TypeAdapter(dict[int, Thought])
|
|
||||||
|
|
||||||
|
|
||||||
def thoughts_to_dict(thoughts: dict[int, Thought]) -> dict:
|
|
||||||
"""
|
"""
|
||||||
Thought 转为字典
|
消息类
|
||||||
"""
|
|
||||||
return ThoughtsAdapter.dump_python(thoughts)
|
|
||||||
|
|
||||||
|
|
||||||
class Dialog(BaseModel):
|
|
||||||
"""
|
|
||||||
对话类
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识")
|
id: str = Field(default_factory=lambda: str(uuid7()), description="消息唯一标识")
|
||||||
user_prompt: str = Field(default="", description="用户提示词")
|
type: MessageType = Field(..., description="消息类型")
|
||||||
thoughts: dict[int, Thought] = Field(default_factory=dict, description="思考列表")
|
title: str = Field(default="", description="消息标题")
|
||||||
result_output: str = Field(default="", description="结果输出")
|
content: str = Field(default="", description="消息内容")
|
||||||
usage: dict[str, Any] = Field(default_factory=dict, description="对话使用量")
|
is_component_expanded: bool = Field(
|
||||||
|
default=True, description="展开组件,True 表示展开,False 表示折叠"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RunStatus(StrEnum):
|
||||||
|
"""
|
||||||
|
运行状态枚举
|
||||||
|
"""
|
||||||
|
|
||||||
|
RUNNING = "running"
|
||||||
|
FINISHED = "finished"
|
||||||
|
|
||||||
|
|
||||||
|
class Run(BaseModel):
|
||||||
|
"""
|
||||||
|
运行类
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str = Field(default_factory=lambda: str(uuid7()), description="运行唯一标识")
|
||||||
|
messages: dict[str, Message] = Field(default_factory=dict, description="消息字典")
|
||||||
|
status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态")
|
||||||
|
usage: RunUsage = Field(default=RunUsage(), description="运行使用量")
|
||||||
|
usage_limits: UsageLimits | None = Field(default=None, description="运行使用量限制")
|
||||||
is_thinking: bool = Field(
|
is_thinking: bool = Field(
|
||||||
default=False, description="正在思考,True 表示正在思考,False 表示未正在思考"
|
default=False, description="正在思考,True 表示正在思考,False 表示未正在思考"
|
||||||
)
|
)
|
||||||
is_expanded: bool = Field(
|
|
||||||
default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠"
|
|
||||||
|
class TaskType(StrEnum):
|
||||||
|
"""
|
||||||
|
任务类型枚举
|
||||||
|
"""
|
||||||
|
|
||||||
|
CHAT = "聊天"
|
||||||
|
BOOK_FLIGHT = "预定航班"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(StrEnum):
|
||||||
|
"""
|
||||||
|
任务状态枚举
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
|
||||||
|
class Deps(BaseModel):
|
||||||
|
"""
|
||||||
|
依赖项类
|
||||||
|
"""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Task(BaseModel):
|
||||||
|
"""
|
||||||
|
任务类
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: TaskType = Field(default=TaskType.CHAT, description="任务类型")
|
||||||
|
status: TaskStatus = Field(default=TaskStatus.NONE, description="任务状态")
|
||||||
|
deps: Deps | None = Field(default=None, description="任务依赖项")
|
||||||
|
usage: RunUsage = Field(default=RunUsage(), description="任务使用量")
|
||||||
|
usage_limits: UsageLimits | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="任务使用量限制",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Conversation(BaseModel):
|
||||||
|
"""
|
||||||
|
会话类
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str = Field(default_factory=lambda: str(uuid7()), description="会话唯一标识")
|
||||||
|
runs: dict[str, Run] = Field(default_factory=dict, description="运行字典")
|
||||||
|
description: str = Field(default="新会话", description="会话描述")
|
||||||
|
created_at: datetime = Field(..., description="会话创建日期时间")
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationHistoryItem(BaseModel):
|
||||||
|
"""
|
||||||
|
会话历史项类
|
||||||
|
"""
|
||||||
|
|
||||||
|
description: str = Field(..., description="会话描述")
|
||||||
|
created_at: str = Field(..., description="会话创建日期时间")
|
||||||
|
|
||||||
|
|
||||||
|
# Dpes 适配器
|
||||||
|
DepsAdapter = TypeAdapter(Deps)
|
||||||
|
|
||||||
|
|
||||||
|
def deps_to_object(deps: dict[str, Any]) -> Deps | None:
|
||||||
|
"""
|
||||||
|
Deps 转为对象
|
||||||
|
"""
|
||||||
|
if not deps:
|
||||||
|
return None
|
||||||
|
return DepsAdapter.validate_python(deps)
|
||||||
|
|
||||||
|
|
||||||
|
def deps_to_dict(deps: Deps | None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Deps 转为字典
|
||||||
|
"""
|
||||||
|
if not deps:
|
||||||
|
return {}
|
||||||
|
return DepsAdapter.dump_python(deps)
|
||||||
|
|
||||||
|
|
||||||
# Usage 适配器
|
# Usage 适配器
|
||||||
UsageAdapter = TypeAdapter(RunUsage)
|
UsageAdapter = TypeAdapter(RunUsage)
|
||||||
|
|
||||||
|
|
@ -57,9 +150,9 @@ def usage_to_object(usage: dict[str, Any]) -> RunUsage:
|
||||||
"""
|
"""
|
||||||
Usage 转为对象
|
Usage 转为对象
|
||||||
"""
|
"""
|
||||||
if isinstance(usage, dict) and usage:
|
if not usage:
|
||||||
return UsageAdapter.validate_python(usage)
|
return RunUsage()
|
||||||
return RunUsage()
|
return UsageAdapter.validate_python(usage)
|
||||||
|
|
||||||
|
|
||||||
def usage_to_dict(usage: RunUsage) -> dict[str, Any]:
|
def usage_to_dict(usage: RunUsage) -> dict[str, Any]:
|
||||||
|
|
@ -69,72 +162,23 @@ def usage_to_dict(usage: RunUsage) -> dict[str, Any]:
|
||||||
return UsageAdapter.dump_python(usage)
|
return UsageAdapter.dump_python(usage)
|
||||||
|
|
||||||
|
|
||||||
# UsageLimits 适配器
|
# UsageLimits 适配器
|
||||||
UsageLimitsAdapter = TypeAdapter(UsageLimits)
|
UsageLimitsAdapter = TypeAdapter(UsageLimits)
|
||||||
|
|
||||||
|
|
||||||
def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits:
|
def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits | None:
|
||||||
"""
|
"""
|
||||||
UsageLimits 转为对象
|
UsageLimits 转为对象
|
||||||
"""
|
"""
|
||||||
if isinstance(usage_limits, dict) and usage_limits:
|
if not usage_limits:
|
||||||
return UsageLimitsAdapter.validate_python(usage_limits)
|
return None
|
||||||
return UsageLimits(request_limit=5)
|
return UsageLimitsAdapter.validate_python(usage_limits)
|
||||||
|
|
||||||
|
|
||||||
def usage_limits_to_dict(usage_limits: UsageLimits) -> dict[str, Any]:
|
def usage_limits_to_dict(usage_limits: UsageLimits | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
UsageLimits 转为字典
|
UsageLimits 转为字典
|
||||||
"""
|
"""
|
||||||
|
if not usage_limits:
|
||||||
|
return {}
|
||||||
return UsageLimitsAdapter.dump_python(usage_limits)
|
return UsageLimitsAdapter.dump_python(usage_limits)
|
||||||
|
|
||||||
|
|
||||||
class TaskType(StrEnum):
|
|
||||||
"""
|
|
||||||
任务类型枚举
|
|
||||||
"""
|
|
||||||
|
|
||||||
BOOK_FLIGHT = "book_flight"
|
|
||||||
|
|
||||||
|
|
||||||
class Task(BaseModel):
|
|
||||||
"""
|
|
||||||
任务类
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str = Field(default_factory=lambda: str(uuid7()), description="任务唯一标识")
|
|
||||||
type: TaskType = Field(..., description="任务类型")
|
|
||||||
node: str = Field(default="", description="任务节点")
|
|
||||||
deps: Any = Field(default=None, description="任务依赖项")
|
|
||||||
usage: dict[str, Any] = Field(default_factory=dict, description="任务使用量")
|
|
||||||
usage_limits: dict[str, Any] = Field(
|
|
||||||
default_factory=dict, description="任务使用量限制"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Conversation(BaseModel):
|
|
||||||
"""
|
|
||||||
会话类
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str = Field(default_factory=lambda: str(uuid7()), description="会话唯一标识")
|
|
||||||
description: str = Field(default="新会话", description="会话描述")
|
|
||||||
is_running: bool = Field(
|
|
||||||
default=False,
|
|
||||||
description="会话正在运行,True 表示正在运行,False 表示未正在运行",
|
|
||||||
)
|
|
||||||
dialogs: dict[str, Dialog] = Field(default_factory=dict, description="对话字典")
|
|
||||||
task: Task | None = Field(default=None, description="会话任务")
|
|
||||||
created_at: str = Field(
|
|
||||||
...,
|
|
||||||
description="创建时间",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TaskNodeResultEvent(BaseModel):
|
|
||||||
"""
|
|
||||||
任务节点结果事件类
|
|
||||||
"""
|
|
||||||
|
|
||||||
task: Task | None = Field(..., description="任务实例")
|
|
||||||
content: str = Field(default="", description="任务结果内容")
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ async def run_stream_events(
|
||||||
user_prompt=user_prompt,
|
user_prompt=user_prompt,
|
||||||
message_history=message_history,
|
message_history=message_history,
|
||||||
):
|
):
|
||||||
|
print(event)
|
||||||
yield event
|
yield event
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ flight_details_extraction_agent = Agent(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@flight_search_agent.tool
|
@flight_search_agent.tool(name="提取所有航班详情")
|
||||||
async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]:
|
async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]:
|
||||||
"""
|
"""
|
||||||
工具:提取所有航班详情
|
工具:提取所有航班详情
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue