From a7fead671aab0544b41bda1a5d90ec1e750158ed Mon Sep 17 00:00:00 2001 From: liubiren Date: Tue, 11 Aug 2026 14:24:31 +0800 Subject: [PATCH] 1 --- agent/application/pages/conversation.py | 528 ++++++++++------------ agent/application/pages/index.py | 6 +- agent/application/states/conversation.py | 213 ++++----- agent/application/states/database.py | 73 ++- agent/application/states/models.py | 38 +- agent/application/workshop/book_flight.py | 87 +++- 6 files changed, 475 insertions(+), 470 deletions(-) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 7f0fddc..cef3424 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -6,9 +6,8 @@ import reflex as rx from typing import Tuple from application.states.models import ( - Conversation, - Dialog, - Thought, + MessageHistoryItem, + MessageType, ConversationHistoryItem, ) from application.states import ConversationState, AuthState @@ -32,7 +31,7 @@ def conversation_history_item( return rx.list.item( rx.vstack( rx.hstack( - # 对话描述 + # 会话历史项的会话描述 rx.text( item.description, flex=1, @@ -51,7 +50,7 @@ def conversation_history_item( ), rx.hstack( rx.spacer(), - # 创建时间 + # 会话历史项的会话创建日期时间 rx.text( item.created_at, line_height="20px", @@ -68,7 +67,7 @@ def conversation_history_item( on_click=lambda: ConversationState.set_actived_conversation(item.id), width="100%", ), - # 更多按钮:点击更多按钮显示气泡卡片,可删除会话 + # 会话历史项的更多按钮 rx.box( rx.popover.root( rx.popover.trigger( @@ -143,9 +142,9 @@ def conversation_history_item( z_index="99", min_width="14px", cursor="pointer", - opacity=rx.cond(highlight, "1", "0"), + opacity=rx.cond(is_highlight, "1", "0"), transition="opacity 0.18s ease-in-out", - pointer_events=rx.cond(highlight, "auto", "none"), + pointer_events=rx.cond(is_highlight, "auto", "none"), ), display="flex", position="relative", @@ -154,12 +153,12 @@ def conversation_history_item( padding="16px", margin_bottom="8px", background=rx.cond( - highlight, + is_highlight, "var(--prismui-background-3)", "var(--prismui-background-color-1)", ), 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", style={ "&:hover": { @@ -175,16 +174,16 @@ def conversation_history_item( def conversation_history( - is_shown: bool, + is_conversation_history_shown: bool, ) -> rx.Component: """ 会话历史 - :param is_shown: 展示 / 隐藏 + :param is_conversation_history_shown: 展示 / 隐藏 :return: Component """ return rx.box( rx.vstack( - # 标题 + # 会话历史的标题 rx.hstack( rx.text( "会话历史", @@ -219,9 +218,9 @@ def conversation_history( backdrop_filter="blur(50px)", gap="12px", ), - width=rx.cond(is_shown, "25%", "0px"), - min_width=rx.cond(is_shown, "240px", "0px"), - max_width=rx.cond(is_shown, "380px", "0px"), + width=rx.cond(is_conversation_history_shown, "25%", "0px"), + min_width=rx.cond(is_conversation_history_shown, "240px", "0px"), + max_width=rx.cond(is_conversation_history_shown, "380px", "0px"), height="100%", transition="all 0.18s ease-in-out", overflow="hidden", @@ -334,282 +333,251 @@ def guidance() -> rx.Component: ) -def thought_showing(item: Tuple[int, Thought]): +def user_prompt(item: MessageHistoryItem) -> rx.Component: """ - 思考展示 - :param item: 思考实例 + 用户提示词 + :return: Component + """ + return rx.hstack( + rx.spacer(), + rx.vstack( + rx.text( + item.content, + max_width="600px", + padding="12px 16px", + background_color="var(--prismui-background-color-3)", + border_radius="var(--prismui-border-radius-3)", + line_height="1.5", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + rx.hstack( + # 再次发送 + rx.box( + rx.icon( + "rotate-ccw", + width="14px", + height="14px", + color="var(--prismui-color-2)", + ), + padding="4px", + border_radius="var(--prismui-border-radius-1)", + cursor="pointer", + style={ + "_hover": { + "background_color": "var(--prismui-background-color-2)", + } + }, + ), + width="100%", + margin_top="8px", + ), + ), + align_items="flex-start", + gap="4px", + width="100%", + margin_top="8px", + ) + + +def thinking(item: MessageHistoryItem) -> rx.Component: + """ + 思考 :return: Component """ - # 思考实例 - thought = item[1] return rx.vstack( - rx.match( - thought.type, - ( - "thinking", + # 思考的标题栏 + rx.box( + rx.hstack( rx.text( - thought.content, - line_height="1.6", + item.title, font_size="var(--prismui-font-size-1)", color="var(--prismui-color-3)", ), + rx.icon( + "chevron-right", + width="14px", + height="14px", + color="var(--prismui-color-3)", + transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"), + transition="transform 0.18s ease-in-out", + ), + align_items="center", + gap="4px", + margin_bottom="8px", + line_height="22px", ), - rx.fragment(), + # 点击事件,展示 / 隐藏消息历史项 + on_click=lambda: ConversationState.toggle_message_history_item_shown( + item.id + ), + cursor="pointer", ), - align_items="flex-start", + # 思考的内容 + rx.text( + item.content, + line_height="1.6", + font_size="var(--prismui-font-size-1)", + color="var(--prismui-color-3)", + ), + margin_bottom="8px", + ) + + +def result_output(item: MessageHistoryItem) -> rx.Component: + """ + 结果输出 + :return: Component + """ + return rx.markdown( + item.content, + component_map={ + "p": lambda text: rx.text( + text, + margin="4px 0", + line_height="1.75", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + "strong": lambda text: rx.text( + text, + display="inline", + font_weight="var(--prismui-font-weight-3)", + ), + "code": lambda text: rx.text( + text, + display="inline", + background_color="var(--prismui-background-color-6)", + border_radius="var(--prismui-border-radius-1)", + color="var(--prismui-color-5)", + ), + "h1": lambda text: rx.text( + text, + margin="8px 0 4px", + line_height="1.75", + font_size="var(--prismui-font-size-4)", + font_weight="var(--prismui-font-weight-3)", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + "h2": lambda text: rx.text( + text, + margin="6px 0 4px", + line_height="1.75", + font_size="var(--prismui-font-size-3)", + font_weight="var(--prismui-font-weight-3)", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + "h3": lambda text: rx.text( + text, + margin="4px 0", + line_height="1.75", + font_weight="var(--prismui-font-weight-3)", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + "ul": lambda children: rx.vstack( + children, + align_items="flex-start", + margin="4px 0", + style={"padding": "0 !important"}, + ), + "ol": lambda children: rx.vstack( + children, + align_items="flex-start", + margin="4px 0", + style={"padding": "0 !important"}, + ), + "li": lambda text: rx.hstack( + rx.box( + width="4.5px", + height="4.5px", + margin_right="8px", + background_color="var(--prismui-background-color-7)", + border_radius="100%", + ), + rx.text( + text, + line_height="1.75", + word_wrap="break-word", + word_break="break-all", + white_space="pre-line", + ), + align_items="center", + justify_content="flex-start", + margin_bottom="4px", + ), + "hr": lambda _: rx.divider( + height="1px", + margin="12px 0", + background_color="var(--prismui-background-color-2)", + ), + "table": lambda children: rx.el.table( + children, + width="100%", + margin="8px 0", + border_collapse="collapse", + ), + "thead": lambda children: rx.el.thead(children), + "tbody": lambda children: rx.el.tbody(children), + "tr": lambda children: rx.el.tr(children), + "th": lambda children: rx.el.th( + children, + padding="8px 12px", + background_color="var(--prismui-background-color-6)", + border="1px solid var(--prismui-background-color-2)", + text_align="left", + ), + "td": lambda children: rx.el.td( + children, + padding="8px 12px", + border="1px solid var(--prismui-background-color-2)", + text_align="left", + ), + }, width="100%", ) -def message_history_item(item: Tuple[str, Dialog]) -> rx.Component: +def message_history_item(item: MessageHistoryItem) -> 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.vstack( - rx.text( - dialog.user_prompt, - max_width="600px", - padding="12px 16px", - background_color="var(--prismui-background-color-3)", - border_radius="var(--prismui-border-radius-3)", - line_height="1.5", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - rx.hstack( - # 再次发送 - rx.box( - rx.icon( - "rotate-ccw", - width="14px", - height="14px", - color="var(--prismui-color-2)", - ), - padding="4px", - border_radius="var(--prismui-border-radius-1)", - cursor="pointer", - style={ - "_hover": { - "background_color": "var(--prismui-background-color-2)", - } - }, - ), - width="100%", - margin_top="8px", - ), - ), - align_items="flex-start", - gap="4px", - width="100%", - margin_top="8px", - ), - rx.fragment(), - ), - # 思考折叠面板 - rx.cond( - dialog.thoughts, - rx.vstack( - # 标题 + ConversationState.awaiting_stream, + rx.box( rx.box( - rx.hstack( - rx.text( - rx.cond(is_thinking, "思考中", "思考完成"), - font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", - ), - rx.icon( - "chevron-right", - width="14px", - height="14px", - color="var(--prismui-color-3)", - transform=rx.cond( - is_expanded, "rotate(90deg)", "rotate(0deg)" - ), - transition="transform 0.18s ease-in-out", - ), - align_items="center", - gap="4px", - margin_bottom="8px", - line_height="22px", - ), - # 点击事件,展开/折叠思考折叠面板 - on_click=lambda: ConversationState.toggle_collapse(dialog.id), - cursor="pointer", - ), - # 思考 - rx.box( - rx.box( - rx.auto_scroll( - rx.foreach( - dialog.thoughts, - 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", + class_name="loadin", + flex_shrink="0", ), + display="flex", + justify_content="center", + align_items="center", + width="32px", + height="18px", margin_bottom="8px", + overflow="hidden", ), - # 若思考列表为空且正在运行则显示加载动效 - rx.cond( - ConversationState.running_status, - 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.fragment(), + rx.match( + item.type, + (MessageType.USER_PROMPT, user_prompt(item)), + (MessageType.THINKING, thinking(item)), + (MessageType.RESULT_OUTPUT, result_output(item)), ), ), - # 结果输出 - rx.markdown( - dialog.result_output, - component_map={ - "p": lambda text: rx.text( - text, - margin="4px 0", - line_height="1.75", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - "strong": lambda text: rx.text( - text, - display="inline", - font_weight="var(--prismui-font-weight-3)", - ), - "code": lambda text: rx.text( - text, - display="inline", - background_color="var(--prismui-background-color-6)", - border_radius="var(--prismui-border-radius-1)", - color="var(--prismui-color-5)", - ), - "h1": lambda text: rx.text( - text, - margin="8px 0 4px", - line_height="1.75", - font_size="var(--prismui-font-size-4)", - font_weight="var(--prismui-font-weight-3)", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - "h2": lambda text: rx.text( - text, - margin="6px 0 4px", - line_height="1.75", - font_size="var(--prismui-font-size-3)", - font_weight="var(--prismui-font-weight-3)", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - "h3": lambda text: rx.text( - text, - margin="4px 0", - line_height="1.75", - font_weight="var(--prismui-font-weight-3)", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - "ul": lambda children: rx.vstack( - children, - align_items="flex-start", - margin="4px 0", - style={"padding": "0 !important"}, - ), - "ol": lambda children: rx.vstack( - children, - align_items="flex-start", - margin="4px 0", - style={"padding": "0 !important"}, - ), - "li": lambda text: rx.hstack( - rx.box( - width="4.5px", - height="4.5px", - margin_right="8px", - background_color="var(--prismui-background-color-7)", - border_radius="100%", - ), - rx.text( - text, - line_height="1.75", - word_wrap="break-word", - word_break="break-all", - white_space="pre-line", - ), - align_items="center", - justify_content="flex-start", - margin_bottom="4px", - ), - "hr": lambda _: rx.divider( - height="1px", - margin="12px 0", - background_color="var(--prismui-background-color-2)", - ), - "table": lambda children: rx.el.table( - children, - width="100%", - margin="8px 0", - border_collapse="collapse", - ), - "thead": lambda children: rx.el.thead(children), - "tbody": lambda children: rx.el.tbody(children), - "tr": lambda children: rx.el.tr(children), - "th": lambda children: rx.el.th( - children, - padding="8px 12px", - background_color="var(--prismui-background-color-6)", - border="1px solid var(--prismui-background-color-2)", - text_align="left", - ), - "td": lambda children: rx.el.td( - children, - padding="8px 12px", - border="1px solid var(--prismui-background-color-2)", - text_align="left", - ), - }, - width="100%", - ), padding="0 16px", width="100%", - key=dialog.id, ) @@ -618,17 +586,15 @@ def message_history() -> rx.Component: 消息历史 :return: Component """ - # 消息历史 - message_history = ConversationState.message_history return rx.box( # 若消息历史为空则显示引导,否则渲染消息历史项 rx.cond( - message_history.length() == 0, + ConversationState.message_history.length() == 0, guidance(), rx.vstack( rx.auto_scroll( rx.foreach( - message_history, + ConversationState.message_history, message_history_item, ), width="100%", @@ -765,18 +731,19 @@ def user_prompt_sending() -> rx.Component: line_height="21px", color="var(--prismui-color-9)", ), - loading=ConversationState.running_status, - # 是否禁用绑定用户提示词发送禁用状态 - disabled=ConversationState.is_user_prompt_sending_disabled, - # 点击事件:发送用户提示词 - on_click=ConversationState.handle_user_prompt, + # 加载状态:绑定当前会话的运行状态 + loading=ConversationState.is_running, + # 不可点击状态:绑定用户提示词发送按钮不可点击状态 + disabled=ConversationState.is_user_prompt_sending_button_disabled, + # 点击事件:运行 + on_click=ConversationState.run, display="inline-flex", position="relative", justify_content="center", align_items="center", padding="0 12px", background_color=rx.cond( - ConversationState.is_user_prompt_sending_disabled, + ConversationState.is_user_prompt_sending_button_disabled, "var(--prismui-color-11)", "var(--prismui-color-5)", ), @@ -787,7 +754,7 @@ def user_prompt_sending() -> rx.Component: white_space="nowrap", overflow="hidden", cursor=rx.cond( - ConversationState.is_user_prompt_sending_disabled, + ConversationState.is_user_prompt_sending_button_disabled, "not-allowed", "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, ) -> rx.Component: """ - 会话历史折叠面板按钮 + 会话历史显示 / 隐藏按钮 :param is_conversation_history_shown: 会话历史展示状态 :return: Component """ @@ -870,13 +837,10 @@ def conversation_page() -> rx.Component: """ 会话页面:布局参考 MetaChat,从左到右分别为会话历史和工作区。其中,工作区从上到下分别为消息历史和用户提示词输入框。 """ - # 会话历史展示状态 - is_shown = ConversationState.is_conversation_history_shown - return rx.box( rx.hstack( # 会话历史 - conversation_history(is_shown), + conversation_history(ConversationState.is_conversation_history_shown), # 工作区 rx.box( # 对话历史 @@ -905,8 +869,10 @@ def conversation_page() -> rx.Component: overflow="hidden", 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, position="relative", diff --git a/agent/application/pages/index.py b/agent/application/pages/index.py index b54c86f..927c9da 100644 --- a/agent/application/pages/index.py +++ b/agent/application/pages/index.py @@ -32,7 +32,7 @@ def nav_button( is_actived = nav_button == AuthState.activated_nav_button return rx.vstack( - # 图标 + # 导航按钮的图标 rx.image( nav_buttons[nav_button]["src"], width="36px", @@ -47,7 +47,7 @@ def nav_button( border_radius="var(--prismui-border-radius-3)", box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"), ), - # 标签 + # 导航按钮的文本 rx.text( nav_buttons[nav_button]["text"], line_height="20px", @@ -57,7 +57,7 @@ def nav_button( align_items="center", gap="4px", cursor="pointer", - # 点击事件:将指定导航按钮设置为激活的导航按钮 + # 点击事件:设置激活导航按钮 on_click=AuthState.set_activated_nav_button(nav_button), ) diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 2a09937..001ba8a 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -27,8 +27,8 @@ from application.states.models import ( Conversation, Message, MessageType, - Work, - WorkType, + WorkFlow, + WorkFlowType, ConversationHistoryItem, usage_validate_python, ) @@ -149,7 +149,8 @@ class ConversationState(rx.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] # 删除后,若当前用户的会话字典为空则创建会话 @@ -224,7 +225,7 @@ class ConversationState(rx.State): conversation.user_prompt = user_prompt.strip() @rx.var - def is_user_prompt_send_button_disabled(self) -> bool: + def is_user_prompt_sending_button_disabled(self) -> bool: """ 用户提示词发送按钮不可点击状态 :return: bool @@ -238,8 +239,8 @@ class ConversationState(rx.State): @rx.var def is_running(self) -> bool: """ - 获取当前会话的运行状态 - :return: 当前会话的运行状态(True 表示正在运行,False 表示运行完成) + 当前会话正在运行 + :return: 当前会话正在运行(True 表示正在运行,False 表示运行完成) """ # 当前会话 conversation = self.conversations.get(self.actived_conversation_id) @@ -247,6 +248,18 @@ class ConversationState(rx.State): return False 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 async def run(self) -> AsyncGenerator[None]: """ @@ -264,13 +277,22 @@ class ConversationState(rx.State): # 将正在运行设置为是 conversation.is_running = True - # 构建用户提示词消息实例 - message = Message( - type=MessageType.USER_PROMPT, - content=(user_prompt := conversation.user_prompt), + # 将等待流式输出设置为是 + 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, + content=(user_prompt := conversation.user_prompt), + ), + ) ) - # 添加至消息字典 - conversation.messages[message.id] = message # 清空用户提示词 conversation.user_prompt = "" yield # 通知前端更新渲染 @@ -280,40 +302,32 @@ class ConversationState(rx.State): # 初始化工具调用唯一标识集合 tool_call_ids: set[str] = set() 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: # 预定航班 - case WorkType.BOOK_FLIGHT: + case WorkFlowType.BOOK_FLIGHT: 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 _: from application.workshop.unstructured_dialogue import ( run_stream_events, ) - # 运行并流式输出事件 - stream_events = run_stream_events( - user_prompt=user_prompt, - message_history=message_history, - usage=usage, - ) + + # 运行并流式输出事件 + stream_events = run_stream_events( + user_prompt=user_prompt, + message_history=await db_state.get_message_history( + conversation_id=self.actived_conversation_id + ), + usage=usage_validate_python(conversation.usage), + ) # 获取运行流式输出事件 async for event in stream_events: + # 将等待流式输出设置为否 + conversation.awaiting_stream = False + message: Message | None = None match event: # ========== 开始事件 ========== case PartStartEvent( @@ -335,24 +349,6 @@ class ConversationState(rx.State): # 将消息实例唯一标识与片段索引映射 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): # 构建消息实例 @@ -399,102 +395,67 @@ class ConversationState(rx.State): ] message.is_running = False message.title = "思考完成" - - # ========== 函数工具调用事件 ========== - 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}" + message.content = 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.update_conversation_record( + conversation.id, usage=result.usage ) - # 创建结果记录 - await db_state.create_result_record( - conversation_id=self.conversation_id, - _id=dialog.id, + # 创建运行记录 + await db_state.create_run_record( + conversation_id=conversation.id, new_messages=result.new_messages(), ) - # 强制更新会话并推送前端 - self.conversations[self.actived_conversation_id] = conversation - yield + if isinstance(message, Message): + # 强制更新会话并推送前端 + self.conversations[self.actived_conversation_id] = conversation + yield + # 创建对话记录 + await db_state.create_message_record( + conversation_id=conversation.id, + message=message, + ) except Exception as e: ... finally: # 将正在运行设置为否 conversation.is_running = False + # 将等待流式输出设置为否 + conversation.awaiting_stream = False self.conversations[self.actived_conversation_id] = conversation yield @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) if not conversation: 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 diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 887efd4..ea07715 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -5,23 +5,17 @@ from datetime import datetime, timedelta from random import choices from typing import Any - -from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter +from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter, RunUsage from pydantic_ai._uuid import uuid7 import reflex as rx -from sqlalchemy import desc + from sqlmodel import Field, JSON, SQLModel, select, update from application.states.models import ( Conversation, - TaskStatus, - RunStatus, MessageType, - Run, Message, - deps_to_object, - usage_validate_python, - usage_limits_to_object, + usage_dump_python, ) @@ -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 description: 会话描述 + :param is_deleted: 会话已删除 + :param usage: 使用量 :return: None """ async with rx.asession() as session: record = await session.get(ConversationRecord, conversation_id) if not record: 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() - async def save_new_messages_record1( + async def create_message_record( self, conversation_id: str, - run_id: str, - user_prompt: str, - thoughts: dict[int, Any], - result_output: str, - usage: dict[str, Any], + message: Message, ) -> dict[str, Message]: """ - 保存新增消息 + 创建消息记录 :param conversation_id: 会话唯一标识 - :param user_prompt: 用户提示词 - :param result_output: 结果输出 - :return: 保存消息记录的唯一标识 + :param message: 消息实例 + :return: 消息实例 """ async with rx.asession() as session: record = MessageRecord( - id=id, + id=message.id, conversation_id=conversation_id, - user_prompt=user_prompt, - thoughts=thoughts, - result_output=result_output, - usage=usage, + type=message.type, + title=message.title, + content=message.content, ) session.add(record) await session.commit() await session.refresh(record) - return { - record.id: Dialog( - id=record.id, - user_prompt=record.user_prompt, - thoughts=record.thoughts, - result_output=record.result_output, - usage=record.usage, - ) - } + return {message.id: message} async def get_message_history(self, conversation_id: str) -> list[ModelMessage]: """ @@ -337,15 +331,13 @@ class DatabaseState(rx.State): ) return message_history - async def save_new_messages_record( + async def create_run_record( self, - id: str, conversation_id: str, new_messages: list[ModelMessage], ) -> None: """ - 保存新增消息 - :param id: 运行唯一标识 + 创建运行记录 :param conversation_id: 会话唯一标识 :param new_messages: 新增消息 :return: None @@ -353,7 +345,6 @@ class DatabaseState(rx.State): async with rx.asession() as session: session.add( RunRecord( - id=id, conversation_id=conversation_id, new_messages=ModelMessagesTypeAdapter.dump_json( new_messages diff --git a/agent/application/states/models.py b/agent/application/states/models.py index e02dbeb..f3b09b2 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -35,8 +35,8 @@ class Message(BaseModel): is_running: bool = Field( default=False, description="正在运行,True 表示正在运行,False 表示运行完成" ) - is_expanded: bool = Field( - default=False, description="展开组件,True 表示展开,False 表示折叠" + is_shown: bool = Field( + default=False, description="展示组件,True 表示展示,False 表示隐藏" ) @@ -85,12 +85,12 @@ class WorkFlow(BaseModel): 工作流类 """ - type: WorkFlowType = Field(..., description="工作类型") - deps: Deps | None = Field(default=None, description="工作依赖项") - usage: RunUsage = Field(default=RunUsage(), description="工作使用量") + type: WorkFlowType = Field(..., description="工作流类型") + deps: Deps | None = Field(default=None, description="工作流依赖项") + usage: RunUsage = Field(default=RunUsage(), description="工作流使用量") usage_limits: UsageLimits | None = Field( default=None, - description="任务使用量限制", + description="工作流使用量限制", ) @@ -109,6 +109,7 @@ class Conversation(BaseModel): is_running: bool = Field( default=False, description="正在运行,True 表示正在运行, False 表示运行结束" ) + awaiting_stream: bool = Field(default=False, description="等待流式输出,True 表示等待流式输出,False 表示已开始流式输出或已完成") class ConversationHistoryItem(BaseModel): @@ -121,6 +122,23 @@ class ConversationHistoryItem(BaseModel): 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 适配器 DepsAdapter = TypeAdapter(Deps) @@ -149,16 +167,18 @@ UsageAdapter = TypeAdapter(RunUsage) def usage_validate_python(usage: dict[str, Any]) -> RunUsage: """ - Usage 转为对象 + 将 Usage 反序列化 """ if not usage: return RunUsage() 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) diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index 05b1c21..7028250 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -6,21 +6,21 @@ import datetime from typing import AsyncGenerator, Literal 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 application.states.models import ( - Task, - TaskNodeResultEvent, - TaskType, + WorkFlow, + WorkFlowType, usage_limits_to_object, usage_to_dict, usage_to_object, + Deps, ) 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( - type=WorkType.BOOK_FLIGHT, - tools={"extract_flight_details": {"": ""}}, + type=WorkFlowType.BOOK_FLIGHT, deps=Deps( flight_info=flight_info, date=datetime.date(2025, 1, 10), @@ -185,8 +184,6 @@ async def run_stream_events( usage: RunUsage, user_prompt: str, message_history: list[ModelMessage], - work: WorkType | None = None, - ) -> AsyncGenerator: result = None while True: @@ -264,3 +261,73 @@ async def run_stream_events( ) yield event 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 \ No newline at end of file