This commit is contained in:
liubiren 2026-08-11 14:24:31 +08:00
parent 942c1bea71
commit a7fead671a
6 changed files with 475 additions and 470 deletions

View File

@ -6,9 +6,8 @@ import reflex as rx
from typing import Tuple from typing import Tuple
from application.states.models import ( from application.states.models import (
Conversation, MessageHistoryItem,
Dialog, MessageType,
Thought,
ConversationHistoryItem, ConversationHistoryItem,
) )
from application.states import ConversationState, AuthState from application.states import ConversationState, AuthState
@ -32,7 +31,7 @@ def conversation_history_item(
return rx.list.item( return rx.list.item(
rx.vstack( rx.vstack(
rx.hstack( rx.hstack(
# 话描述 # 会话历史项的会话描述
rx.text( rx.text(
item.description, item.description,
flex=1, flex=1,
@ -51,7 +50,7 @@ def conversation_history_item(
), ),
rx.hstack( rx.hstack(
rx.spacer(), rx.spacer(),
# 创建时间 # 会话历史项的会话创建日期时间
rx.text( rx.text(
item.created_at, item.created_at,
line_height="20px", line_height="20px",
@ -68,7 +67,7 @@ def conversation_history_item(
on_click=lambda: ConversationState.set_actived_conversation(item.id), on_click=lambda: ConversationState.set_actived_conversation(item.id),
width="100%", width="100%",
), ),
# 更多按钮:点击更多按钮显示气泡卡片,可删除会话 # 会话历史项的更多按钮
rx.box( rx.box(
rx.popover.root( rx.popover.root(
rx.popover.trigger( rx.popover.trigger(
@ -143,9 +142,9 @@ def conversation_history_item(
z_index="99", z_index="99",
min_width="14px", min_width="14px",
cursor="pointer", cursor="pointer",
opacity=rx.cond(highlight, "1", "0"), opacity=rx.cond(is_highlight, "1", "0"),
transition="opacity 0.18s ease-in-out", transition="opacity 0.18s ease-in-out",
pointer_events=rx.cond(highlight, "auto", "none"), pointer_events=rx.cond(is_highlight, "auto", "none"),
), ),
display="flex", display="flex",
position="relative", position="relative",
@ -154,12 +153,12 @@ def conversation_history_item(
padding="16px", padding="16px",
margin_bottom="8px", margin_bottom="8px",
background=rx.cond( background=rx.cond(
highlight, is_highlight,
"var(--prismui-background-3)", "var(--prismui-background-3)",
"var(--prismui-background-color-1)", "var(--prismui-background-color-1)",
), ),
border_radius="var(--prismui-border-radius-3)", border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond(highlight, "var(--prismui-box-shadow-7)", "none"), box_shadow=rx.cond(is_highlight, "var(--prismui-box-shadow-7)", "none"),
cursor="pointer", cursor="pointer",
style={ style={
"&:hover": { "&:hover": {
@ -175,16 +174,16 @@ def conversation_history_item(
def conversation_history( def conversation_history(
is_shown: bool, is_conversation_history_shown: bool,
) -> rx.Component: ) -> rx.Component:
""" """
会话历史 会话历史
:param is_shown: 展示 / 隐藏 :param is_conversation_history_shown: 展示 / 隐藏
:return: Component :return: Component
""" """
return rx.box( return rx.box(
rx.vstack( rx.vstack(
# 标题 # 会话历史的标题
rx.hstack( rx.hstack(
rx.text( rx.text(
"会话历史", "会话历史",
@ -219,9 +218,9 @@ def conversation_history(
backdrop_filter="blur(50px)", backdrop_filter="blur(50px)",
gap="12px", gap="12px",
), ),
width=rx.cond(is_shown, "25%", "0px"), width=rx.cond(is_conversation_history_shown, "25%", "0px"),
min_width=rx.cond(is_shown, "240px", "0px"), min_width=rx.cond(is_conversation_history_shown, "240px", "0px"),
max_width=rx.cond(is_shown, "380px", "0px"), max_width=rx.cond(is_conversation_history_shown, "380px", "0px"),
height="100%", height="100%",
transition="all 0.18s ease-in-out", transition="all 0.18s ease-in-out",
overflow="hidden", overflow="hidden",
@ -334,55 +333,16 @@ def guidance() -> rx.Component:
) )
def thought_showing(item: Tuple[int, Thought]): def user_prompt(item: MessageHistoryItem) -> rx.Component:
""" """
思考展示 用户提示词
:param item: 思考实例
:return: Component :return: Component
""" """
# 思考实例 return rx.hstack(
thought = item[1]
return rx.vstack(
rx.match(
thought.type,
(
"thinking",
rx.text(
thought.content,
line_height="1.6",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
),
),
rx.fragment(),
),
align_items="flex-start",
width="100%",
)
def message_history_item(item: Tuple[str, Dialog]) -> rx.Component:
"""
消息历史项
:param item: 对话实例
:return: Component
"""
# 对话实例
dialog = item[1]
# 推理状态
is_thinking = dialog.is_thinking
# 思考折叠面板展开状态
is_expanded = dialog.is_expanded
return rx.vstack(
# 用户提示词
rx.cond(
dialog.user_prompt,
rx.hstack(
rx.spacer(), rx.spacer(),
rx.vstack( rx.vstack(
rx.text( rx.text(
dialog.user_prompt, item.content,
max_width="600px", max_width="600px",
padding="12px 16px", padding="12px 16px",
background_color="var(--prismui-background-color-3)", background_color="var(--prismui-background-color-3)",
@ -418,18 +378,20 @@ def message_history_item(item: Tuple[str, Dialog]) -> rx.Component:
gap="4px", gap="4px",
width="100%", width="100%",
margin_top="8px", margin_top="8px",
), )
rx.fragment(),
),
# 思考折叠面板 def thinking(item: MessageHistoryItem) -> rx.Component:
rx.cond( """
dialog.thoughts, 思考
rx.vstack( :return: Component
# 标题 """
return rx.vstack(
# 思考的标题栏
rx.box( rx.box(
rx.hstack( rx.hstack(
rx.text( rx.text(
rx.cond(is_thinking, "思考中", "思考完成"), item.title,
font_size="var(--prismui-font-size-1)", font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)", color="var(--prismui-color-3)",
), ),
@ -438,9 +400,7 @@ def message_history_item(item: Tuple[str, Dialog]) -> rx.Component:
width="14px", width="14px",
height="14px", height="14px",
color="var(--prismui-color-3)", color="var(--prismui-color-3)",
transform=rx.cond( transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"),
is_expanded, "rotate(90deg)", "rotate(0deg)"
),
transition="transform 0.18s ease-in-out", transition="transform 0.18s ease-in-out",
), ),
align_items="center", align_items="center",
@ -448,54 +408,30 @@ def message_history_item(item: Tuple[str, Dialog]) -> rx.Component:
margin_bottom="8px", margin_bottom="8px",
line_height="22px", line_height="22px",
), ),
# 点击事件,展开/折叠思考折叠面板 # 点击事件,展示 / 隐藏消息历史项
on_click=lambda: ConversationState.toggle_collapse(dialog.id), on_click=lambda: ConversationState.toggle_message_history_item_shown(
item.id
),
cursor="pointer", cursor="pointer",
), ),
# 思考 # 思考的内容
rx.box( rx.text(
rx.box( item.content,
rx.auto_scroll( line_height="1.6",
rx.foreach( font_size="var(--prismui-font-size-1)",
dialog.thoughts, color="var(--prismui-color-3)",
thought_showing,
),
width="100%",
align_self="start",
),
style={"overflow": "hidden", "min-height": "0"},
),
display="grid",
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
width="100%",
opacity=rx.cond(is_expanded, "1", "0"),
overflow="hidden",
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
), ),
margin_bottom="8px", margin_bottom="8px",
), )
# 若思考列表为空且正在运行则显示加载动效
rx.cond(
ConversationState.running_status, def result_output(item: MessageHistoryItem) -> rx.Component:
rx.box( """
rx.box( 结果输出
class_name="loadin", :return: Component
flex_shrink="0", """
), return rx.markdown(
display="flex", item.content,
justify_content="center",
align_items="center",
width="32px",
height="18px",
margin_bottom="8px",
overflow="hidden",
),
rx.fragment(),
),
),
# 结果输出
rx.markdown(
dialog.result_output,
component_map={ component_map={
"p": lambda text: rx.text( "p": lambda text: rx.text(
text, text,
@ -606,10 +542,42 @@ def message_history_item(item: Tuple[str, Dialog]) -> rx.Component:
), ),
}, },
width="100%", width="100%",
)
def message_history_item(item: MessageHistoryItem) -> rx.Component:
"""
消息历史项
:param item: 对话实例
:return: Component
"""
return rx.vstack(
# 若等待流式输出则显示加载动效,否则根据消息类型渲染组件
rx.cond(
ConversationState.awaiting_stream,
rx.box(
rx.box(
class_name="loadin",
flex_shrink="0",
),
display="flex",
justify_content="center",
align_items="center",
width="32px",
height="18px",
margin_bottom="8px",
overflow="hidden",
),
rx.match(
item.type,
(MessageType.USER_PROMPT, user_prompt(item)),
(MessageType.THINKING, thinking(item)),
(MessageType.RESULT_OUTPUT, result_output(item)),
),
), ),
padding="0 16px", padding="0 16px",
width="100%", width="100%",
key=dialog.id,
) )
@ -618,17 +586,15 @@ def message_history() -> rx.Component:
消息历史 消息历史
:return: Component :return: Component
""" """
# 消息历史
message_history = ConversationState.message_history
return rx.box( return rx.box(
# 若消息历史为空则显示引导,否则渲染消息历史项 # 若消息历史为空则显示引导,否则渲染消息历史项
rx.cond( rx.cond(
message_history.length() == 0, ConversationState.message_history.length() == 0,
guidance(), guidance(),
rx.vstack( rx.vstack(
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
message_history, ConversationState.message_history,
message_history_item, message_history_item,
), ),
width="100%", width="100%",
@ -765,18 +731,19 @@ def user_prompt_sending() -> rx.Component:
line_height="21px", line_height="21px",
color="var(--prismui-color-9)", color="var(--prismui-color-9)",
), ),
loading=ConversationState.running_status, # 加载状态:绑定当前会话的运行状态
# 是否禁用绑定用户提示词发送禁用状态 loading=ConversationState.is_running,
disabled=ConversationState.is_user_prompt_sending_disabled, # 不可点击状态:绑定用户提示词发送按钮不可点击状态
# 点击事件:发送用户提示词 disabled=ConversationState.is_user_prompt_sending_button_disabled,
on_click=ConversationState.handle_user_prompt, # 点击事件:运行
on_click=ConversationState.run,
display="inline-flex", display="inline-flex",
position="relative", position="relative",
justify_content="center", justify_content="center",
align_items="center", align_items="center",
padding="0 12px", padding="0 12px",
background_color=rx.cond( background_color=rx.cond(
ConversationState.is_user_prompt_sending_disabled, ConversationState.is_user_prompt_sending_button_disabled,
"var(--prismui-color-11)", "var(--prismui-color-11)",
"var(--prismui-color-5)", "var(--prismui-color-5)",
), ),
@ -787,7 +754,7 @@ def user_prompt_sending() -> rx.Component:
white_space="nowrap", white_space="nowrap",
overflow="hidden", overflow="hidden",
cursor=rx.cond( cursor=rx.cond(
ConversationState.is_user_prompt_sending_disabled, ConversationState.is_user_prompt_sending_button_disabled,
"not-allowed", "not-allowed",
"pointer", "pointer",
), ),
@ -828,11 +795,11 @@ def user_prompt_sending() -> rx.Component:
) )
def conversation_history_collapse_button( def conversation_history_toggle_button(
is_conversation_history_shown: bool, is_conversation_history_shown: bool,
) -> rx.Component: ) -> rx.Component:
""" """
会话历史折叠面板按钮 会话历史显示 / 隐藏按钮
:param is_conversation_history_shown: 会话历史展示状态 :param is_conversation_history_shown: 会话历史展示状态
:return: Component :return: Component
""" """
@ -870,13 +837,10 @@ def conversation_page() -> rx.Component:
""" """
会话页面布局参考 MetaChat从左到右分别为会话历史和工作区其中工作区从上到下分别为消息历史和用户提示词输入框 会话页面布局参考 MetaChat从左到右分别为会话历史和工作区其中工作区从上到下分别为消息历史和用户提示词输入框
""" """
# 会话历史展示状态
is_shown = ConversationState.is_conversation_history_shown
return rx.box( return rx.box(
rx.hstack( rx.hstack(
# 会话历史 # 会话历史
conversation_history(is_shown), conversation_history(ConversationState.is_conversation_history_shown),
# 工作区 # 工作区
rx.box( rx.box(
# 对话历史 # 对话历史
@ -905,8 +869,10 @@ def conversation_page() -> rx.Component:
overflow="hidden", overflow="hidden",
transition="all 0.18s ease-in-out", transition="all 0.18s ease-in-out",
), ),
# 会话历史折叠面板按钮 # 会话历史显示 / 隐藏按钮
conversation_history_collapse_button(is_shown), conversation_history_toggle_button(
ConversationState.is_conversation_history_shown
),
# 挂载事件:恢复会话状态 # 挂载事件:恢复会话状态
on_mount=AuthState.resume_conversation_state, on_mount=AuthState.resume_conversation_state,
position="relative", position="relative",

View File

@ -32,7 +32,7 @@ def nav_button(
is_actived = nav_button == AuthState.activated_nav_button is_actived = nav_button == AuthState.activated_nav_button
return rx.vstack( return rx.vstack(
# 图标 # 导航按钮的图标
rx.image( rx.image(
nav_buttons[nav_button]["src"], nav_buttons[nav_button]["src"],
width="36px", width="36px",
@ -47,7 +47,7 @@ def nav_button(
border_radius="var(--prismui-border-radius-3)", border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"), box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"),
), ),
# 标签 # 导航按钮的文本
rx.text( rx.text(
nav_buttons[nav_button]["text"], nav_buttons[nav_button]["text"],
line_height="20px", line_height="20px",
@ -57,7 +57,7 @@ def nav_button(
align_items="center", align_items="center",
gap="4px", gap="4px",
cursor="pointer", cursor="pointer",
# 点击事件:将指定导航按钮设置激活导航按钮 # 点击事件:设置激活导航按钮
on_click=AuthState.set_activated_nav_button(nav_button), on_click=AuthState.set_activated_nav_button(nav_button),
) )

View File

@ -27,8 +27,8 @@ from application.states.models import (
Conversation, Conversation,
Message, Message,
MessageType, MessageType,
Work, WorkFlow,
WorkType, WorkFlowType,
ConversationHistoryItem, ConversationHistoryItem,
usage_validate_python, usage_validate_python,
) )
@ -149,7 +149,8 @@ class ConversationState(rx.State):
""" """
# 获取当前数据库状态 # 获取当前数据库状态
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.update_conversation_record(conversation_id, is_deleted=True)
del self.conversations[conversation_id] del self.conversations[conversation_id]
# 删除后,若当前用户的会话字典为空则创建会话 # 删除后,若当前用户的会话字典为空则创建会话
@ -224,7 +225,7 @@ class ConversationState(rx.State):
conversation.user_prompt = user_prompt.strip() conversation.user_prompt = user_prompt.strip()
@rx.var @rx.var
def is_user_prompt_send_button_disabled(self) -> bool: def is_user_prompt_sending_button_disabled(self) -> bool:
""" """
用户提示词发送按钮不可点击状态 用户提示词发送按钮不可点击状态
:return: bool :return: bool
@ -238,8 +239,8 @@ class ConversationState(rx.State):
@rx.var @rx.var
def is_running(self) -> bool: def is_running(self) -> bool:
""" """
获取当前会话的运行状态 当前会话正在运行
:return: 当前会话的运行状态True 表示正在运行False 表示运行完成 :return: 当前会话正在运行True 表示正在运行False 表示运行完成
""" """
# 当前会话 # 当前会话
conversation = self.conversations.get(self.actived_conversation_id) conversation = self.conversations.get(self.actived_conversation_id)
@ -247,6 +248,18 @@ class ConversationState(rx.State):
return False return False
return conversation.is_running return conversation.is_running
@rx.var
def awaiting_stream(self) -> bool:
"""
当前会话正在等待流式输出
:return: 当前会话正在等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成
"""
# 当前会话
conversation = self.conversations.get(self.actived_conversation_id)
if not conversation:
return False
return conversation.awaiting_stream
@rx.event @rx.event
async def run(self) -> AsyncGenerator[None]: async def run(self) -> AsyncGenerator[None]:
""" """
@ -264,13 +277,22 @@ class ConversationState(rx.State):
# 将正在运行设置为是 # 将正在运行设置为是
conversation.is_running = True conversation.is_running = True
# 构建用户提示词消息实例 # 将等待流式输出设置为是
message = Message( conversation.awaiting_stream = True
# 获取数据库状态
db_state = await self.get_db_state()
# 先创建消息记录再添加消息实例
conversation.messages.update(
await db_state.create_message_record(
conversation_id=conversation.id,
message=Message(
type=MessageType.USER_PROMPT, type=MessageType.USER_PROMPT,
content=(user_prompt := conversation.user_prompt), content=(user_prompt := conversation.user_prompt),
),
)
) )
# 添加至消息字典
conversation.messages[message.id] = message
# 清空用户提示词 # 清空用户提示词
conversation.user_prompt = "" conversation.user_prompt = ""
yield # 通知前端更新渲染 yield # 通知前端更新渲染
@ -280,40 +302,32 @@ class ConversationState(rx.State):
# 初始化工具调用唯一标识集合 # 初始化工具调用唯一标识集合
tool_call_ids: set[str] = set() tool_call_ids: set[str] = set()
try: try:
# 获取数据库状态
db_state = await self.get_db_state()
# 获取消息历史列表
message_history = await db_state.get_message_history(
conversation_id=self.actived_conversation_id
)
usage = usage_validate_python(conversation.usage)
# 匹配工作流类型 # 匹配工作流类型
match conversation.work_flow: match conversation.work_flow:
# 预定航班 # 预定航班
case WorkType.BOOK_FLIGHT: case WorkFlowType.BOOK_FLIGHT:
from application.workshop.book_flight import run_stream_events from application.workshop.book_flight import run_stream_events
stream_events = run_stream_events(
work=conversation.work,
user_prompt=user_prompt,
message_history=message_history,
usage=usage,
)
# 非结构化对话 # 非结构化对话
case _: case _:
from application.workshop.unstructured_dialogue import ( from application.workshop.unstructured_dialogue import (
run_stream_events, run_stream_events,
) )
# 运行并流式输出事件 # 运行并流式输出事件
stream_events = run_stream_events( stream_events = run_stream_events(
user_prompt=user_prompt, user_prompt=user_prompt,
message_history=message_history, message_history=await db_state.get_message_history(
usage=usage, conversation_id=self.actived_conversation_id
),
usage=usage_validate_python(conversation.usage),
) )
# 获取运行流式输出事件 # 获取运行流式输出事件
async for event in stream_events: async for event in stream_events:
# 将等待流式输出设置为否
conversation.awaiting_stream = False
message: Message | None = None
match event: match event:
# ========== 开始事件 ========== # ========== 开始事件 ==========
case PartStartEvent( case PartStartEvent(
@ -335,24 +349,6 @@ class ConversationState(rx.State):
# 将消息实例唯一标识与片段索引映射 # 将消息实例唯一标识与片段索引映射
index_map_to_message_id[index] = message.id index_map_to_message_id[index] = message.id
# 工具调用分片开始事件
case ToolCallPart(
tool_name=tool_name, tool_call_id=tool_call_id
):
# 构建消息实例
message = Message(
type=MessageType.TOOL_CALL,
title=tool_name,
content=",",
is_running=True,
)
tool_call_ids.add(tool_call_id)
# 添加至消息字典
dialog.thoughts[index] = Thought(
type="tool_call",
content="正在生成调用参数",
)
# 文本分片开始事件 # 文本分片开始事件
case TextPart(content=content): case TextPart(content=content):
# 构建消息实例 # 构建消息实例
@ -399,102 +395,67 @@ class ConversationState(rx.State):
] ]
message.is_running = False message.is_running = False
message.title = "思考完成" message.title = "思考完成"
message.content = content
# ========== 函数工具调用事件 ==========
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 AgentRunResultEvent(result=result): case AgentRunResultEvent(result=result):
# 创建对话记录 # 更新使用量
await db_state.create_dialog_record( await db_state.update_conversation_record(
conversation_id=self.conversation_id, conversation.id, usage=result.usage
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( await db_state.create_run_record(
conversation_id=self.conversation_id, conversation_id=conversation.id,
_id=dialog.id,
new_messages=result.new_messages(), new_messages=result.new_messages(),
) )
if isinstance(message, Message):
# 强制更新会话并推送前端 # 强制更新会话并推送前端
self.conversations[self.actived_conversation_id] = conversation self.conversations[self.actived_conversation_id] = conversation
yield yield
# 创建对话记录
await db_state.create_message_record(
conversation_id=conversation.id,
message=message,
)
except Exception as e: except Exception as e:
... ...
finally: finally:
# 将正在运行设置为否 # 将正在运行设置为否
conversation.is_running = False conversation.is_running = False
# 将等待流式输出设置为否
conversation.awaiting_stream = False
self.conversations[self.actived_conversation_id] = conversation self.conversations[self.actived_conversation_id] = conversation
yield yield
@rx.event @rx.event
async def generate_prd(self) -> None: async def init_work_flow(self, work_flow_type: WorkFlowType) -> 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_message_collapse(self, message_id: str) -> None:
"""
展开/折叠消息组件
""" """
# 当前会话 # 当前会话
conversation = self.conversations.get(self.actived_conversation_id) conversation = self.conversations.get(self.actived_conversation_id)
if not conversation: if not conversation:
return return
conversation.messages[message_id].is_expanded ^= True
match work_flow_type:
# 预定航班
case WorkFlowType.BOOK_FLIGHT:
from application.workshop.book_flight import init_work_flow
# 初始化预定航班任务
conversation.work_flow = init_work_flow()
conversation.user_prompt = f"帮我找一班从 {conversation.work_flow.deps.origin}{conversation.work_flow.deps.destination}{conversation.work_flow.deps.date} 的航班"
@rx.event
def toggle_message_history_item_shown(self, message_id: str) -> None:
"""
展示 / 隐藏消息历史项
"""
# 当前会话
conversation = self.conversations.get(self.actived_conversation_id)
if not conversation:
return
conversation.messages[message_id].is_shown ^= True

View File

@ -5,23 +5,17 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from random import choices from random import choices
from typing import Any from typing import Any
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter, RunUsage
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter
from pydantic_ai._uuid import uuid7 from pydantic_ai._uuid import uuid7
import reflex as rx import reflex as rx
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 ( from application.states.models import (
Conversation, Conversation,
TaskStatus,
RunStatus,
MessageType, MessageType,
Run,
Message, Message,
deps_to_object, usage_dump_python,
usage_validate_python,
usage_limits_to_object,
) )
@ -265,56 +259,56 @@ class DatabaseState(rx.State):
) )
} }
async def delete_conversation_record(self, conversation_id: str) -> None: async def update_conversation_record(
self,
conversation_id: str,
description: str | None = None,
is_deleted: bool | None = None,
usage: RunUsage | None = None,
) -> None:
""" """
删除会话记录逻辑删除 更新会话记录
:param conversation_id: 指定会话唯一标识 :param conversation_id: 指定会话唯一标识
:param description: 会话描述
:param is_deleted: 会话已删除
:param usage: 使用量
: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(ConversationRecord, conversation_id)
if not record: if not record:
return return
record.is_deleted = True if isinstance(description, str):
record.description = description
if isinstance(is_deleted, bool):
record.is_deleted = is_deleted
if isinstance(usage, RunUsage):
record.usage = usage_dump_python(usage)
await session.commit() await session.commit()
async def save_new_messages_record1( async def create_message_record(
self, self,
conversation_id: str, conversation_id: str,
run_id: str, message: Message,
user_prompt: str,
thoughts: dict[int, Any],
result_output: str,
usage: dict[str, Any],
) -> dict[str, Message]: ) -> dict[str, Message]:
""" """
保存新增消息 创建消息记录
:param conversation_id: 会话唯一标识 :param conversation_id: 会话唯一标识
:param user_prompt: 用户提示词 :param message: 消息实例
:param result_output: 结果输出 :return: 消息实例
:return: 保存消息记录的唯一标识
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = MessageRecord( record = MessageRecord(
id=id, id=message.id,
conversation_id=conversation_id, conversation_id=conversation_id,
user_prompt=user_prompt, type=message.type,
thoughts=thoughts, title=message.title,
result_output=result_output, content=message.content,
usage=usage,
) )
session.add(record) session.add(record)
await session.commit() await session.commit()
await session.refresh(record) await session.refresh(record)
return { return {message.id: message}
record.id: Dialog(
id=record.id,
user_prompt=record.user_prompt,
thoughts=record.thoughts,
result_output=record.result_output,
usage=record.usage,
)
}
async def get_message_history(self, conversation_id: str) -> list[ModelMessage]: async def get_message_history(self, conversation_id: str) -> list[ModelMessage]:
""" """
@ -337,15 +331,13 @@ class DatabaseState(rx.State):
) )
return message_history return message_history
async def save_new_messages_record( async def create_run_record(
self, self,
id: str,
conversation_id: str, conversation_id: str,
new_messages: list[ModelMessage], new_messages: list[ModelMessage],
) -> None: ) -> None:
""" """
保存新增消息 创建运行记录
:param id: 运行唯一标识
:param conversation_id: 会话唯一标识 :param conversation_id: 会话唯一标识
:param new_messages: 新增消息 :param new_messages: 新增消息
:return: None :return: None
@ -353,7 +345,6 @@ class DatabaseState(rx.State):
async with rx.asession() as session: async with rx.asession() as session:
session.add( session.add(
RunRecord( RunRecord(
id=id,
conversation_id=conversation_id, conversation_id=conversation_id,
new_messages=ModelMessagesTypeAdapter.dump_json( new_messages=ModelMessagesTypeAdapter.dump_json(
new_messages new_messages

View File

@ -35,8 +35,8 @@ class Message(BaseModel):
is_running: bool = Field( is_running: bool = Field(
default=False, description="正在运行True 表示正在运行False 表示运行完成" default=False, description="正在运行True 表示正在运行False 表示运行完成"
) )
is_expanded: bool = Field( is_shown: bool = Field(
default=False, description="开组件True 表示展开False 表示折叠" default=False, description="示组件True 表示展示False 表示隐藏"
) )
@ -85,12 +85,12 @@ class WorkFlow(BaseModel):
工作流类 工作流类
""" """
type: WorkFlowType = Field(..., description="工作类型") type: WorkFlowType = Field(..., description="工作类型")
deps: Deps | None = Field(default=None, description="工作依赖项") deps: Deps | None = Field(default=None, description="工作依赖项")
usage: RunUsage = Field(default=RunUsage(), description="工作使用量") usage: RunUsage = Field(default=RunUsage(), description="工作使用量")
usage_limits: UsageLimits | None = Field( usage_limits: UsageLimits | None = Field(
default=None, default=None,
description="任务使用量限制", description="工作流使用量限制",
) )
@ -109,6 +109,7 @@ class Conversation(BaseModel):
is_running: bool = Field( is_running: bool = Field(
default=False, description="正在运行True 表示正在运行, False 表示运行结束" default=False, description="正在运行True 表示正在运行, False 表示运行结束"
) )
awaiting_stream: bool = Field(default=False, description="等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成")
class ConversationHistoryItem(BaseModel): class ConversationHistoryItem(BaseModel):
@ -121,6 +122,23 @@ class ConversationHistoryItem(BaseModel):
created_at: str = Field(..., description="会话创建日期时间") created_at: str = Field(..., description="会话创建日期时间")
class MessageHistoryItem(BaseModel):
"""
消息历史项类
"""
id: str = Field(..., description="消息唯一标识")
type: MessageType = Field(..., description="消息类型")
title: str = Field(default="", description="消息标题")
content: str = Field(default="", description="消息内容")
is_running: bool = Field(
default=False, description="正在运行True 表示正在运行False 表示运行完成"
)
is_shown: bool = Field(
default=False, description="展示组件True 表示展示False 表示隐藏"
)
# Dpes 适配器 # Dpes 适配器
DepsAdapter = TypeAdapter(Deps) DepsAdapter = TypeAdapter(Deps)
@ -149,16 +167,18 @@ UsageAdapter = TypeAdapter(RunUsage)
def usage_validate_python(usage: dict[str, Any]) -> RunUsage: def usage_validate_python(usage: dict[str, Any]) -> RunUsage:
""" """
Usage 转为对象 Usage 反序列化
""" """
if not usage: if not usage:
return RunUsage() return RunUsage()
return UsageAdapter.validate_python(usage) return UsageAdapter.validate_python(usage)
def usage_to_dict(usage: RunUsage) -> dict[str, Any]: def usage_dump_python(usage: RunUsage) -> dict[str, Any]:
""" """
Usage 转为字典 Usage 序列化
:param usage: 使用量
:return: python 字典
""" """
return UsageAdapter.dump_python(usage) return UsageAdapter.dump_python(usage)

View File

@ -6,21 +6,21 @@ import datetime
from typing import AsyncGenerator, Literal from typing import AsyncGenerator, Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
from application.states.models import ( from application.states.models import (
Task, WorkFlow,
TaskNodeResultEvent, WorkFlowType,
TaskType,
usage_limits_to_object, usage_limits_to_object,
usage_to_dict, usage_to_dict,
usage_to_object, usage_to_object,
Deps,
) )
from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS
class Deps(BaseModel): class Deps_(BaseModel):
""" """
依赖项类 依赖项类
""" """
@ -167,10 +167,9 @@ flight_info = """
""" """
def init_task() -> Task: def init_work_flow() -> WorkFlow:
return WorkFlow( return WorkFlow(
type=WorkType.BOOK_FLIGHT, type=WorkFlowType.BOOK_FLIGHT,
tools={"extract_flight_details": {"": ""}},
deps=Deps( deps=Deps(
flight_info=flight_info, flight_info=flight_info,
date=datetime.date(2025, 1, 10), date=datetime.date(2025, 1, 10),
@ -185,8 +184,6 @@ async def run_stream_events(
usage: RunUsage, usage: RunUsage,
user_prompt: str, user_prompt: str,
message_history: list[ModelMessage], message_history: list[ModelMessage],
work: WorkType | None = None,
) -> AsyncGenerator: ) -> AsyncGenerator:
result = None result = None
while True: while True:
@ -264,3 +261,73 @@ async def run_stream_events(
) )
yield event yield event
return return
# 工具调用分片开始事件
case ToolCallPart(
tool_name=tool_name, tool_call_id=tool_call_id
):
# 构建消息实例
message = Message(
type=MessageType.TOOL_CALL,
title=tool_name,
content=",",
is_running=True,
)
tool_call_ids.add(tool_call_id)
# 添加至消息字典
dialog.thoughts[index] = Thought(
type="tool_call",
content="正在生成调用参数",
)
# ========== 函数工具调用事件 ==========
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 _:
continue
# ========== 函数工具结果事件 ==========
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 _:
continue