diff --git a/agent/application/domain_models.py b/agent/application/domain_models.py index 6d101cc..1200d28 100644 --- a/agent/application/domain_models.py +++ b/agent/application/domain_models.py @@ -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 表示折叠" + ) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 0d6d7c5..a57b242 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -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, ) diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 27b4b46..f679624 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -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) diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 0887554..d994713 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -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: diff --git a/agent/database.db b/agent/database.db index b08185e..3076509 100644 Binary files a/agent/database.db and b/agent/database.db differ