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 表示未正在运行"
)
dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话列表")
created_at: datetime = Field(default_factory=datetime.now, 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 表示折叠"
created_at: str = Field(
...,
description="创建时间",
)

View File

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

View File

@ -26,9 +26,8 @@ import reflex as rx
from application.states.database import DatabaseState
from application.domain_models import (
Conversation,
DialogItem,
Dialog,
Thought,
ConversationHistoryItem,
)
instructions: str = """
@ -117,34 +116,6 @@ class ConversationState(rx.State):
"""
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
def set_shown_more_conversation_id(
self, conversation_id: str, is_shown: bool
@ -252,29 +223,16 @@ class ConversationState(rx.State):
return conversation.is_running
@rx.var
def dialog_items(self) -> List[DialogItem]:
def dialogs(self) -> Dict[str, Dialog]:
"""
当前会话的对话项列表
:return: 当前会话的对话项列表
当前会话的对话字典
:return: 当前会话的对话字典
"""
# 当前会话
conversation = self.conversations.get(self.conversation_id)
if not conversation:
return []
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
return {}
return conversation.dialogs
@rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]:
@ -293,7 +251,6 @@ class ConversationState(rx.State):
conversation = self.conversations[self.conversation_id]
# 将运行状态设置为正在运行
conversation.is_running = True
yield
database_state = await self.get_state(DatabaseState)
@ -303,20 +260,20 @@ class ConversationState(rx.State):
conversation_id=self.conversation_id, user_prompt=user_prompt
)
)
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
# 将最后一个对话作为当前对话
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(
conversation_id=self.conversation_id,
user_prompt=user_prompt,
message_history=message_history,
message_history=await database_state.get_message_history(
conversation_id=self.conversation_id
),
) as events:
async for event in events:
match event:
@ -336,7 +293,6 @@ class ConversationState(rx.State):
dialog.thoughts[index] = Thought(
type="thinking", content=content
)
yield
# 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id):
@ -347,7 +303,6 @@ class ConversationState(rx.State):
type="tool_search",
content="正在生成检索关键词",
)
yield
# 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id):
@ -357,7 +312,6 @@ class ConversationState(rx.State):
type="capability_load",
content="正在生成加载参数",
)
yield
# 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id):
@ -367,12 +321,10 @@ class ConversationState(rx.State):
type="tool_call",
content="正在生成调用参数",
)
yield
# 文本分片开始事件
case TextPart(content=content):
dialog.result_output = content
yield
# ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta):
@ -382,14 +334,12 @@ 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(
@ -399,12 +349,11 @@ class ConversationState(rx.State):
):
match part:
# 思考分片结束事件
case ThinkingPart(part_kind=part_kind, content=content):
case ThinkingPart(content=content):
# 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠
if next_part_kind == "text":
dialog.is_thinking = False
dialog.is_expanded = False
yield
# ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
@ -414,21 +363,18 @@ 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(
@ -442,17 +388,14 @@ 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):
@ -470,7 +413,10 @@ class ConversationState(rx.State):
)
# 将当前会话的运行状态设置为运行完成
conversation.is_running = False
yield
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
@rx.event
def toggle_collapse(self, dialog_id: str) -> None:

View File

@ -122,6 +122,22 @@ class ResultRecord(SQLModel, table=True):
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])
@ -221,12 +237,13 @@ class DatabaseState(rx.State):
.order_by(ConversationRecord.id, DialogRecord.id)
)
for conversation_record, dialog_record in result.all():
record = records.setdefault(
conversation_record.id,
Conversation(
id=conversation_record.id,
description=conversation_record.description,
created_at=conversation_record.created_at,
created_at=format_at(conversation_record.created_at),
),
)
if not dialog_record:
@ -263,7 +280,7 @@ class DatabaseState(rx.State):
record.id: Conversation(
id=record.id,
description=record.description,
created_at=record.created_at,
created_at=format_at(record.created_at),
)
}

Binary file not shown.