diff --git a/agent/application/domain_models.py b/agent/application/domain_models.py index 1200d28..749bee6 100644 --- a/agent/application/domain_models.py +++ b/agent/application/domain_models.py @@ -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="创建时间", ) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index a57b242..dd612fd 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -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", ) diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index d994713..ae40dc3 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -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: diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 32d63cd..b1b6701 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -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), ) } diff --git a/agent/database.db b/agent/database.db index 3076509..aeb3431 100644 Binary files a/agent/database.db and b/agent/database.db differ