This commit is contained in:
liubiren 2026-07-28 20:08:03 +08:00
parent f9b91d45c1
commit 7b89324981
5 changed files with 111 additions and 133 deletions

View File

@ -46,31 +46,7 @@ class Conversation(BaseModel):
default=False, description="正在运行True 表示正在运行False 表示未正在运行" default=False, description="正在运行True 表示正在运行False 表示未正在运行"
) )
dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话列表") dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话列表")
created_at: datetime = Field(default_factory=datetime.now, description="创建时间") created_at: str = Field(
...,
description="创建时间",
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

@ -3,37 +3,37 @@
会话页面 会话页面
""" """
import reflex as rx import reflex as rx
from typing import Dict from typing import Tuple
from application.domain_models import ( from application.domain_models import (
Conversation, Conversation,
DialogItem, Dialog,
Thought, Thought,
ConversationHistoryItem,
) )
from application.states import ConversationState, AuthState from application.states import ConversationState, AuthState
def conversation_history_item_showing( def conversation_history_item_showing(
item: ConversationHistoryItem, item: Tuple[str, Conversation],
) -> rx.Component: ) -> rx.Component:
""" """
会话历史项展示 会话历史项展示
:param conversation: 会话历史项 :param item: 会话历史项
:return: Component :return: Component
""" """
conversation = item[1]
# 高亮:若为当前会话或显示更多则高亮 # 高亮:若为当前会话或显示更多则高亮
highlight: bool = (item.id == ConversationState.shown_more_conversation_id) | ( highlight: bool = (
item.id == ConversationState.conversation_id conversation.id == ConversationState.shown_more_conversation_id
) ) | (conversation.id == ConversationState.conversation_id)
return rx.list.item( return rx.list.item(
rx.vstack( rx.vstack(
rx.hstack( rx.hstack(
# 对话描述 # 对话描述
rx.text( rx.text(
item.description, conversation.description,
flex=1, flex=1,
height="22px", height="22px",
line_height="22px", line_height="22px",
@ -52,7 +52,7 @@ def conversation_history_item_showing(
rx.spacer(), rx.spacer(),
# 创建时间 # 创建时间
rx.text( rx.text(
item.created_at, conversation.created_at,
line_height="20px", line_height="20px",
font_size="var(--prismui-font-size-1)", font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-8)", color="var(--prismui-color-8)",
@ -64,7 +64,7 @@ def conversation_history_item_showing(
margin_bottom="8px", margin_bottom="8px",
), ),
# 点击事件:切换会话 # 点击事件:切换会话
on_click=lambda: ConversationState.switch_conversation(item.id), on_click=lambda: ConversationState.switch_conversation(conversation.id),
width="100%", width="100%",
), ),
# 更多按钮:点击更多按钮显示气泡卡片,可删除会话 # 更多按钮:点击更多按钮显示气泡卡片,可删除会话
@ -97,7 +97,7 @@ def conversation_history_item_showing(
"删除", "删除",
# 点击事件:删除对话 # 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation( on_click=lambda: ConversationState.delete_conversation(
item.id conversation.id
), ),
display="flex", display="flex",
align_items="center", align_items="center",
@ -130,7 +130,7 @@ def conversation_history_item_showing(
overflow="visible", overflow="visible",
), ),
on_open_change=lambda is_shown: ConversationState.set_shown_more_conversation_id( on_open_change=lambda is_shown: ConversationState.set_shown_more_conversation_id(
item.id, is_shown conversation.id, is_shown
), ),
open_delay=0, open_delay=0,
), ),
@ -195,7 +195,7 @@ def conversation_history_items_showing(
), ),
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
ConversationState.conversation_history_items, ConversationState.conversations,
conversation_history_item_showing, conversation_history_item_showing,
), ),
flex="1", flex="1",
@ -331,19 +331,21 @@ def greeting_showing() -> rx.Component:
) )
def thought_showing(item: Thought): def thought_showing(item: Tuple[int, Thought]):
""" """
思考展示 思考展示
:param item: 思考实例 :param item: 思考实例
:return: Component :return: Component
""" """
# 思考实例
thought = item[1]
return rx.vstack( return rx.vstack(
rx.match( rx.match(
item.type, thought.type,
( (
"thinking", "thinking",
rx.text( rx.text(
item.content, thought.content,
line_height="22px", line_height="22px",
font_size="var(--prismui-font-size-2)", font_size="var(--prismui-font-size-2)",
color="var(--prismui-color-3)", color="var(--prismui-color-3)",
@ -356,16 +358,18 @@ def thought_showing(item: Thought):
) )
def dialog_item_showing(item: DialogItem) -> rx.Component: def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
""" """
对话展示 对话展示
:param item: 对话实例 :param item: 对话实例
:return: Component :return: Component
""" """
# 对话实例
dialog = item[1]
# 推理状态 # 推理状态
is_thinking = item.is_thinking is_thinking = dialog.is_thinking
# 思考折叠面板展开状态 # 思考折叠面板展开状态
is_expanded = item.is_expanded is_expanded = dialog.is_expanded
return rx.vstack( return rx.vstack(
rx.hstack( rx.hstack(
@ -373,7 +377,7 @@ def dialog_item_showing(item: DialogItem) -> rx.Component:
rx.vstack( rx.vstack(
# 用户提示词 # 用户提示词
rx.text( rx.text(
item.user_prompt, dialog.user_prompt,
max_width="600px", max_width="600px",
padding="12px 16px", padding="12px 16px",
background_color="var(--prismui-background-color-3)", background_color="var(--prismui-background-color-3)",
@ -439,13 +443,13 @@ def dialog_item_showing(item: DialogItem) -> rx.Component:
), ),
cursor="pointer", cursor="pointer",
# 点击事件,展开/折叠思考折叠面板 # 点击事件,展开/折叠思考折叠面板
on_click=lambda: ConversationState.toggle_collapse(item.id), on_click=lambda: ConversationState.toggle_collapse(dialog.id),
), ),
rx.box( rx.box(
rx.box( rx.box(
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
item.thoughts, dialog.thoughts,
thought_showing, thought_showing,
), ),
width="100%", width="100%",
@ -467,7 +471,7 @@ def dialog_item_showing(item: DialogItem) -> rx.Component:
), ),
# 结果输出 # 结果输出
rx.markdown( rx.markdown(
item.result_output, dialog.result_output,
component_map={ component_map={
"p": lambda text: rx.text( "p": lambda text: rx.text(
text, text,
@ -545,14 +549,42 @@ def dialog_item_showing(item: DialogItem) -> rx.Component:
"hr": lambda _: rx.divider( "hr": lambda _: rx.divider(
height="1px", height="1px",
margin="12px 0", margin="12px 0",
background_color="var(--prismui-background-color-6)", background_color="transparent",
),
"table": lambda children: rx.el.table(
children,
style={
"margin": "12px 0",
"width": "100%",
"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 text: rx.el.th(
text,
style={
"padding": "10px 12px",
"background": "var(--prismui-background-color-3)",
"border": "1px solid var(--prismui-background-color-6)",
"text-align": "left",
},
),
"td": lambda text: rx.el.td(
text,
style={
"padding": "10px 12px",
"border": "1px solid var(--prismui-background-color-6)",
"vertical-align": "top",
},
), ),
}, },
width="100%", width="100%",
), ),
padding="0 16px", padding="0 16px",
width="100%", width="100%",
key=item.id, key=dialog.id,
) )
@ -563,21 +595,26 @@ def dialog_items_showing() -> rx.Component:
""" """
# 当前会话的对话列表 # 当前会话的对话列表
dialog_items = ConversationState.dialog_items dialogs = ConversationState.dialogs
# 若对话列表为空则显示欢迎,否则显示对话列表 # 若对话列表为空则显示欢迎,否则显示对话列表
return rx.cond( return rx.cond(
dialog_items.length() == 0, dialogs.length() == 0,
# 欢迎展示 # 欢迎展示
greeting_showing(), greeting_showing(),
rx.vstack( rx.vstack(
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
dialog_items, dialogs,
dialog_item_showing, dialog_showing,
), ),
width="100%", width="100%",
padding="0 12px", padding="0 12px",
padding_bottom="180px", padding_bottom="180px",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
), ),
flex="1", flex="1",
width="100%", width="100%",
@ -778,12 +815,14 @@ def user_prompt_sending() -> rx.Component:
position="absolute", position="absolute",
bottom="0", bottom="0",
left="0", left="0",
right="0",
z_index="99", z_index="99",
display="flex", display="flex",
flex_direction="column", flex_direction="column",
width="100%", width="100%",
max_width="1200px", max_width="1200px",
padding="0 12px 12px", padding="0 12px 12px",
margin_x="auto",
border="none", border="none",
) )

View File

@ -26,9 +26,8 @@ import reflex as rx
from application.states.database import DatabaseState from application.states.database import DatabaseState
from application.domain_models import ( from application.domain_models import (
Conversation, Conversation,
DialogItem, Dialog,
Thought, Thought,
ConversationHistoryItem,
) )
instructions: str = """ instructions: str = """
@ -117,34 +116,6 @@ class ConversationState(rx.State):
""" """
self.is_conversation_history_shown = not self.is_conversation_history_shown self.is_conversation_history_shown = not self.is_conversation_history_shown
@rx.var
def conversation_history_items(self) -> List[ConversationHistoryItem]:
"""
获取会话历史项列表用于前端渲染会话历史
:return: 会话历史项列表
"""
items: List[ConversationHistoryItem] = []
for conversation in reversed(
self.conversations.values()
): # 按照会话唯一标识倒序排序
# 格式化会话创建时间
match (datetime.now().date() - conversation.created_at.date()).days:
case 0:
created_at = f"{conversation.created_at.strftime('%H:%M')}"
case 1:
created_at = f"昨天 {conversation.created_at.strftime('%H:%M')}"
case _:
created_at = conversation.created_at.strftime("%Y-%m-%d %H:%M")
items.append(
ConversationHistoryItem(
id=conversation.id,
description=conversation.description,
created_at=created_at,
)
)
return items
@rx.event @rx.event
def set_shown_more_conversation_id( def set_shown_more_conversation_id(
self, conversation_id: str, is_shown: bool self, conversation_id: str, is_shown: bool
@ -252,29 +223,16 @@ class ConversationState(rx.State):
return conversation.is_running return conversation.is_running
@rx.var @rx.var
def dialog_items(self) -> List[DialogItem]: def dialogs(self) -> Dict[str, Dialog]:
""" """
当前会话的对话项列表 当前会话的对话字典
:return: 当前会话的对话项列表 :return: 当前会话的对话字典
""" """
# 当前会话 # 当前会话
conversation = self.conversations.get(self.conversation_id) conversation = self.conversations.get(self.conversation_id)
if not conversation: if not conversation:
return [] return {}
return conversation.dialogs
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 @rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]: async def handle_user_prompt(self) -> AsyncGenerator[None]:
@ -293,7 +251,6 @@ class ConversationState(rx.State):
conversation = self.conversations[self.conversation_id] conversation = self.conversations[self.conversation_id]
# 将运行状态设置为正在运行 # 将运行状态设置为正在运行
conversation.is_running = True conversation.is_running = True
yield
database_state = await self.get_state(DatabaseState) database_state = await self.get_state(DatabaseState)
@ -303,20 +260,20 @@ class ConversationState(rx.State):
conversation_id=self.conversation_id, user_prompt=user_prompt conversation_id=self.conversation_id, user_prompt=user_prompt
) )
) )
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
# 将最后一个对话作为当前对话 # 将最后一个对话作为当前对话
dialog = next(reversed(conversation.dialogs.values())) 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] = {} tool_call_ids: Dict[str, int] = {}
async with agent.run_stream_events( async with agent.run_stream_events(
conversation_id=self.conversation_id, conversation_id=self.conversation_id,
user_prompt=user_prompt, user_prompt=user_prompt,
message_history=message_history, message_history=await database_state.get_message_history(
conversation_id=self.conversation_id
),
) as events: ) as events:
async for event in events: async for event in events:
match event: match event:
@ -336,7 +293,6 @@ class ConversationState(rx.State):
dialog.thoughts[index] = Thought( dialog.thoughts[index] = Thought(
type="thinking", content=content type="thinking", content=content
) )
yield
# 工具检索分片开始事件 # 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id): case ToolSearchCallPart(tool_call_id=tool_call_id):
@ -347,7 +303,6 @@ class ConversationState(rx.State):
type="tool_search", type="tool_search",
content="正在生成检索关键词", content="正在生成检索关键词",
) )
yield
# 能力加载分片开始事件 # 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id): case LoadCapabilityCallPart(tool_call_id=tool_call_id):
@ -357,7 +312,6 @@ class ConversationState(rx.State):
type="capability_load", type="capability_load",
content="正在生成加载参数", content="正在生成加载参数",
) )
yield
# 工具调用分片开始事件 # 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id): case ToolCallPart(tool_call_id=tool_call_id):
@ -367,12 +321,10 @@ class ConversationState(rx.State):
type="tool_call", type="tool_call",
content="正在生成调用参数", content="正在生成调用参数",
) )
yield
# 文本分片开始事件 # 文本分片开始事件
case TextPart(content=content): case TextPart(content=content):
dialog.result_output = content dialog.result_output = content
yield
# ========== 增量事件 ========== # ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta): case PartDeltaEvent(index=index, delta=delta):
@ -382,14 +334,12 @@ class ConversationState(rx.State):
content_delta=content_delta, content_delta=content_delta,
): ):
dialog.thoughts[index].content += content_delta or "" dialog.thoughts[index].content += content_delta or ""
yield
# 文本分片增量事件 # 文本分片增量事件
case TextPartDelta( case TextPartDelta(
content_delta=content_delta, content_delta=content_delta,
): ):
dialog.result_output += content_delta dialog.result_output += content_delta
yield
# ========== 结束事件 ========== # ========== 结束事件 ==========
case PartEndEvent( case PartEndEvent(
@ -399,12 +349,11 @@ class ConversationState(rx.State):
): ):
match part: match part:
# 思考分片结束事件 # 思考分片结束事件
case ThinkingPart(part_kind=part_kind, content=content): case ThinkingPart(content=content):
# 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠 # 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠
if next_part_kind == "text": if next_part_kind == "text":
dialog.is_thinking = False dialog.is_thinking = False
dialog.is_expanded = False dialog.is_expanded = False
yield
# ========== 函数工具调用事件 ========== # ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part): case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
@ -414,21 +363,18 @@ class ConversationState(rx.State):
# 工具检索 # 工具检索
case "tool_search": case "tool_search":
dialog.thoughts[index].content = "正在检索" dialog.thoughts[index].content = "正在检索"
yield
# 能力加载 # 能力加载
case "capability_load": case "capability_load":
dialog.thoughts[index].content = ( dialog.thoughts[index].content = (
f"正在加载能力 {part.tool_name}" f"正在加载能力 {part.tool_name}"
) )
yield
# 工具调用 # 工具调用
case "tool_call": case "tool_call":
dialog.thoughts[index].content = ( dialog.thoughts[index].content = (
f"正在调用工具 {part.tool_name}" f"正在调用工具 {part.tool_name}"
) )
yield
# ========== 函数工具结果事件 ========== # ========== 函数工具结果事件 ==========
case FunctionToolResultEvent( case FunctionToolResultEvent(
@ -442,17 +388,14 @@ class ConversationState(rx.State):
dialog.thoughts[index].content = ( dialog.thoughts[index].content = (
content if isinstance(content, str) else "" content if isinstance(content, str) else ""
) # 暂仅考虑文本内容 ) # 暂仅考虑文本内容
yield
# 能力加载 # 能力加载
case "capability_load": case "capability_load":
dialog.thoughts[index].content = "已加载" dialog.thoughts[index].content = "已加载"
yield
# 工具调用 # 工具调用
case "tool_call": case "tool_call":
dialog.thoughts[index].content = f"已调用" dialog.thoughts[index].content = f"已调用"
yield
# ========== 智能体运行结果事件 ========== # ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result): case AgentRunResultEvent(result=result):
@ -470,6 +413,9 @@ class ConversationState(rx.State):
) )
# 将当前会话的运行状态设置为运行完成 # 将当前会话的运行状态设置为运行完成
conversation.is_running = False conversation.is_running = False
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield yield
@rx.event @rx.event

View File

@ -122,6 +122,22 @@ class ResultRecord(SQLModel, table=True):
new_messages: str = Field(description="新增消息") new_messages: str = Field(description="新增消息")
def format_at(at: datetime) -> str:
"""
格式化日期时间
:param at: 日期时间
:return: 格式化后的日期时间字符串
"""
match (datetime.now().date() - at.date()).days:
case 0:
formatted_at = f"{at.strftime('%H:%M')}"
case 1:
formatted_at = f"昨天 {at.strftime('%H:%M')}"
case _:
formatted_at = at.strftime("%Y-%m-%d %H:%M")
return formatted_at
# 思考领域模型类型适配器 # 思考领域模型类型适配器
ThoughtTypeAdapter = TypeAdapter(Dict[int, Thought]) ThoughtTypeAdapter = TypeAdapter(Dict[int, Thought])
@ -221,12 +237,13 @@ class DatabaseState(rx.State):
.order_by(ConversationRecord.id, DialogRecord.id) .order_by(ConversationRecord.id, DialogRecord.id)
) )
for conversation_record, dialog_record in result.all(): for conversation_record, dialog_record in result.all():
record = records.setdefault( record = records.setdefault(
conversation_record.id, conversation_record.id,
Conversation( Conversation(
id=conversation_record.id, id=conversation_record.id,
description=conversation_record.description, description=conversation_record.description,
created_at=conversation_record.created_at, created_at=format_at(conversation_record.created_at),
), ),
) )
if not dialog_record: if not dialog_record:
@ -263,7 +280,7 @@ class DatabaseState(rx.State):
record.id: Conversation( record.id: Conversation(
id=record.id, id=record.id,
description=record.description, description=record.description,
created_at=record.created_at, created_at=format_at(record.created_at),
) )
} }

Binary file not shown.