Python/智能体/application/components/conversation.py

296 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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")