This commit is contained in:
liubiren 2026-07-28 01:18:38 +08:00
parent 998a0fc572
commit f9b91d45c1
5 changed files with 190 additions and 103 deletions

View File

@ -3,7 +3,7 @@
领域模型
"""
from datetime import datetime
from typing import Dict
from typing import Dict, List
from pydantic import BaseModel, Field
from pydantic_ai._uuid import uuid7
@ -57,3 +57,20 @@ class ConversationHistoryItem(BaseModel):
id: str = Field(..., description="会话唯一标识")
description: str = Field(..., description="会话描述")
created_at: str = Field(..., description="会话创建时间")
class DialogItem(BaseModel):
"""
对话项领域模型
"""
id: str = Field(..., description="对话唯一标识")
user_prompt: str = Field(..., description="用户提示词")
thoughts: List[Thought] = Field(default_factory=list, description="思考列表")
result_output: str = Field(default="", description="结果输出")
is_thinking: bool = Field(
default=False, description="正在思考True 表示正在思考False 表示未正在思考"
)
is_expanded: bool = Field(
default=False, description="思考折叠面板展开状态True 表示展开False 表示折叠"
)

View File

@ -7,33 +7,33 @@ from typing import Dict
from application.domain_models import (
Conversation,
Dialog,
DialogItem,
Thought,
ConversationHistoryItem,
)
from application.states import ConversationState, AuthState
def conversation_history_item(
conversation: ConversationHistoryItem,
def conversation_history_item_showing(
item: ConversationHistoryItem,
) -> rx.Component:
"""
会话历史项
会话历史项展示
:param conversation: 会话历史项
:return: Component
"""
# 高亮:若为当前会话或显示更多则高亮
highlight: bool = (
conversation.id == ConversationState.shown_more_conversation_id
) | (conversation.id == ConversationState.conversation_id)
highlight: bool = (item.id == ConversationState.shown_more_conversation_id) | (
item.id == ConversationState.conversation_id
)
return rx.list.item(
rx.vstack(
rx.hstack(
# 对话描述
rx.text(
conversation.description,
item.description,
flex=1,
height="22px",
line_height="22px",
@ -52,7 +52,7 @@ def conversation_history_item(
rx.spacer(),
# 创建时间
rx.text(
conversation.created_at,
item.created_at,
line_height="20px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-8)",
@ -64,7 +64,7 @@ def conversation_history_item(
margin_bottom="8px",
),
# 点击事件:切换会话
on_click=lambda: ConversationState.switch_conversation(conversation.id),
on_click=lambda: ConversationState.switch_conversation(item.id),
width="100%",
),
# 更多按钮:点击更多按钮显示气泡卡片,可删除会话
@ -97,7 +97,7 @@ def conversation_history_item(
"删除",
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation.id
item.id
),
display="flex",
align_items="center",
@ -130,7 +130,7 @@ def conversation_history_item(
overflow="visible",
),
on_open_change=lambda is_shown: ConversationState.set_shown_more_conversation_id(
conversation.id, is_shown
item.id, is_shown
),
open_delay=0,
),
@ -138,7 +138,7 @@ def conversation_history_item(
top="27px",
right="16px",
transform="translateY(-50%)",
z_index=99,
z_index="99",
min_width="14px",
cursor="pointer",
opacity=rx.cond(highlight, "1", "0"),
@ -172,11 +172,11 @@ def conversation_history_item(
)
def conversation_history(
def conversation_history_items_showing(
is_conversation_history_shown: bool,
) -> rx.Component:
"""
会话历史
会话历史列表展示
:param is_conversation_history_shown: 会话历史展示状态
:return: Component
"""
@ -193,11 +193,10 @@ def conversation_history(
align_items="center",
justify_content="space-between",
),
# 会话历史列表
rx.auto_scroll(
rx.foreach(
ConversationState.conversation_history_items,
conversation_history_item,
conversation_history_item_showing,
),
flex="1",
align_items="stretch",
@ -227,9 +226,9 @@ def conversation_history(
)
def greeting() -> rx.Component:
def greeting_showing() -> rx.Component:
"""
欢迎
欢迎展示
:return: Component
"""
return rx.vstack(
@ -332,20 +331,19 @@ def greeting() -> rx.Component:
)
def thought(thought):
def thought_showing(item: Thought):
"""
思考卡片
:param thought: 思考实例
思考展示
:param item: 思考实例
:return: Component
"""
return rx.vstack(
rx.match(
thought[1].type,
item.type,
(
"thinking",
rx.text(
thought[1].content,
item.content,
line_height="22px",
font_size="var(--prismui-font-size-2)",
color="var(--prismui-color-3)",
@ -358,16 +356,16 @@ def thought(thought):
)
def dialog(dialog: Dialog) -> rx.Component:
def dialog_item_showing(item: DialogItem) -> rx.Component:
"""
对话
:param dialog: 对话实例
对话项展示
:param item: 对话项实例
:return: Component
"""
# 推理状态
is_thinking = dialog.is_thinking
is_thinking = item.is_thinking
# 思考折叠面板展开状态
is_expanded = dialog.is_expanded
is_expanded = item.is_expanded
return rx.vstack(
rx.hstack(
@ -375,13 +373,12 @@ def dialog(dialog: Dialog) -> rx.Component:
rx.vstack(
# 用户提示词
rx.text(
dialog.user_prompt,
item.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",
color="var(--prismui-color-text)",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
@ -442,14 +439,14 @@ def dialog(dialog: Dialog) -> rx.Component:
),
cursor="pointer",
# 点击事件,展开/折叠思考折叠面板
on_click=lambda: ConversationState.toggle_collapse(dialog.id),
on_click=lambda: ConversationState.toggle_collapse(item.id),
),
rx.box(
rx.box(
rx.auto_scroll(
rx.foreach(
dialog.thoughts,
thought,
item.thoughts,
thought_showing,
),
width="100%",
margin_top="8px",
@ -470,20 +467,41 @@ def dialog(dialog: Dialog) -> rx.Component:
),
# 结果输出
rx.markdown(
dialog.result_output,
item.result_output,
component_map={
"p": lambda text: rx.text(
text,
margin_bottom="8px",
margin="6px 0",
line_height="1.5",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
),
"h1": lambda text: rx.text(
text,
margin="12px 0 6px",
line_height="1.5",
font_size="20px",
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_bottom="8px",
margin="10px 0 6px",
line_height="1.5",
font_size="18px",
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="8px 0 6px",
line_height="1.5",
font_size="16px",
font_weight="var(--prismui-font-weight-3)",
word_wrap="break-word",
word_break="break-all",
@ -495,54 +513,71 @@ def dialog(dialog: Dialog) -> rx.Component:
font_weight="var(--prismui-font-weight-3)",
),
"ul": lambda children: rx.vstack(
children, margin_bottom="8px", gap="4px"
children,
align_items="flex-start",
margin="6px 0",
style={"padding": "0 !important"},
),
"li": lambda text: rx.text(
text,
display="list-item",
list_style_type="disc",
list_style_position="outside",
margin="0 0 4px 16px",
line_height="1.5",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-1)",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
style={},
"ol": lambda children: rx.vstack(
children,
align_items="flex-start",
margin="6px 0",
style={"padding": "0 !important"},
),
"li": lambda text: rx.hstack(
rx.icon(
"dot",
flex_shrink="0",
width="21px",
height="21px",
color="var(--prismui-color-5)",
),
rx.text(
text,
line_height="1.5",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
),
align_items="flex-start",
margin_bottom="6px",
),
"hr": lambda _: rx.divider(
height="1px",
margin="12px 0",
background_color="var(--prismui-background-color-6)",
),
"hr": lambda _: rx.divider(margin="8px 0"),
},
width="100%",
margin_top="8px",
),
padding="0 16px",
width="100%",
key=dialog.id,
key=item.id,
)
def dialogs_showing() -> rx.Component:
def dialog_items_showing() -> rx.Component:
"""
对话列表展示
对话列表展示
:return: Component
"""
# 当前会话的对话列表
dialogs = ConversationState.dialogs
dialog_items = ConversationState.dialog_items
# 若对话列表为空则显示欢迎,否则显示对话列表
return rx.cond(
dialogs.length() == 0,
# 欢迎
greeting(),
dialog_items.length() == 0,
# 欢迎展示
greeting_showing(),
rx.vstack(
rx.auto_scroll(
rx.foreach(
dialogs,
dialog,
dialog_items,
dialog_item_showing,
),
width="100%",
padding="0 12px",
padding_bottom="180px",
),
flex="1",
width="100%",
@ -556,7 +591,7 @@ def dialogs_showing() -> rx.Component:
)
def create_conversation() -> rx.Component:
def conversation_creating() -> rx.Component:
"""
创建会话
:return: Component
@ -629,13 +664,15 @@ def create_conversation() -> rx.Component:
on_open_change=ConversationState.toggle_conversation_creating,
),
display="flex",
width="100%",
max_width="1200px",
height="39px",
padding="0 12px",
justify_content="flex-end",
align_items="center",
gap="4px",
padding="0 12px",
margin_bottom="8px",
width="100%",
max_width="1200px",
height="39px",
background="transparent",
)
@ -645,6 +682,8 @@ def user_prompt_sending() -> rx.Component:
:return: Component
"""
return rx.vstack(
# 创建对话
conversation_creating(),
rx.vstack(
rx.text_area(
# 值绑定用户提示词
@ -654,7 +693,8 @@ def user_prompt_sending() -> rx.Component:
placeholder="请输入您的问题按Enter换行",
vertical_align="middle",
width="100%",
height="64px",
min_height="64px",
max_height="224px",
padding="4px 0",
background_color="var(--prismui-background-color-1)",
border="none !important",
@ -665,6 +705,10 @@ def user_prompt_sending() -> rx.Component:
"border": "none !important",
"outline": "none !important",
"box-shadow": "none !important",
"field-sizing": "content !important",
"min-height": "64px",
"max-height": "224px",
"overflow-y": "auto",
},
},
),
@ -731,6 +775,10 @@ def user_prompt_sending() -> rx.Component:
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-10)",
),
position="absolute",
bottom="0",
left="0",
z_index="99",
display="flex",
flex_direction="column",
width="100%",
@ -756,12 +804,14 @@ def conversation_history_collapse_button(
height="16px",
color="var(--prismui-color-3)",
),
# 点击事件:切换会话历史折叠面板展开状态
on_click=ConversationState.toggle_conversation_history_shown,
position="absolute",
top="calc(50% - 20px)",
left=rx.cond(
is_conversation_history_shown, "calc(clamp(240px, 25%, 380px) - 8px)", "0"
),
z_index=99,
z_index="99",
width="16px",
height="40px",
background_color="var(--prismui-background-color-1)",
@ -773,8 +823,6 @@ def conversation_history_collapse_button(
box_shadow="var(--prismui-box-shadow-4)",
transition="all 0.3s ease-in-out",
cursor="pointer",
# 点击事件:切换会话历史折叠面板展开状态
on_click=ConversationState.toggle_conversation_history_shown,
)
@ -787,29 +835,26 @@ def conversation() -> rx.Component:
return rx.box(
rx.hstack(
# 会话历史
conversation_history(is_conversation_history_shown),
# 会话历史列表展示
conversation_history_items_showing(is_conversation_history_shown),
rx.box(
rx.vstack(
# 对话列表展示
dialogs_showing(),
# 创建对话
create_conversation(),
# 用户提示词发送
user_prompt_sending(),
display="flex",
flex_flow="column",
dialog_items_showing(),
width="100%",
height="100%",
gap="8px",
align_items="center",
),
# 用户提示词发送
user_prompt_sending(),
position="relative",
display="flex",
flex="1",
width="0",
height="100%",
background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)",
padding_bottom="120px",
background="var(--prismui-background-2)",
),
position="relative",
display="flex",
@ -821,11 +866,11 @@ def conversation() -> rx.Component:
),
# 会话历史折叠面板按钮
conversation_history_collapse_button(is_conversation_history_shown),
# 挂载事件:恢复会话状态
on_mount=AuthState.resume_conversation_state,
position="relative",
width="100%",
height="100%",
border_radius="var(--prismui-border-radius-4)",
overflow="hidden",
# 挂载事件:恢复当前用户会话状态
on_mount=AuthState.resume_conversation_state,
)

View File

@ -190,9 +190,9 @@ class AuthState(rx.State):
@rx.event
async def resume_conversation_state(self):
"""
恢复当前用户会话状态
恢复会话状态
"""
# 恢复当前用户会话状态
# 恢复会话状态
conversation_state = await self.get_state(ConversationState)
await conversation_state.resume(user_id=self.user_id)

View File

@ -26,7 +26,7 @@ import reflex as rx
from application.states.database import DatabaseState
from application.domain_models import (
Conversation,
Dialog,
DialogItem,
Thought,
ConversationHistoryItem,
)
@ -252,16 +252,29 @@ class ConversationState(rx.State):
return conversation.is_running
@rx.var
def dialogs(self) -> List[Dialog]:
def dialog_items(self) -> List[DialogItem]:
"""
当前会话的对话列表
:return: 当前会话的对话列表
当前会话的对话列表
:return: 当前会话的对话列表
"""
# 当前会话
conversation = self.conversations.get(self.conversation_id)
if not conversation:
return []
return list(conversation.dialogs.values())
items: List[DialogItem] = []
for dialog in conversation.dialogs.values():
items.append(
DialogItem(
id=dialog.id,
user_prompt=dialog.user_prompt,
thoughts=list(dialog.thoughts.values()),
result_output=dialog.result_output,
is_thinking=dialog.is_thinking,
is_expanded=dialog.is_expanded,
)
)
return items
@rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]:
@ -278,27 +291,26 @@ class ConversationState(rx.State):
# 当前会话
conversation = self.conversations[self.conversation_id]
# 将当前会话的运行状态设置为正在运行
# 将运行状态设置为正在运行
conversation.is_running = True
yield # 通知前端渲染
yield
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 获取消息历史
message_history = await database_state.get_message_history(
conversation_id=self.conversation_id
)
# 创建对话记录再添加对话实例
conversation.dialogs.update(
await database_state.create_dialog_record(
conversation_id=self.conversation_id, user_prompt=user_prompt
)
)
# 将最后一个会话作为当前会
# 将最后一个对话作为当前对
dialog = next(reversed(conversation.dialogs.values()))
# 获取消息历史
message_history = await database_state.get_message_history(
conversation_id=self.conversation_id
)
# 初始化工具调用唯一标识和片段索引映射字典
tool_call_ids: Dict[str, int] = {}
async with agent.run_stream_events(
@ -324,6 +336,7 @@ class ConversationState(rx.State):
dialog.thoughts[index] = Thought(
type="thinking", content=content
)
yield
# 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id):
@ -334,6 +347,7 @@ class ConversationState(rx.State):
type="tool_search",
content="正在生成检索关键词",
)
yield
# 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id):
@ -343,6 +357,7 @@ class ConversationState(rx.State):
type="capability_load",
content="正在生成加载参数",
)
yield
# 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id):
@ -352,10 +367,12 @@ class ConversationState(rx.State):
type="tool_call",
content="正在生成调用参数",
)
yield
# 文本分片开始事件
case TextPart(content=content):
dialog.result_output = content
yield
# ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta):
@ -365,12 +382,14 @@ class ConversationState(rx.State):
content_delta=content_delta,
):
dialog.thoughts[index].content += content_delta or ""
yield
# 文本分片增量事件
case TextPartDelta(
content_delta=content_delta,
):
dialog.result_output += content_delta
yield
# ========== 结束事件 ==========
case PartEndEvent(
@ -385,6 +404,7 @@ class ConversationState(rx.State):
if next_part_kind == "text":
dialog.is_thinking = False
dialog.is_expanded = False
yield
# ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
@ -394,18 +414,21 @@ class ConversationState(rx.State):
# 工具检索
case "tool_search":
dialog.thoughts[index].content = "正在检索"
yield
# 能力加载
case "capability_load":
dialog.thoughts[index].content = (
f"正在加载能力 {part.tool_name}"
)
yield
# 工具调用
case "tool_call":
dialog.thoughts[index].content = (
f"正在调用工具 {part.tool_name}"
)
yield
# ========== 函数工具结果事件 ==========
case FunctionToolResultEvent(
@ -419,14 +442,17 @@ class ConversationState(rx.State):
dialog.thoughts[index].content = (
content if isinstance(content, str) else ""
) # 暂仅考虑文本内容
yield
# 能力加载
case "capability_load":
dialog.thoughts[index].content = "已加载"
yield
# 工具调用
case "tool_call":
dialog.thoughts[index].content = f"已调用"
yield
# ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result):
@ -444,8 +470,7 @@ class ConversationState(rx.State):
)
# 将当前会话的运行状态设置为运行完成
conversation.is_running = False
yield
yield
@rx.event
def toggle_collapse(self, dialog_id: str) -> None:

Binary file not shown.