441 lines
16 KiB
Python
441 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
会话状态
|
||
"""
|
||
from typing import AsyncGenerator
|
||
from datetime import datetime
|
||
|
||
from pydantic_ai.messages import (
|
||
FunctionToolCallEvent,
|
||
FunctionToolResultEvent,
|
||
LoadCapabilityCallPart,
|
||
PartDeltaEvent,
|
||
PartEndEvent,
|
||
PartStartEvent,
|
||
TextPart,
|
||
TextPartDelta,
|
||
ThinkingPart,
|
||
ThinkingPartDelta,
|
||
ToolCallPart,
|
||
ToolSearchCallPart,
|
||
)
|
||
from pydantic_ai.run import AgentRunResultEvent
|
||
import reflex as rx
|
||
|
||
from application.states.database import DatabaseState
|
||
from application.states.models import (
|
||
Conversation,
|
||
ConversationHistoryItem,
|
||
usage_to_dict,
|
||
)
|
||
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):
|
||
"""
|
||
会话状态
|
||
"""
|
||
|
||
# 当前用户唯一标识
|
||
user_id: str = ""
|
||
# 当前用户的会话字典(私有变量)
|
||
# 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用
|
||
_conversations: dict[str, Conversation] = {} # 键为会话唯一标识,值为会话实例
|
||
# 激活会话唯一标识
|
||
actived_conversation_id: str = ""
|
||
|
||
# 显示会话历史,True 表示显示,False 表示隐藏
|
||
is_conversation_history_shown: bool = False
|
||
# 会话历史项显示悬停气泡,True 表示显示,False 表示隐藏
|
||
is_conversation_history_item_popover_shown: bool = False
|
||
|
||
# 用户提示词
|
||
user_prompt: str = ""
|
||
|
||
# 当前数据库状态(私有变量)
|
||
_db_state: DatabaseState | None = None
|
||
|
||
async def get_db_state(self) -> DatabaseState:
|
||
"""
|
||
获取当前数据库状态
|
||
:return: 当前数据库状态
|
||
"""
|
||
# 若数据库状态为 None 则获取数据库状态,否则直接返回当前数据库状态
|
||
if not self._db_state:
|
||
self._db_state = await self.get_state(DatabaseState)
|
||
return self._db_state
|
||
|
||
async def resume_conversation_state(self, user_id: str) -> None:
|
||
"""
|
||
恢复当前用户的会话状态
|
||
:param user_id: 用户唯一标识
|
||
:return: None
|
||
"""
|
||
self.user_id = user_id.strip()
|
||
if not self.user_id:
|
||
return
|
||
|
||
# 获取当前数据库状态
|
||
db_state = await self.get_db_state()
|
||
# 获取当前用户的会话字典
|
||
self._conversations = await db_state.get_conversations(self.user_id)
|
||
# 若当前用户的会话字典为空则先创建会话记录再添加会话实例
|
||
if not self._conversations:
|
||
self._conversations.update(
|
||
await db_state.create_conversation_record(self.user_id)
|
||
)
|
||
# 将最后一个会话的唯一标识作为激活会话唯一标识
|
||
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||
|
||
@rx.event
|
||
def toggle_conversation_history_shown(self) -> None:
|
||
"""
|
||
显示 / 隐藏会话历史
|
||
:return: None
|
||
"""
|
||
self.is_conversation_history_shown = not self.is_conversation_history_shown
|
||
|
||
@rx.var
|
||
def conversation_history_items(self) -> list[ConversationHistoryItem]:
|
||
"""
|
||
获取会话历史项列表(按照会话创建日期时间降序排序)
|
||
: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
|
||
"""
|
||
self.is_conversation_history_item_popover_shown = is_shown
|
||
|
||
@rx.event
|
||
async def delete_conversation(self, conversation_id: str) -> None:
|
||
"""
|
||
删除会话
|
||
:param conversation_id: 需删除的会话唯一标识
|
||
:return: None
|
||
"""
|
||
# 获取当前数据库状态
|
||
db_state = await self.get_db_state()
|
||
await db_state.delete_conversation_record(conversation_id)
|
||
del self._conversations[conversation_id]
|
||
|
||
# 删除后,若当前用户的会话字典为空则创建会话
|
||
if not self._conversations:
|
||
self._conversations.update(
|
||
await db_state.create_conversation_record(self.user_id)
|
||
)
|
||
|
||
# 删除后,若激活会话唯一标识不存在则将最后一个会话的唯一标识作为激活会话唯一标识
|
||
if self.actived_conversation_id not in self._conversations:
|
||
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||
|
||
@rx.event
|
||
def set_actived_conversation(self, conversation_id: str) -> None:
|
||
"""
|
||
设置激活会话
|
||
:param conversation_id: 会话唯一标识
|
||
:return: None
|
||
"""
|
||
self.actived_conversation_id = conversation_id
|
||
|
||
@rx.event
|
||
async def create_conversation(self) -> None:
|
||
"""
|
||
创建会话
|
||
:return: None
|
||
"""
|
||
# 获取当前数据库状态
|
||
db_state = await self.get_db_state()
|
||
# 创建会话记录再添加会话实例
|
||
self._conversations.update(
|
||
await db_state.create_conversation_record(self.user_id)
|
||
)
|
||
# 将最后一个会话的唯一标识作为激活会话唯一标识
|
||
self.actived_conversation_id = next(reversed(self._conversations.keys()))
|
||
|
||
@rx.event
|
||
def set_user_prompt(self, user_prompt: str) -> None:
|
||
"""
|
||
设置用户提示词
|
||
:param user_prompt: 用户提示词
|
||
:return: None
|
||
"""
|
||
self.user_prompt = user_prompt.strip()
|
||
|
||
@rx.var
|
||
def is_user_prompt_send_button_disabled(self) -> bool:
|
||
"""
|
||
用户提示词发送按钮不可点击
|
||
:return: bool
|
||
"""
|
||
# 当前会话
|
||
conversation = self._conversations.get(self.actived_conversation_id)
|
||
if not conversation:
|
||
return True
|
||
return conversation.is_running or not self.user_prompt
|
||
|
||
@rx.var
|
||
def running_status(self) -> bool:
|
||
"""
|
||
获取当前会话的运行状态
|
||
:return: 当前会话的运行状态(True 表示正在运行,False 表示运行完成)
|
||
"""
|
||
# 当前会话
|
||
conversation = self.conversations.get(self.conversation_id)
|
||
if not conversation:
|
||
return False
|
||
return conversation.is_running
|
||
|
||
@rx.var
|
||
def dialogs(self) -> dict[str, Dialog]:
|
||
"""
|
||
当前会话的对话字典
|
||
:return: 当前会话的对话字典
|
||
"""
|
||
# 当前会话
|
||
conversation = self.conversations.get(self.conversation_id)
|
||
if not conversation:
|
||
return {}
|
||
return conversation.dialogs
|
||
|
||
@rx.event
|
||
async def handle_user_prompt(self) -> AsyncGenerator[None]:
|
||
"""
|
||
处理用户提示词
|
||
:return: AsyncGenerator[None]
|
||
"""
|
||
if not self.user_prompt:
|
||
return
|
||
|
||
# 当前会话
|
||
conversation = self.conversations[self.conversation_id]
|
||
# 将正在运行设置为是
|
||
conversation.is_running = True
|
||
# 当前对话
|
||
dialog = Dialog(user_prompt=self.user_prompt)
|
||
# 清空用户提示词
|
||
self.user_prompt = ""
|
||
# 添加对话实例
|
||
conversation.dialogs.update({dialog.id: dialog})
|
||
# 强制更新会话并推送前端
|
||
self.conversations[self.conversation_id] = conversation
|
||
yield
|
||
|
||
# 获取数据库状态
|
||
db_state = await self.get_db_state()
|
||
# 获取消息历史列表
|
||
message_history = await db_state.get_message_history(
|
||
conversation_id=self.conversation_id
|
||
)
|
||
|
||
# 初始化工具调用唯一标识和片段索引映射字典
|
||
tool_call_ids: dict[str, int] = {}
|
||
# 获取运行流式输出事件
|
||
async for event in run_stream_events(
|
||
task=conversation.task,
|
||
user_prompt=dialog.user_prompt,
|
||
message_history=message_history,
|
||
):
|
||
match event:
|
||
# ========== 开始事件 ==========
|
||
case PartStartEvent(
|
||
index=index,
|
||
part=part,
|
||
previous_part_kind=previous_part_kind,
|
||
):
|
||
match part:
|
||
# 思考分片开始事件
|
||
case ThinkingPart(content=content):
|
||
# 若上一分片种类为空则将正在思考设置为正在思考
|
||
if not previous_part_kind:
|
||
dialog.is_thinking = True
|
||
dialog.thoughts[index] = Thought(
|
||
type="thinking", content=content
|
||
)
|
||
|
||
# 工具检索分片开始事件
|
||
case ToolSearchCallPart(tool_call_id=tool_call_id):
|
||
# 创建工具调用唯一标识与片段索引映射
|
||
tool_call_ids[tool_call_id] = index
|
||
|
||
dialog.thoughts[index] = Thought(
|
||
type="tool_search",
|
||
content="正在生成检索关键词",
|
||
)
|
||
|
||
# 能力加载分片开始事件
|
||
case LoadCapabilityCallPart(tool_call_id=tool_call_id):
|
||
tool_call_ids[tool_call_id] = index
|
||
|
||
dialog.thoughts[index] = Thought(
|
||
type="capability_load",
|
||
content="正在生成加载参数",
|
||
)
|
||
|
||
# 工具调用分片开始事件
|
||
case ToolCallPart(tool_call_id=tool_call_id):
|
||
tool_call_ids[tool_call_id] = index
|
||
|
||
dialog.thoughts[index] = Thought(
|
||
type="tool_call",
|
||
content="正在生成调用参数",
|
||
)
|
||
|
||
# 文本分片开始事件
|
||
case TextPart(content=content):
|
||
dialog.result_output = content
|
||
|
||
# ========== 增量事件 ==========
|
||
case PartDeltaEvent(index=index, delta=delta):
|
||
match delta:
|
||
# 思考分片增量事件
|
||
case ThinkingPartDelta(
|
||
content_delta=content_delta,
|
||
):
|
||
dialog.thoughts[index].content += content_delta or ""
|
||
|
||
# 文本分片增量事件
|
||
case TextPartDelta(
|
||
content_delta=content_delta,
|
||
):
|
||
dialog.result_output += content_delta
|
||
|
||
# ========== 结束事件 ==========
|
||
case PartEndEvent(
|
||
index=index,
|
||
part=part,
|
||
next_part_kind=next_part_kind,
|
||
):
|
||
match part:
|
||
# 思考分片结束事件
|
||
case ThinkingPart(content=content):
|
||
# 若下一分片种类为文本则将思考状态设置为思考完成
|
||
if next_part_kind == "text":
|
||
dialog.is_thinking = False
|
||
|
||
# ========== 函数工具调用事件 ==========
|
||
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
|
||
# 获取分片索引
|
||
index = tool_call_ids[tool_call_id]
|
||
match dialog.thoughts[index].type:
|
||
# 工具检索
|
||
case "tool_search":
|
||
dialog.thoughts[index].content = (
|
||
f"正在检索 {part.args_as_json_str()}"
|
||
)
|
||
|
||
# 能力加载
|
||
case "capability_load":
|
||
dialog.thoughts[index].content = (
|
||
f"正在加载能力 {part.tool_name}"
|
||
)
|
||
|
||
# 工具调用
|
||
case "tool_call":
|
||
dialog.thoughts[index].content = (
|
||
f"正在调用工具 {part.tool_name}"
|
||
)
|
||
|
||
# ========== 函数工具结果事件 ==========
|
||
case FunctionToolResultEvent(
|
||
tool_call_id=tool_call_id,
|
||
content=content,
|
||
):
|
||
index = tool_call_ids[tool_call_id]
|
||
match dialog.thoughts[index].type:
|
||
# 工具检索
|
||
case "tool_search":
|
||
dialog.thoughts[index].content = (
|
||
content if isinstance(content, str) else ""
|
||
) # 暂仅考虑文本内容
|
||
|
||
# 能力加载
|
||
case "capability_load":
|
||
dialog.thoughts[index].content = f"已加载 {content}"
|
||
|
||
# 工具调用
|
||
case "tool_call":
|
||
dialog.thoughts[index].content = f"已调用 {content}"
|
||
|
||
case TaskNodeResultEvent(task=task, content=content):
|
||
# 更新任务
|
||
conversation.task = task
|
||
dialog.result_output += content
|
||
|
||
# ========== 智能体运行结果事件 ==========
|
||
case AgentRunResultEvent(result=result):
|
||
# 创建对话记录
|
||
await db_state.create_dialog_record(
|
||
conversation_id=self.conversation_id,
|
||
id=dialog.id,
|
||
user_prompt=dialog.user_prompt,
|
||
thoughts=thoughts_to_dict(dialog.thoughts),
|
||
result_output=dialog.result_output,
|
||
usage=usage_to_dict(result.usage),
|
||
)
|
||
# 创建结果记录
|
||
await db_state.create_result_record(
|
||
conversation_id=self.conversation_id,
|
||
dialog_id=dialog.id,
|
||
new_messages=result.new_messages(),
|
||
)
|
||
|
||
# 强制更新会话并推送前端
|
||
self.conversations[self.conversation_id] = conversation
|
||
yield
|
||
|
||
# 将正在运行设置为否
|
||
conversation.is_running = False
|
||
self.conversations[self.conversation_id] = conversation
|
||
yield
|
||
|
||
@rx.event
|
||
async def generate_prd(self) -> None:
|
||
"""
|
||
预订航班
|
||
"""
|
||
from application.tasks.book_flight import init_task
|
||
|
||
# 当前会话
|
||
conversation = self.conversations[self.conversation_id]
|
||
# 初始化预定航班任务
|
||
conversation.task = init_task()
|
||
self.user_prompt = f"帮我找一班从 {conversation.task.deps.origin} 到 {conversation.task.deps.destination} 在 {conversation.task.deps.date} 的航班"
|
||
|
||
@rx.event
|
||
def toggle_collapse(self, dialog_id: str) -> None:
|
||
"""
|
||
展开/折叠思考折叠面板
|
||
"""
|
||
# 指定运行
|
||
dialog = self.conversations[self.conversation_id].dialogs[dialog_id]
|
||
dialog.is_expanded = not dialog.is_expanded
|