This commit is contained in:
liubiren 2026-07-06 20:13:37 +08:00
parent 1789f8a0a3
commit a6b2bcf81c
7 changed files with 661 additions and 439 deletions

View File

@ -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")

View File

@ -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")

View File

@ -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),
)

View File

@ -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),
)

View File

@ -12,6 +12,20 @@ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
from sqlmodel import Field as SqlField, SQLModel 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 初始化数据库表 # 重新初始化:先手动删除 alembic 相关配置和文件夹,再使用 reflex db init 初始化数据库表
class MessageHistory(SQLModel, table=True): class MessageHistory(SQLModel, table=True):
@ -44,37 +58,12 @@ class MessageHistory(SQLModel, table=True):
) )
class EventKind(StrEnum): class ReasoningKind(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):
"""分片类型"""
THINKING = "thinking" THINKING = "thinking"
TOOL_SEARCH = "tool-search" TOOL_SEARCH = "tool-search"
CAPABILITY_LOAD = "capability-load" CAPABILITY_LOAD = "load-capability"
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"
TOOL_CALL = "tool-call" TOOL_CALL = "tool-call"
TEXT = "text" TEXT = "text"
TOOL_RETURN = "tool-return" TOOL_RETURN = "tool-return"
@ -85,7 +74,7 @@ class Kind(StrEnum):
class Reasoning(BaseModel): class Reasoning(BaseModel):
"""推理类""" """推理类"""
kind: Kind = Field(..., description="推理") kind: ReasoningKind = Field(..., description="推理")
content: str = Field(default="", description="推理内容") content: str = Field(default="", description="推理内容")
@ -98,12 +87,16 @@ class Run(BaseModel):
) )
is_reasoning: bool = Field( is_reasoning: bool = Field(
default=False, 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="回复正文") assistant_content: str = Field(default="", description="回复正文")
is_running: bool = Field( is_running: bool = Field(
default=False, default=False,
description="运行状态True 表示正在运行False 表示非正在运行", description="运行状态True 表示正在运行False 表示运行完成",
) )

View File

@ -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 import Agent, ThinkingPartDelta
from pydantic_ai._uuid import uuid7 from pydantic_ai._uuid import uuid7
from pydantic_ai.capabilities import AgentCapability
from pydantic_ai.messages import ( from pydantic_ai.messages import (
FunctionToolCallEvent, FunctionToolCallEvent,
FunctionToolResultEvent, FunctionToolResultEvent,
LoadCapabilityCallPart, LoadCapabilityCallPart,
ModelMessage,
PartDeltaEvent, PartDeltaEvent,
PartEndEvent, PartEndEvent,
PartStartEvent, PartStartEvent,
@ -21,12 +19,12 @@ from pydantic_ai.messages import (
ToolCallPart, ToolCallPart,
ToolSearchCallPart, ToolSearchCallPart,
) )
from pydantic_ai.run import AgentRunResultEvent
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.run import AgentRunResultEvent
import reflex as rx 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.create_conversation_modal import CreateConversationModalState
from application.state.database import DatabaseState from application.state.database import DatabaseState
@ -156,10 +154,10 @@ class ConversationState(rx.State):
return conversation.runs if conversation else {} return conversation.runs if conversation else {}
@rx.var @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) conversation = self.conversations.get(self.conversation_id)
@ -221,12 +219,13 @@ class ConversationState(rx.State):
match part: match part:
# 思考分片开始事件 # 思考分片开始事件
case ThinkingPart(content=content): case ThinkingPart(content=content):
# 若上一分片种类为空则将推理状态设置为正在推理 # 若上一分片种类为空则将推理状态设置为正在推理、推理面板展开状态设置为展开
if not previous_part_kind: if not previous_part_kind:
run.is_reasoning = True run.is_reasoning = True
run.is_reasoning_panel_open = True
run.reasonings[index] = Reasoning( run.reasonings[index] = Reasoning(
kind=Kind.THINKING, content=content kind=ReasoningKind.THINKING, content=content
) )
yield yield
@ -236,7 +235,7 @@ class ConversationState(rx.State):
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
run.reasonings[index] = Reasoning( run.reasonings[index] = Reasoning(
kind=Kind.TOOL_SEARCH, content="正在检索" kind=ReasoningKind.TOOL_SEARCH, content="正在生成检索关键词"
) )
yield yield
@ -245,7 +244,8 @@ class ConversationState(rx.State):
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
run.reasonings[index] = Reasoning( run.reasonings[index] = Reasoning(
kind=Kind.LOAD_CAPABILITY, content="正在加载能力" kind=ReasoningKind.CAPABILITY_LOAD,
content="正在生成加载参数",
) )
yield yield
@ -254,7 +254,7 @@ class ConversationState(rx.State):
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
run.reasonings[index] = Reasoning( run.reasonings[index] = Reasoning(
kind=Kind.TOOL_CALL, content="正在调用工具" kind=ReasoningKind.TOOL_CALL, content="正在生成调用参数"
) )
yield yield
@ -289,54 +289,58 @@ class ConversationState(rx.State):
match part: match part:
# 思考分片结束事件 # 思考分片结束事件
case ThinkingPart(part_kind=part_kind, content=content): 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 = False
run.is_reasoning_panel_open = False
yield 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( case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
part=part, # 获取分片索引
): index = tool_call_ids[tool_call_id]
match part: match run.reasonings[index].kind:
# 工具检索分片函数工具调用事件 # 工具检索
case ToolSearchCallPart(tool_call_id=tool_call_id): case ReasoningKind.TOOL_SEARCH:
index = tool_call_ids[tool_call_id] run.reasonings[index].content = "正在检索"
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( case FunctionToolResultEvent(
tool_call_id=tool_call_id,
content=content, content=content,
part=part,
): ):
match part: index = tool_call_ids[tool_call_id]
# 工具检索分片函数工具结果事件 match run.reasonings[index].kind:
case ToolSearchCallPart(tool_call_id=tool_call_id): # 工具检索
index = tool_call_ids[tool_call_id] case ReasoningKind.TOOL_SEARCH:
run.reasonings[index].content += content or "" 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 yield
# ========== 智能体运行结果事件 ========== # ========== 智能体运行结果事件 ==========
@ -347,6 +351,15 @@ class ConversationState(rx.State):
run_id=run_id, run_id=run_id,
new_messages=result.new_messages(), new_messages=result.new_messages(),
) )
# 将运行状态设置为非正在运行 # 将运行状态设置为运行完成
run.is_running = False run.is_running = False
yield 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

View File

@ -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