diff --git a/智能体/application/components/chat.py b/智能体/application/components/chat.py deleted file mode 100644 index 1aded1c..0000000 --- a/智能体/application/components/chat.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- -""" -聊天页面相关组件 -""" -import reflex as rx - -from application.models import PartType, Part, Dialog -from application.state import ChatState - - -def render_user_prompt(user_prompt: str) -> rx.Component: - """ - 渲染用户提示词 - :param user_prompt: 用户提示词 - :return: Component - """ - return rx.markdown( - user_prompt, - color=rx.color("gray", 12), # 文字颜色 - background_color=rx.color("gray", 2), # 背景颜色 - display="inline-block", # 布局模式:自适应文本宽度 - max_width="85%", # 最大宽度 - padding_x="1.25em", # 水平内边距 - padding_y="0.5em", # 垂直内边距 - margin_left="auto", # 左侧外边距自动调整 - margin_bottom="8px", # 底部外边距 - border_radius="12px", # 圆角 - ) - - -def render_part(dialog_id: str, part_id: str, part: Part): - """ - 渲染片段 - :param dialog_id: 对话唯一标识 - :param part_id: 片段唯一标识 - :param part: 片段实例 - :return: Component - """ - return rx.match( - part.part_type, - ( - PartType.TEXT, - rx.markdown( - part.content, - color=rx.color("gray", 12), # 字体颜色 - background_color="transparent", # 背景颜色:设置为透明以继承父元素背景颜色 - display="block", # 布局模式:铺满 - max_width="85%", # 最大宽度 - padding="0", # 内边距 - margin_right="auto", # 右侧外边距自动调整 - margin_bottom="12px", # 底部外边距 - key=part_id, - ), - ), # 片段类型为文本 - (PartType.FINISHED, rx.fragment(key=part_id)), # 片段类型为结束 - rx.box( - rx.cond( - part.is_open, - rx.hstack( - rx.match( - part.part_type, - ( - PartType.THINKING, - rx.text( - "正在思考", - font_size="0.90rem", - bold=True, - color=rx.color("gray", 10), # 字体颜色 - ), - ), - ( - PartType.TOOL_NAME, - rx.text( - "正在调用", - " ", - part.content, - " ", - font_size="0.90rem", - bold=True, - color=rx.color("gray", 10), - ), - ), - ), - rx.spacer(), - rx.icon("chevron_up", size=16, color=rx.color("gray", 6)), - width="100%", - cursor="pointer", - padding_x="1em", - padding_y="0.6em", - background_color=rx.color("gray", 2), - border_radius="8px", - on_click=lambda: ChatState.toggle_part_collapse(dialog_id, part_id), - ), # 折叠面板打开时标题栏 - rx.hstack( - rx.hstack( - rx.text( - loading_prefix, - font_size="0.90rem", - bold=True, - color=rx.color("gray", 10), - ), - loading_dot_group(part.is_streaming), - spacing="8", # 间距 - align_items="center", - ), - rx.spacer(), - rx.icon("chevron_down", size=16, color=rx.color("gray", 6)), - width="100%", - cursor="pointer", - on_click=lambda: ChatState.toggle_part_collapse(dialog_id, part_id), - padding_x="1em", - padding_y="0.6em", - background_color=rx.color("gray", 2), - border_radius="8px", - ), # 折叠面板关闭时标题栏 - ), - rx.cond(part.is_open, detail_box, rx.fragment()), - width="100%", - max_width="85%", - margin_bottom="10px", - cursor="pointer", - key=part_id, - ), # 片段类型为工具相关 - ) - - -def render_dialog_item(dialog_id: str, dialog: Dialog) -> rx.Component: - """ - 渲染对话项 - :param dialog: 对话实例 - :return: Component - """ - return rx.box( - rx.box( - render_user_prompt(user_prompt=dialog.user_prompt), - text_align="right", - width="100%", - margin_bottom="8px", - ), - rx.box( - rx.foreach( - dialog.output, - lambda part_id, part: render_part( - dialog_id=dialog_id, part_id=part_id, part=part - ), - ), - text_align="left", - width="100%", - margin_bottom="8px", - ), - width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值 - margin_x="auto", # 水平外边距:自动调整 - key=dialog_id, - ) - - -def render_dialog_list() -> rx.Component: - """ - 渲染对话列表 - :return: Component - """ - return rx.auto_scroll( - rx.foreach( - ChatState.get_dialogs, - lambda dialog_id, dialog: render_dialog_item( - dialog_id=dialog_id, dialog=dialog - ), - ), - flex="1", - padding="8px", - overflow_y="auto", - ) - - -def render_input_bar() -> rx.Component: - """ - 渲染输入栏 - """ - return rx.center( - rx.vstack( - rx.form( - rx.hstack( - rx.input( - name="user_prompt", - placeholder="请输入...", - flex="auto", - ), - rx.button( - "发送", - type="submit", - loading=ChatState.get_current_chat_status, # 正在流式输出时按钮显示为 loading - disabled=ChatState.get_current_chat_status, # 正在流式输出时按钮禁用 - ), - max_width="50em", - margin="0 auto", - align_items="center", - ), - on_submit=ChatState.process_input, # 处理输入,返回流式输出 - reset_on_submit=True, # 提交后清空输入框 - ), - rx.text( - "抹茶兔兔工作室", - text_align="center", - font_size=".75em", - color=rx.color("mauve", 10), - ), - width="100%", - padding_x="16px", - align="stretch", - ), - position="sticky", - bottom="0", - left="0", - padding_y="16px", - backdrop_filter="auto", - backdrop_blur="lg", - border_top=f"1px solid {rx.color('mauve', 3)}", - background_color=rx.color("mauve", 2), - align="stretch", - width="100%", - ) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center") diff --git a/智能体/application/components/conversation.py b/智能体/application/components/conversation.py new file mode 100644 index 0000000..d99a350 --- /dev/null +++ b/智能体/application/components/conversation.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +""" +渲染对话相关组件 +""" +import reflex as rx + +from application.models import Run, Reasoning +from application.state.conversation import ConversationState + + +def render_user_prompt(run: Run) -> rx.Component: + """ + 渲染用户提示词 + :param run: 运行实例 + :return: Component + """ + return rx.box( + rx.markdown( + run.user_prompt, + color=rx.color("gray", 12), # 文字颜色 + background_color=rx.color("gray", 2), # 背景颜色 + display="inline-block", # 布局模式:自适应文本宽度 + max_width="85%", # 最大宽度 + padding_x="1.25em", # 水平内边距 + padding_y="0.5em", # 垂直内边距 + margin_left="auto", # 左侧外边距自动调整 + margin_bottom="8px", # 底部外边距 + border_radius="12px", # 圆角 + ), + text_align="right", + width="100%", + margin_bottom="8px", + ) + + +def render_reasoning(reasoning_id: int, reasoning: Reasoning): + """ + 渲染推理 + :param reasoning_id: 对话唯一标识 + :param reasoning: 推理实例 + :return: Component + """ + return rx.hstack( + rx.vstack( + rx.box( + width="8px", + height="8px", + background_color=rx.color("blue", 7), + ), + rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)), + align_items="center", + spacing="0", + height="100%", + ), + rx.markdown(reasoning.content, color=rx.color("gray", 11), padding_y="4px"), + align_items="flex-start", + spacing="2", + width="100%", + key=reasoning_id, + ) + + +def render_reasoning_panel(run_id: str, run: Run) -> rx.Component: + """ + 渲染推理面板 + :param run_id: 运行唯一标识 + :param run: 运行实例 + :return: Component + """ + # 推理字典 + reasonings = run.reasonings + # 推理面板展开状态 + is_reasoning_panel_open = run.is_reasoning_panel_open + + return rx.cond( + not reasonings, + rx.fragment(), + rx.box( + # 标题栏 + rx.hstack( + rx.cond( + run.is_reasoning, + rx.badge("正在推理"), + rx.badge("推理完成"), + ), + rx.spacer(), + # 若推理面板展开则渲染上箭头,否则渲染下箭头 + rx.icon( + rx.cond(is_reasoning_panel_open, "chevron_up", "chevron_down"), + size=16, + color=rx.color("mauve", 6), + ), + width="100%", + margin_bottom="6px", + on_click=lambda: ConversationState.toggle_reasoning_panel( + run_id + ), # 点击事件:展开/折叠指定运行唯一标识的推理面板 + ), + # 若推理面板展开则遍历渲染推理,否则不渲染 + rx.cond( + is_reasoning_panel_open, + rx.foreach( + reasonings, + lambda reasoning_id, reasoning: render_reasoning( + reasoning_id=reasoning_id, reasoning=reasoning + ), + ), + rx.fragment(), + ), + text_align="left", + width="100%", + margin_bottom="8px", + ), + ) + + +def render_assistant_content(run: Run) -> rx.Component: + """ + 渲染回复正文 + :param run: 运行实例,包含用户提示词、推理字典和回复正文 + :return: Component + """ + # 若回复正文为空则不渲染,否则渲染回复正文 + return rx.cond( + not run.assistant_content, + rx.fragment(), + rx.box( + rx.markdown( + run.assistant_content, + color=rx.color("gray", 12), # 文字颜色 + background_color=rx.color("gray", 2), # 背景颜色 + display="inline-block", # 布局模式:自适应文本宽度 + max_width="85%", # 最大宽度 + padding_x="1.25em", # 水平内边距 + padding_y="0.5em", # 垂直内边距 + margin_left="auto", # 左侧外边距自动调整 + margin_bottom="8px", # 底部外边距 + border_radius="12px", # 圆角 + ), + ), + ) + + +def render_run(run_id: str, run: Run) -> rx.Component: + """ + 渲染运行 + :param run: 运行,包含用户提示词、推理字典和回复正文 + :return: Component + """ + return rx.box( + # 渲染用户提示词 + render_user_prompt(run=run), + # 渲染推理面板 + render_reasoning_panel(run_id=run_id, run=run), + # 渲染回复正文 + render_assistant_content(run=run), + width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值 + margin_x="auto", # 水平外边距:自动调整 + key=run_id, + ) + + +def render_welcome() -> rx.Component: + """ + 渲染欢迎信息 + :return: Component + """ + return rx.center( + rx.vstack( + rx.spacer(), + # 智能体图标和名称 + rx.hstack( + rx.icon("info", size=18), + rx.text("智能体"), + ), + # 预设用户提示词 + rx.box( + rx.vstack( + # 标题 + rx.text("猜你想问"), + rx.hstack( + rx.button("你可以帮我做什么"), + ), + ) + ), + spacing="4", + width="min(100%, 40em)", + padding_y="40px", + ), + border="none", + ) + + +def render_conversation() -> rx.Component: + """ + 渲染对话,包括若干次运行 + :return: Component + """ + # 运行字典 + runs = ConversationState.get_runs + + # 若运行字典为空则渲染欢迎信息,否则遍历渲染运行 + return rx.auto_scroll( + rx.cond( + not runs, + # 渲染欢迎信息 + render_welcome(), + rx.foreach( + runs, + lambda run_id, run: render_run(run_id=run_id, run=run), + ), + ), + flex="1", + padding="8px", + overflow_y="auto", + ) + + +def render_custom_input_box() -> rx.Component: + """ + 渲染自定义输入框 + """ + return rx.form( + rx.box( + rx.vstack( + # 输入区域 + rx.input( + name="user_prompt", + placeholder="发消息...", + flex="auto", + border="none", # 外框边线 + outline="none", # 高亮轮廓线 + padding_bottom="8px", + ), + # 操作区域,暂仅包含发送按钮 + rx.hstack( + rx.spacer(), # 占位符 + # 发送按钮 + rx.button( + rx.icon("arrow-up", size=18), + color_scheme="blue", + radius="full", + width="36px", + height="36px", + padding="0", + type="submit", + loading=ConversationState.get_running_status, + disabled=ConversationState.get_running_status, + ), + width="100%", + ), + spacing="8", + padding_x="12px", + padding_y="12px", + ), + border=f"1px solid {rx.color('mauve', 4)}", + radius="large", + background_color="white", + ), + max_width="50em", # 最大宽度 + margin="0 auto", # 水平居中 + align_items="center", # 子元素垂直居中 + spacing="0", # 子元素间距 + ) + + +def render_input_box() -> rx.Component: + """ + 渲染输入框 + """ + return rx.center( + rx.vstack( + render_custom_input_box(), # 渲染自定义输入框 + # 底部文案 + rx.text( + "内容由大模型生成,无法确保准确性和完整性,仅供参考", + text_align="center", + font_size=".75em", + color=rx.color("mauve", 10), + ), + width="100%", + padding_x="16px", + align="stretch", + ), + position="sticky", + bottom="0", + left="0", + padding_y="16px", + backdrop_filter="auto", + backdrop_blur="lg", + border_top=f"1px solid {rx.color('mauve', 3)}", + background_color=rx.color("mauve", 2), + align="stretch", + width="100%", + ) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center") diff --git a/智能体/application/components/frame.py b/智能体/application/components/frame.py new file mode 100644 index 0000000..8547e36 --- /dev/null +++ b/智能体/application/components/frame.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +""" +渲染框架相关组件 +""" + +import reflex as rx + +from application.state.conversation import ConversationState +from application.state.create_conversation_modal import CreateConversationModalState +from application.state.frame import FrameState +from application.models import SidebarIconNavButtonKind + + +def render_conversation(chat_id: str, chat_description: str) -> rx.Component: + """ + 渲染对话 + :param chat_id: 聊天唯一标识 + :param description: 聊天描述 + :return: Component + """ + return rx.drawer.close( + rx.hstack( + rx.button( + chat_description, + on_click=lambda: ChatState.switch_chat(chat_id), # 点击按钮将切换会话 + width="80%", + variant="surface", + ), # 点击按钮将切换聊天 + rx.button( + rx.icon( + tag="trash", + on_click=lambda: ChatState.delete_chat(chat_id), # 点击按钮删除聊天 + stroke_width=1, + ), + width="20%", + variant="surface", + color_scheme="red", + ), + width="100%", + ), + key=chat_id, # 使用聊天唯一标识作为键 + ) + + +def render_chat_list(trigger) -> rx.Component: + """ + 渲染聊天列表 + """ + return rx.drawer.root( + rx.drawer.trigger(trigger), + rx.drawer.overlay(), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.heading("聊天列表", color=rx.color("mauve", 11)), + rx.divider(), + rx.foreach( + ChatState.get_chats, # 获取聊天列表 + lambda chat_id, chat: render_chat_item( + chat_id=chat_id, + chat_description=chat.description, + ), # 创建聊天组件 + ), + align_items="stretch", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="2em", + background_color=rx.color("mauve", 2), + outline="none", + ) + ), + direction="left", + ) + + +def render_create_chat_modal(trigger) -> rx.Component: + """ + 渲染新建聊天模态窗 + """ + return rx.dialog.root( + rx.dialog.trigger(trigger), + rx.dialog.content( + rx.form( + rx.hstack( + rx.input( + name="chat_description", + placeholder="请输入聊天描述(可选)", + flex="auto", + min_width="20ch", + ), + rx.button("新建"), + spacing="2", + wrap="wrap", + width="100%", + ), + on_submit=ChatState.create_chat, + ), + background_color=rx.color("mauve", 1), + ), # 模态窗内容容器 + open=CreateChatState.is_open, + on_open_change=CreateChatState.toggle, + ) + + +def render_sidebar() -> rx.Component: + """ + 渲染侧边栏 + """ + return rx.box( + rx.vstack( + rx.vstack( + # 智能体图标和名称 + rx.vstack( + rx.icon("info"), + rx.text("智能体"), + spacing="2", + margin_bottom="16px", + ), + # 分隔线 + rx.divider(margin_bottom="12px"), + # 对话 + rx.button( + rx.vstack( + rx.icon("message-square", size=18), + rx.text("对话"), + spacing="2", + ), + width="100%", + justify_content="flex-start", + variant=rx.cond( + ChatState.active_sidebar_tab == "chat", "soft", "surface" + ), + color_scheme="blue", + on_click=ChatState.set_active_tab("chat"), + ), + spacing="10px", + ), + # 底部语言、设置按钮 + rx.vstack( + rx.button(rx.text("EN"), variant="surface", width="100%"), + rx.button(rx.icon("settings"), variant="surface", width="100%"), + spacing="6px", + ), + height="100vh", + width="64px", + padding_y="20px", + padding_x="12px", + align_items="stretch", + justify_content="space-between", + ), + width="64px", + min_width="64px", + background=rx.color("mauve", 2), + border_right=f"1px solid {rx.color('mauve', 3)}", + position="sticky", + top=0, + ) + + +def render_sidebar_icon_nav_button(sidebar_icon_nav_button: SidebarIconNavButton) -> rx.Component: + """ + 渲染侧边栏图标导航按钮 + :param sidebar_icon_nav_button: 侧边栏图标导航按钮 + """ + # 获取侧边栏图标导航按钮名称 + sidebar_icon_nav_button_name = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["name"] + # 获取侧边栏图标导航按钮图标名称 + sidebar_icon_nav_button_icon = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["icon"] + + # 若当前侧边栏图标导航按钮非指定图标导航按钮则正常渲染,否则高亮渲染 + return rx.cond( + FrameState.sidebar_icon_nav_button != sidebar_icon_nav_button, + rx.button( + rx.vstack( + rx.icon(sidebar_icon_nav_button, size=18), + rx.text(sidebar_icon_nav_button), + spacing="2", + align_items="center", + ), + width="100%", + justify_content="center", + variant="surface", + color_scheme="blue", + on_click=FrameState.set_active_sidebar_tab(sidebar_icon_nav_button_name), + padding_y="10px", + ), + rx.button( + rx.vstack( + rx.icon(sidebar_icon_nav_button_name, size=18), + rx.text(sidebar_icon_nav_button_name), + spacing="2", + align_items="center", + ), + width="100%", + justify_content="center", + variant="soft", + color_scheme="blue", + on_click=FrameState.set_active_sidebar_tab(sidebar_icon_nav_button_name), + padding_y="10px", + ), + ) + + +def render_frame() -> rx.Component: + """ + 渲染框架,参考 MateChat + """ + return rx.hstack( + rx.badge( + ConversationState.get_current_chat_description, + size="3", + variant="soft", + margin_inline_end="auto", + ), + render_create_chat_modal( + rx.box(rx.tooltip(rx.icon("message-square-plus"), content="新建聊天")) + ), + render_chat_list( + rx.box( + rx.tooltip( + rx.icon("messages-square"), + content="聊天历史", + ) + ) + ), + justify_content="space-between", + align_items="center", + padding="12px", + border_bottom=f"1px solid {rx.color('mauve', 3)}", + background_color=rx.color("mauve", 2), + ) diff --git a/智能体/application/components/navbar.py b/智能体/application/components/navbar.py deleted file mode 100644 index 6d3edd7..0000000 --- a/智能体/application/components/navbar.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -""" -导航栏相关组件 -""" - -import reflex as rx - -from application.state.chat import ChatState -from application.state.create_chat import CreateChatState - - -def render_chat_item(chat_id: str, chat_description: str) -> rx.Component: - """ - 渲染聊天项 - :param chat_id: 聊天唯一标识 - :param description: 聊天描述 - :return: Component - """ - return rx.drawer.close( - rx.hstack( - rx.button( - chat_description, - on_click=lambda: ChatState.switch_chat(chat_id), # 点击按钮将切换会话 - width="80%", - variant="surface", - ), # 点击按钮将切换聊天 - rx.button( - rx.icon( - tag="trash", - on_click=lambda: ChatState.delete_chat(chat_id), # 点击按钮删除聊天 - stroke_width=1, - ), - width="20%", - variant="surface", - color_scheme="red", - ), - width="100%", - ), - key=chat_id, # 使用聊天唯一标识作为键 - ) - - -def render_chat_list(trigger) -> rx.Component: - """ - 渲染聊天列表 - """ - return rx.drawer.root( - rx.drawer.trigger(trigger), - rx.drawer.overlay(), - rx.drawer.portal( - rx.drawer.content( - rx.vstack( - rx.heading("聊天列表", color=rx.color("mauve", 11)), - rx.divider(), - rx.foreach( - ChatState.get_chats, # 获取聊天列表 - lambda chat_id, chat: render_chat_item( - chat_id=chat_id, - chat_description=chat.description, - ), # 创建聊天组件 - ), - align_items="stretch", - width="100%", - ), - top="auto", - right="auto", - height="100%", - width="20em", - padding="2em", - background_color=rx.color("mauve", 2), - outline="none", - ) - ), - direction="left", - ) - - -def render_create_chat_modal(trigger) -> rx.Component: - """ - 渲染新建聊天模态窗 - """ - return rx.dialog.root( - rx.dialog.trigger(trigger), - rx.dialog.content( - rx.form( - rx.hstack( - rx.input( - name="chat_description", - placeholder="请输入聊天描述(可选)", - flex="auto", - min_width="20ch", - ), - rx.button("新建"), - spacing="2", - wrap="wrap", - width="100%", - ), - on_submit=ChatState.create_chat, - ), - background_color=rx.color("mauve", 1), - ), # 模态窗内容容器 - open=CreateChatState.is_open, - on_open_change=CreateChatState.toggle, - ) - - -def render_navbar() -> rx.Component: - """ - 渲染导航栏 - """ - return rx.hstack( - rx.badge( - ChatState.get_current_chat_description, - size="3", - variant="soft", - margin_inline_end="auto", - ), - render_create_chat_modal( - rx.box(rx.tooltip(rx.icon("message-square-plus"), content="新建聊天")) - ), - render_chat_list( - rx.box( - rx.tooltip( - rx.icon("messages-square"), - content="聊天历史", - ) - ) - ), - justify_content="space-between", - align_items="center", - padding="12px", - border_bottom=f"1px solid {rx.color('mauve', 3)}", - background_color=rx.color("mauve", 2), - ) diff --git a/智能体/application/models.py b/智能体/application/models.py index ecdb853..50e1142 100644 --- a/智能体/application/models.py +++ b/智能体/application/models.py @@ -12,6 +12,20 @@ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter from sqlmodel import Field as SqlField, SQLModel +class SidebarIconNavButtonKind(StrEnum): + """侧边栏图标导航按钮种类""" + + CONVERSATION = "conversation" + KNOWLEDGE = "knowledge" + + +class SidebarIconNavButton(BaseModel): + """侧边栏图标导航按钮类""" + + icon: str = Field(..., description="侧边栏图标导航按钮图标") + name: str = Field(..., description="侧边栏图标导航按钮名称") + + # 消息历史数据表模型 # 重新初始化:先手动删除 alembic 相关配置和文件夹,再使用 reflex db init 初始化数据库表 class MessageHistory(SQLModel, table=True): @@ -44,37 +58,12 @@ class MessageHistory(SQLModel, table=True): ) -class EventKind(StrEnum): - """事件类型""" - - PART_START = "part_start" - PART_DELTA = "part_delta" - PART_END = "part_end" - FUNCTION_TOOL_CALL = "function_tool_call" - FUNCTION_TOOL_RESULT = "function_tool_result" - RUN_START = "run_start" - RUN_END = "run_end" - - -class PartKind(StrEnum): - """分片类型""" +class ReasoningKind(StrEnum): + """推理种类""" THINKING = "thinking" TOOL_SEARCH = "tool-search" - CAPABILITY_LOAD = "capability-load" - TOOL_CALL = "tool-call" - TEXT = "text" - TOOL_RETURN = "tool-return" - RETRY_PROMPT = "retry-prompt" - RUN_RETURN = "run-return" - - -class Kind(StrEnum): - """分段种类""" - - THINKING = "thinking" - TOOL_SEARCH = "tool-search" - LOAD_CAPABILITY = "load-capability" + CAPABILITY_LOAD = "load-capability" TOOL_CALL = "tool-call" TEXT = "text" TOOL_RETURN = "tool-return" @@ -85,7 +74,7 @@ class Kind(StrEnum): class Reasoning(BaseModel): """推理类""" - kind: Kind = Field(..., description="推理类型") + kind: ReasoningKind = Field(..., description="推理种类") content: str = Field(default="", description="推理内容") @@ -98,12 +87,16 @@ class Run(BaseModel): ) is_reasoning: bool = Field( default=False, - description="推理状态,True 表示正在推理,False 表示非正在推理", + description="推理状态,True 表示正在推理,False 表示推理完成", + ) + is_reasoning_panel_open: bool = Field( + default=False, + description="推理面板展开状态,True 表示推理面板展开,False 表示推理面板折叠", ) assistant_content: str = Field(default="", description="回复正文") is_running: bool = Field( default=False, - description="运行状态,True 表示正在运行,False 表示非正在运行", + description="运行状态,True 表示正在运行,False 表示运行完成", ) diff --git a/智能体/application/state/conversation.py b/智能体/application/state/conversation.py index 935cbcb..8a527ea 100644 --- a/智能体/application/state/conversation.py +++ b/智能体/application/state/conversation.py @@ -2,16 +2,14 @@ """ 对话状态 """ -from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast +from typing import Any, AsyncGenerator, Dict from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai._uuid import uuid7 -from pydantic_ai.capabilities import AgentCapability from pydantic_ai.messages import ( FunctionToolCallEvent, FunctionToolResultEvent, LoadCapabilityCallPart, - ModelMessage, PartDeltaEvent, PartEndEvent, PartStartEvent, @@ -21,12 +19,12 @@ from pydantic_ai.messages import ( ToolCallPart, ToolSearchCallPart, ) -from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider +from pydantic_ai.run import AgentRunResultEvent import reflex as rx -from application.models import Conversation, EventKind, PartKind, Run, Reasoning, Kind +from application.models import Conversation, Reasoning, ReasoningKind, Run from application.state.create_conversation_modal import CreateConversationModalState from application.state.database import DatabaseState @@ -156,10 +154,10 @@ class ConversationState(rx.State): return conversation.runs if conversation else {} @rx.var - def get_run_running_status(self) -> bool: + def get_running_status(self) -> bool: """ - 获取当前运行运行状态 - :return: 当前运行状态(True 表示正在运行,False 表示非正在运行) + 获取当前运行状态 + :return: 当前运行状态(True 表示正在运行,False 表示运行完成) """ # 当前对话 conversation = self.conversations.get(self.conversation_id) @@ -221,12 +219,13 @@ class ConversationState(rx.State): match part: # 思考分片开始事件 case ThinkingPart(content=content): - # 若上一分片种类为空则将推理状态设置为正在推理 + # 若上一分片种类为空则将推理状态设置为正在推理、推理面板展开状态设置为展开 if not previous_part_kind: run.is_reasoning = True + run.is_reasoning_panel_open = True run.reasonings[index] = Reasoning( - kind=Kind.THINKING, content=content + kind=ReasoningKind.THINKING, content=content ) yield @@ -236,7 +235,7 @@ class ConversationState(rx.State): tool_call_ids[tool_call_id] = index run.reasonings[index] = Reasoning( - kind=Kind.TOOL_SEARCH, content="正在检索" + kind=ReasoningKind.TOOL_SEARCH, content="正在生成检索关键词" ) yield @@ -245,7 +244,8 @@ class ConversationState(rx.State): tool_call_ids[tool_call_id] = index run.reasonings[index] = Reasoning( - kind=Kind.LOAD_CAPABILITY, content="正在加载能力" + kind=ReasoningKind.CAPABILITY_LOAD, + content="正在生成加载参数", ) yield @@ -254,7 +254,7 @@ class ConversationState(rx.State): tool_call_ids[tool_call_id] = index run.reasonings[index] = Reasoning( - kind=Kind.TOOL_CALL, content="正在调用工具" + kind=ReasoningKind.TOOL_CALL, content="正在生成调用参数" ) yield @@ -289,54 +289,58 @@ class ConversationState(rx.State): match part: # 思考分片结束事件 case ThinkingPart(part_kind=part_kind, content=content): - # 若下一分片种类为文本则将推理状态设置为非正在推理 - if next_part_kind == Kind.TEXT: + # 若下一分片种类为文本则将推理状态设置为推理完成、推理面板展开状态设置为折叠 + if next_part_kind == ReasoningKind.TEXT: run.is_reasoning = False + run.is_reasoning_panel_open = False yield - # 加载能力分片结束事件 - case LoadCapabilityCallPart( - tool_kind=tool_kind, tool_name=tool_name - ): - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - part_kind=PartKind(tool_kind), - next_part_kind=PartKind(next_part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # 调用工具分片结束事件 - case ToolCallPart(part_kind=part_kind, tool_name=tool_name): - yield Event( - event_kind=EventKind(event_kind), - part_index=index, - part_kind=PartKind(part_kind), - next_part_kind=PartKind(next_part_kind), - tool_name=tool_name, - run_id=current_run_id, - ) - # ========== 函数工具调用事件 ========== - case FunctionToolCallEvent( - part=part, - ): - match part: - # 工具检索分片函数工具调用事件 - case ToolSearchCallPart(tool_call_id=tool_call_id): - index = tool_call_ids[tool_call_id] - run.reasonings[index].content = "检索完成" + case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part): + # 获取分片索引 + index = tool_call_ids[tool_call_id] + match run.reasonings[index].kind: + # 工具检索 + case ReasoningKind.TOOL_SEARCH: + run.reasonings[index].content = "正在检索" + yield + + # 能力加载 + case ReasoningKind.CAPABILITY_LOAD: + run.reasonings[index].content = ( + f"正在加载能力 {part.tool_name}" + ) + yield + + # 工具调用 + case ReasoningKind.TOOL_CALL: + run.reasonings[index].content = ( + f"正在调用工具 {part.tool_name}" + ) + yield # ========== 函数工具结果事件 ========== case FunctionToolResultEvent( + tool_call_id=tool_call_id, content=content, - part=part, ): - match part: - # 工具检索分片函数工具结果事件 - case ToolSearchCallPart(tool_call_id=tool_call_id): - index = tool_call_ids[tool_call_id] - run.reasonings[index].content += content or "" + index = tool_call_ids[tool_call_id] + match run.reasonings[index].kind: + # 工具检索 + case ReasoningKind.TOOL_SEARCH: + run.reasonings[index].content = ( + content if isinstance(content, str) else "" + ) # 暂仅考虑文本内容 + yield + + # 能力加载 + case ReasoningKind.CAPABILITY_LOAD: + run.reasonings[index].content = "已加载" + yield + + # 工具调用 + case ReasoningKind.TOOL_CALL: + run.reasonings[index].content = f"已调用" yield # ========== 智能体运行结果事件 ========== @@ -347,6 +351,15 @@ class ConversationState(rx.State): run_id=run_id, new_messages=result.new_messages(), ) - # 将运行状态设置为非正在运行 + # 将运行状态设置为运行完成 run.is_running = False yield + + @rx.event + def toggle_reasoning_panel(self, run_id: str) -> None: + """ + 展开/折叠指定运行唯一标识的推理面板 + """ + # 指定运行 + run = self.conversations[self.conversation_id].runs[run_id] + run.is_reasoning_panel_open = not run.is_reasoning_panel_open diff --git a/智能体/application/state/frame.py b/智能体/application/state/frame.py new file mode 100644 index 0000000..5459574 --- /dev/null +++ b/智能体/application/state/frame.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +框架状态 +""" +import reflex as rx +from application.models import SidebarIconNavButtonKind, SidebarIconNavButton + +# 初始化侧边栏图标导航按钮字典 +sidebar_icon_nav_buttons = { + SidebarIconNavButtonKind.CONVERSATION: {"icon": "message-square", "name": "对话"}, + SidebarIconNavButtonKind.KNOWLEDGE: {"icon": "book", "name": "知识库"}, +} + + +class FrameState(rx.State): + """ + 框架状态 + """ + + # 初始化侧边栏图标导航按钮字典 + SIDEBAR_ICON_NAV_BUTTONS = { + SidebarIconNavButtonKind.CONVERSATION: SidebarIconNavButton( + icon="message-circle-more", name="对话" + ), + SidebarIconNavButtonKind.KNOWLEDGE: SidebarIconNavButton( + icon="book", name="知识库" + ), + } # 侧边栏图标导航按钮字典 + + # 当前侧边栏图标导航按钮 + sidebar_icon_nav_button: SidebarIconNavButton = + + @rx.var + def get_sidebar_icon_nav_button(self) -> SidebarIconNavButton: + """获取当前侧边栏图标导航按钮""" + return self.SIDEBAR_ICON_NAV_BUTTONS[self.sidebar_icon_nav_button] + + @rx.event + def set_active_sidebar_tab(self, icon_id: str): + """切换侧边栏当前标签名称""" + self.sidebar_tab_name = icon_id