From 7d6d1d64a8866192eeb1b039b6076b85816c8c93 Mon Sep 17 00:00:00 2001 From: liubiren Date: Mon, 13 Jul 2026 20:11:49 +0800 Subject: [PATCH] 1 --- agent/alembic/versions/54a15eecb10a_.py | 46 - agent/alembic/versions/6cd382f48060_.py | 67 ++ agent/application/application.py | 5 +- agent/application/components/conversation.py | 622 ------------- agent/application/components/sidebar.py | 2 +- agent/application/models/__init__.py | 13 + .../{models.py => models/datatables.py} | 30 +- agent/application/models/memories.py | 48 + agent/application/pages/__init__.py | 7 +- agent/application/pages/conversation.py | 832 +++++++++++++++--- agent/application/pages/index.py | 11 +- agent/application/pages/library.py | 2 +- agent/application/states/__init__.py | 16 +- agent/application/states/auth.py | 2 +- agent/application/states/conversation.py | 30 +- .../states/conversation_history.py | 6 +- agent/application/states/database.py | 107 +-- agent/application/states/sidebar.py | 4 +- agent/reflex.lock/package.json | 44 +- 19 files changed, 980 insertions(+), 914 deletions(-) delete mode 100644 agent/alembic/versions/54a15eecb10a_.py create mode 100644 agent/alembic/versions/6cd382f48060_.py delete mode 100644 agent/application/components/conversation.py create mode 100644 agent/application/models/__init__.py rename agent/application/{models.py => models/datatables.py} (77%) create mode 100644 agent/application/models/memories.py diff --git a/agent/alembic/versions/54a15eecb10a_.py b/agent/alembic/versions/54a15eecb10a_.py deleted file mode 100644 index ecc0390..0000000 --- a/agent/alembic/versions/54a15eecb10a_.py +++ /dev/null @@ -1,46 +0,0 @@ -"""empty message - -Revision ID: 54a15eecb10a -Revises: -Create Date: 2026-07-10 14:17:15.523102 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - -# revision identifiers, used by Alembic. -revision: str = '54a15eecb10a' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('messagehistory', - sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('run_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('new_message', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - with op.batch_alter_table('messagehistory', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_messagehistory_conversation_id'), ['conversation_id'], unique=False) - batch_op.create_index(batch_op.f('ix_messagehistory_run_id'), ['run_id'], unique=False) - - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('messagehistory', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_messagehistory_run_id')) - batch_op.drop_index(batch_op.f('ix_messagehistory_conversation_id')) - - op.drop_table('messagehistory') - # ### end Alembic commands ### diff --git a/agent/alembic/versions/6cd382f48060_.py b/agent/alembic/versions/6cd382f48060_.py new file mode 100644 index 0000000..126ebd7 --- /dev/null +++ b/agent/alembic/versions/6cd382f48060_.py @@ -0,0 +1,67 @@ +"""empty message + +Revision ID: 6cd382f48060 +Revises: +Create Date: 2026-07-13 17:24:40.090382 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = '6cd382f48060' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('conversations', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_deleted', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('conversations', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_conversations_user_id'), ['user_id'], unique=False) + + op.create_table('dialogs', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('question', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('thought_nodes', sa.JSON(), nullable=False), + sa.Column('answer', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('dialogs', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_dialogs_conversation_id'), ['conversation_id'], unique=False) + + op.create_table('runresults', + sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('dialog_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('new_messages', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('conversation_id', 'dialog_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('runresults') + with op.batch_alter_table('dialogs', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_dialogs_conversation_id')) + + op.drop_table('dialogs') + with op.batch_alter_table('conversations', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_conversations_user_id')) + + op.drop_table('conversations') + # ### end Alembic commands ### diff --git a/agent/application/application.py b/agent/application/application.py index 76e644e..7d9e083 100644 --- a/agent/application/application.py +++ b/agent/application/application.py @@ -3,7 +3,7 @@ 应用主入口 """ import reflex as rx -from application.pages import index +from application.pages.index import render_index app = rx.App( style={ @@ -21,6 +21,7 @@ app = rx.App( "box_sizing": "border-box", }, "--prismui-background-color": "#ffffff", + "--mc-float-block-shadow": "rgba(213, 213, 213, 0.25)", "--devui-global-bg": "#f6f6f8", "--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)", "--mc-box-shadow": "rgba(25, 25, 25, .06)", @@ -46,4 +47,4 @@ app = rx.App( }, # 自定义主题颜色 ) # 此处变量名需使用 app ,具体原因目前尚不清楚 # 注册首页路由 -app.add_page(component=index, route="/") +app.add_page(component=render_index(), route="/") diff --git a/agent/application/components/conversation.py b/agent/application/components/conversation.py deleted file mode 100644 index dd8a873..0000000 --- a/agent/application/components/conversation.py +++ /dev/null @@ -1,622 +0,0 @@ -# -*- coding: utf-8 -*- -""" -渲染对话相关组件 -""" -import reflex as rx - -from application.models import Run, Reasoning, Conversation -from application.state.conversation import ConversationState -from application.state.create_conversation_modal import CreateConversationModalState - - -def render_conversation_history_item( - conversation_id: str, conversation: Conversation -) -> rx.Component: - """ - 渲染对话历史的对话卡片 - :param conversation_id: 对话唯一标识 - :param conversation: 对话实例 - :return: Component - """ - - def more_button( - conversation_id: str, - ) -> rx.Component: - """ - 渲染更多按钮 - :param conversation_id: 对话唯一标识 - :return: Component - """ - return rx.popover.root( - # 触发事件:点击更多按钮 - rx.popover.trigger( - rx.box( - rx.icon("ellipsis", size=14, color="var(--devui-text-weak)"), - cursor="pointer", # 鼠标光标显示为手指 - ) - ), - # 渲染气泡卡片 - rx.popover.content( - rx.vstack( - rx.box( - position="absolute", # 绝对定位 - width="8px", # 宽度 - height="8px", # 高度 - top="-12px", # 向上移动 - left="50%", # 向左移动 - transform="translateX(-50%) rotate(45deg)", # 旋转45度 - background_color="#ffffff", # 背景颜色 - box_shadow="-2px -2px 4px rgba(0,0,0,0.05)", # 阴影 - ), - rx.popover.close( - rx.box( - "删除", - width="100%", # 宽度 - height="24px", # 高度 - padding="4px", # 内边距 - border_radius="4px", # 圆角 - line_height="16px", # 行高 - font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", # 字体 - font_size="12px", # 字体大小 - color="#252b3a", # 字体颜色 - style={ - "_hover": { - "background_color": "#f2f2f3", # 鼠标悬停显示背景颜色 - } - }, - # 点击事件:删除对话 - on_click=lambda: ConversationState.delete_conversation( - conversation_id - ), - ) - ), - position="relative", # 相对定位 - width="100%", # 宽度 - ), - position="relative", # 相对定位 - align="center", # 水平居中对齐 - padding="8px", # 内边距 - border_radius="4px", # 圆角 - background_color="#ffffff", # 背景颜色 - box_shadow="0 2px 12px rgba(0,0,0,0.1)", # 阴影 - overflow="visible", # 溢出可见 - side="bottom", # 气泡卡片位于底部 - side_offset=9, # 偏移量 - ), - open_delay=0, - ) - - # 指定对话的激活状态 - is_actived = conversation_id == ConversationState.conversation_id - - return rx.box( - rx.hstack( - # 渲染对话描述 - rx.text( - conversation.description, - flex=1, - height="22px", - line_height="22px", - font_size="var(--devui-font-size)", - padding_right="4px", - overflow="hidden", - text_overflow="ellipsis", - white_space="nowrap", - ), - # 渲染更多按钮 - rx.box( - more_button(conversation_id=conversation_id), - min_width="14px", - cursor="pointer", # 鼠标光标显示为手指 - style={ - "opacity": rx.cond( - is_actived, "1", "0" - ), # 若已激活则不透明,否则透明 - "pointer_events": rx.cond( - is_actived, "auto", "none" - ), # 若已激活则可点击,否则不可点击 - "transition": "opacity 0.18s ease", - }, - ), - display="flex", # 弹性布局 - align_items="center", # 子元素垂直居中 - width="100%", # 宽度 - margin_bottom="8px", # 底部外边距 - ), - line_height="1.5", # 行高 - color="var(--devui-text-weak)", # 字体颜色 - cursor="pointer", # 鼠标悬停显示手指 - margin_bottom="8px", # 底部外边距 - width="100%", # 宽度 - padding="16px", # 内边距 - border_radius="8px", # 圆角 - style={ - "background": rx.cond( - is_actived, - "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", - "var(--devui-base-bg)", - ), - "box_shadow": rx.cond(is_actived, "2px 2px 8px #e9e9e9", "none"), - # 鼠标悬停时渲染背景颜色和阴影 - "&:hover": { - "background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", - "box_shadow": "2px 2px 8px #e9e9e9", - }, - # 鼠标悬停时强制显示右侧三点按钮,覆盖默认隐藏 - "&:hover > div > div:last-child": { - "opacity": "1 !important", - "pointer_events": "auto !important", - }, - }, - # 点击事件:将指定对话唯一标识设置为当前对话唯一标识 - on_click=lambda: ConversationState.switch_conversation(conversation_id), - ) - - -def render_reasoning_item_component(reasoning_id: int, reasoning: Reasoning): - """ - 渲染推理项组件 - :param reasoning_id: 推理唯一标识 - :param reasoning: 推理实例 - :return: Component - """ - return rx.hstack( - rx.vstack( - rx.box( - width="8px", - height="8px", - background_color=rx.color("blue", 7), - ), - rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)), - align_items="center", - spacing="0", - height="100%", - ), - rx.markdown(reasoning.content, color=rx.color("gray", 11), padding_y="4px"), - align_items="flex-start", - spacing="2", - width="100%", - key=reasoning_id, - ) - - -def render_conversation_box() -> rx.Component: - """ - 渲染对话框 - :return: Component - """ - - def item(run_id: str, run: Run) -> rx.Component: - """ - 对话项 - :param run: 对话实例 - :return: Component - """ - # 推理字典 - reasonings = run.reasonings - # 推理状态 - is_reasoning = run.is_reasoning - # 推理面板展开状态 - is_reasoning_panel_open = run.is_reasoning_panel_open - - return rx.vstack( - rx.hstack( - rx.spacer(), - rx.vstack( - # 用户问题 - rx.markdown( - run.user_prompt, - max_width="600px", - padding="12px 16px", - background_color="var(--devui-global-bg)", - border_radius="12px", - word_wrap="break-word", - word_break="break-all", - style={ - "& p.rt-Text": { - "padding": "0", - "margin": "0 !important", - }, - }, - ), - # 再次发送按钮 - rx.hstack( - rx.box( - rx.icon( - "rotate-ccw", - width="14px", - height="14px", - color="var(--devui-text-weak)", - ), - padding="4px", - border_radius="4px", - cursor="pointer", - ), - width="100%", - margin_top="8px", - ), - ), - # 用户身份标识 - rx.avatar( - src="/user_avatar.svg", - align_self="flex-start", - width="32px", - height="32px", - border_radius="100%", - border="none", - ), - width="100%", - margin_top="8px", - align_items="flex-start", - gap="4px", - font_size="var(--devui-font-size)", - ), - # 智能体回答 - rx.hstack( - # 智能体身份标识 - rx.avatar( - src="/logo.svg", - align_self="flex-start", - width="32px", - height="32px", - border_radius="100%", - border="none", - ), - rx.box( - rx.vstack( - # 标题栏 - rx.cond( - is_reasoning, - rx.hstack( - rx.icon("loader"), - rx.text("推理中..."), - rx.icon("chevron-down"), - ), - rx.hstack( - rx.icon("check"), - rx.text("已推理"), - rx.icon("chevron-up"), - ), - ), - - ) - ), - # 标题栏 - text_align="left", - width="100%", - margin_bottom="8px", - ), - # 渲染回复正文组件,若正在推理则不渲染,否则渲染回复正文组件 - rx.cond( - is_reasoning, - rx.fragment(), - rx.box( - rx.markdown( - run.assistant_content, - color=rx.color("gray", 12), # 文字颜色 - background_color=rx.color("gray", 2), # 背景颜色 - display="inline-block", # 布局模式:自适应文本宽度 - max_width="85%", # 最大宽度 - padding_x="1.25em", # 水平内边距 - padding_y="0.5em", # 垂直内边距 - margin_left="auto", # 左侧外边距自动调整 - margin_bottom="8px", # 底部外边距 - border_radius="12px", # 圆角 - ), - ), - ), - width="100%", - key=run_id, - ) - - # 运行字典 - runs = ConversationState.get_runs - - # 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 - return rx.cond( - runs.length() == 0, - rx.vstack( - rx.vstack( - # 品牌图标、名称和介绍 - rx.vstack( - rx.hstack( - rx.image( - src="/logo.png", - width="64px", - height="64px", - object_fit="contain", - ), - rx.box( - "棱镜球", - font_size="32px", - font_weight="700", - letter_spacing="1px", - ), - display="flex", - align_items="center", - gap="8px", - ), - rx.vstack( - rx.text( - "产品汪的棱镜球,精准捕捉各类业务需求", - width="100%", - text_align="center", - ), - rx.text( - "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", - width="100%", - text_align="center", - ), - width="100%", - line_height="1.5", - font_size="var(--devui-font-size)", - ), - display="flex", - flex_direction="column", - width="100%", - align_items="center", - gap="12px", - color="var(--devui-text)", - ), - # 猜你想问 - rx.vstack( - rx.hstack( - rx.text( - "猜你想问", - line_height="24px", - font_size="16px", - font_weight="700", - ), - display="flex", - width="100%", - justify_content="space-between", - align_items="center", - margin_bottom="16px", - ), - rx.hstack( - rx.box( - "需求梳理", - padding="10px 16px", - background_color="var(--devui-dividing-line)", - border_radius="var(--devui-border-radius-full)", - font_size="var(--devui-font-size)", - color="var(--devui-aide-text)", - cursor="pointer", - ), - display="flex", - flex_wrap="wrap", - align_items="center", - gap="12px", - ), - width="100%", - padding="24px", - background_color="var(--devui-base-bg)", - border_radius="24px", - ), - display="flex", - flex_direction="column", - width="100%", - min_height="0", - margin="auto 0", - align_items="center", - gap="24px", - color="var(--devui-text)", - ), - display="flex", - flex="1", - flex_direction="column", - width="100%", - max_width="1200px", - padding="0 12px", - justify_content="flex-start", - gap="24px", - overflow="auto", - style={ - "&::-webkit-scrollbar": { - "display": "none", - }, - }, - ), - # 遍历渲染对话项 - rx.vstack( - rx.auto_scroll( - rx.foreach( - runs, - lambda i, _: item(i[0], i[1]), - ), - width="100%", - padding="0 12px", - ), - flex="1", - width="100%", - max_width="1200px", - margin_x="auto", - padding_top="20px", - justify_content="flex-start", - overflow_x="hidden", - overflow_y="auto", - ), - ) - - -def render_create_conversation_button() -> rx.Component: - """ - 渲染新建对话按钮 - :return: Component - """ - return rx.hstack( - rx.el.style( - """ - .rt-TooltipArrow polygon { - fill: #ffffff !important; - } - .rt-TooltipText { - color: var(--devui-text) !important; - opacity: 1 !important; - } - """ - ), - rx.spacer(), # 占位符 - rx.dialog.root( - # 触发事件:点击图标 - rx.dialog.trigger( - rx.box( - rx.tooltip( - rx.icon( - "plus", - size=14, - color="var(--devui-text)", - style={ - "_hover": { - "color": "var(--devui-brand)", - } - }, - ), - content="新建对话", - background_color="#ffffff", - box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)", - color="#252b3a", - side="top", - side_offset=9, - ), - display="flex", - width="24px", - height="24px", - background_color="var(--devui-base-bg)", - box_shadow="0 1px 8px #1919190f", - border_radius="var(--devui-border-radius-full)", - justify_content="center", - align_items="center", - cursor="pointer", - ) - ), - rx.dialog.content( - rx.form( - rx.hstack( - rx.input( - name="description", - placeholder="请输入对话描述", - flex="auto", - min_width="20ch", - ), - rx.button("创建"), - spacing="2", - wrap="wrap", - width="100%", - ), - # 提交事件:新建对话 - on_submit=ConversationState.create_conversation, - ), - background_color=rx.color("mauve", 1), - ), - open=CreateConversationModalState.is_open, - on_open_change=CreateConversationModalState.toggle, - ), - display="flex", - width="100%", - max_width="1200px", - height="39px", - padding="0 12px", - justify_content="flex-end", - align_items="center", - gap="4px", - ) - - -def render_input_box() -> rx.Component: - """ - 渲染输入框 - """ - return rx.vstack( - # 渲染自定义输入组件 - rx.form( - rx.box( - rx.vstack( - rx.hstack( - rx.text_area( - name="user_prompt", - placeholder="请输入您的问题,按Enter换行", - vertical_align="middle", - width="100%", - height="64px", - padding="4px 0", - background_color="var(--devui-base-bg)", - font_size="var(--devui-font-size)", - color="var(--devui-text)", - style={ - "border": "none !important", - "outline": "none !important", - "boxShadow": "none !important", - }, - ), - width="100%", - padding="0 16px", - ), - rx.hstack( - rx.spacer(), # 占位符 - rx.button( - rx.icon( - "send", - margin_right="4px", - width="12px", - height="12px", - ), - rx.text("发送"), - position="relative", - display="inline-flex", - padding="0 12px", - background_color="var(--devui-primary)", - border="none", - border_radius="20px", - align_items="center", - justify_content="center", - white_space="nowrap", - inline_height="1.5", - font_size="var(--devui-font-size)", - color="var(--devui-light-text)", - overflow="hidden", - cursor="pointer", - type="submit", - loading=ConversationState.get_running_status, - disabled=ConversationState.get_running_status, - ), - width="100%", - justify_content="flex-end", - align_items="center", - height="32px", - padding="0 16px", - ), - ), - style={ - "* textarea::placeholder": { - "fontFamily": "var(--font-family)", - "fontSize": "var(--devui-font-size)", - "color": "var(--placeholder)", - "opacity": 1, - }, - }, - ), - display="flex", - flex_direction="column", - width="100%", - padding="12px 0", - background_color="var(--devui-base-bg)", - border="none", - border_radius="16px", - box_shadow="0 1px 8px 0 var(--mc-box-shadow)", - on_submit=ConversationState.run, - reset_on_submit=True, - ), - # 渲染底部文案 - rx.text( - "内容由大模型生成,无法确保准确性和完整性,仅供参考", - margin_top="8px", - text_align="center", - font_size="12px", - color="var(--devui-aide-text)", - ), - width="100%", - max_width="1200px", - padding="0 12px 12px", - align_items="center", - ) diff --git a/agent/application/components/sidebar.py b/agent/application/components/sidebar.py index 11a4e96..b44a8d6 100644 --- a/agent/application/components/sidebar.py +++ b/agent/application/components/sidebar.py @@ -85,7 +85,7 @@ def sidebar() -> rx.Component: line_height="20px", font_size="11px", font_weight="700", - color="var(--devui-color-text)", + color="var(--prismui-color-text)", ), display="flex", flex_direction="column", diff --git a/agent/application/models/__init__.py b/agent/application/models/__init__.py new file mode 100644 index 0000000..98689f6 --- /dev/null +++ b/agent/application/models/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from application.models.datatables import Conversations, Dialogs, RunResults +from application.models.memories import Conversation, Dialog, ThoughtNode + + +__all__ = [ + "Conversation", + "Conversations", + "Dialog", + "Dialogs", + "RunResults", + "ThoughtNode", +] diff --git a/agent/application/models.py b/agent/application/models/datatables.py similarity index 77% rename from agent/application/models.py rename to agent/application/models/datatables.py index 1d3da56..f5a3d7b 100644 --- a/agent/application/models.py +++ b/agent/application/models/datatables.py @@ -8,8 +8,8 @@ from pydantic_ai._uuid import uuid7 from sqlmodel import Field, JSON, SQLModel -# 会话数据表模型 -class Conversation(SQLModel, table=True): +# 会话数据表 +class Conversations(SQLModel, table=True): id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, @@ -21,8 +21,15 @@ class Conversation(SQLModel, table=True): created_at: datetime = Field(default_factory=datetime.now, description="创建时间") -# 对话数据表模型 -class Dialog(SQLModel, table=True): +# 思考节点数据表(仅用于定义) +class ThoughtNodes(SQLModel): + + kind: str = Field(description="思考节点类型") + content: str = Field(default="", description="思考节点内容") + + +# 对话数据表 +class Dialogs(SQLModel, table=True): id: str = Field( default_factory=lambda: str(uuid7()), primary_key=True, @@ -30,21 +37,14 @@ class Dialog(SQLModel, table=True): ) conversation_id: str = Field(..., index=True, description="会话唯一标识") question: str = Field(..., description="问题") - thought_nodes: list[ThoughtNode] = Field( - default_factory=list, sa_type=JSON, description="思考节点列表" + thought_nodes: dict[int, ThoughtNodes] = Field( + default_factory=dict, sa_type=JSON, description="思考节点字典" ) answer: str = Field(default="", description="回答") -# 思考节点数据表模型 -class ThoughtNode(SQLModel): - - kind: str = Field(description="思考节点类型") - content: str = Field(default="", description="思考节点内容") - - -# 运行结果数据表模型 -class RunResult(SQLModel, table=True): +# 运行结果数据表 +class RunResults(SQLModel, table=True): conversation_id: str = Field(primary_key=True, description="会话唯一标识") dialog_id: str = Field(primary_key=True, description="对话唯一标识") new_messages: str = Field(description="新增消息") diff --git a/agent/application/models/memories.py b/agent/application/models/memories.py new file mode 100644 index 0000000..628f670 --- /dev/null +++ b/agent/application/models/memories.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +内存模型 +""" +from datetime import datetime +from typing import Dict + +from pydantic import BaseModel, Field + + +class ThoughtNode(BaseModel): + """ + 思考节点内存模型 + """ + + kind: str + content: str + + +class Dialog(BaseModel): + """ + 对话内存模型 + """ + + question: str = Field(..., description="问题") + is_thinking: bool = Field( + default=False, description="思考状态,True 表示思考中,False 表示思考完成" + ) + is_collapse_expanded: bool = Field( + default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" + ) + thought_nodes: dict[int, ThoughtNode] = Field( + default_factory=dict, description="思考节点字典" + ) + answer: str = Field(default="", description="回答") + + +class Conversation(BaseModel): + """ + 会话内存模型 + """ + + description: str = Field(default="新会话", description="会话描述") + is_running: bool = Field( + default=False, description="运行状态,True 表示运行中,False 表示运行完成" + ) + dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话字典") + created_at: datetime = Field(default_factory=datetime.now, description="创建时间") diff --git a/agent/application/pages/__init__.py b/agent/application/pages/__init__.py index 6fa28b7..5b5299e 100644 --- a/agent/application/pages/__init__.py +++ b/agent/application/pages/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from application.pages.index import index -from application.pages.conversation import conversation -from application.pages.library import library +from application.pages.conversation import render_conversation +from application.pages.library import render_library -__all__ = ["index", "conversation", "library"] +__all__ = ["render_conversation", "render_library"] diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 1cd055d..96a2698 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -4,98 +4,737 @@ """ import reflex as rx -from application.components import ( - render_conversation_box, - render_conversation_history_item, - render_input_box, - render_create_conversation_button, +from application.models import Conversation, Dialog, ThoughtNode +from application.states import ( + ConversationHistoryState, + ConversationState, + CreateConversationState, ) -from application.states import ConversationState, ConversationHistoryState -def conversation() -> rx.Component: +def render_conversation_history_item( + conversation_id: str, conversation: Conversation +) -> rx.Component: """ - 会话页面:布局参考 MetaChat,左侧为会话历史,右侧为会话。其中,会话历史为折叠面板。 + 渲染会话历史条目 + :param conversation_id: 会话唯一标识 + :param conversation: 会话实例 + :return: Component """ - # 会话历史折叠面板打开状态 - is_open = ConversationHistoryState.is_open + + # 会话激活状态 + is_conversation_actived = conversation_id == ConversationState.conversation_id return rx.box( rx.hstack( - # 渲染对话历史折叠面板 - rx.box( - rx.vstack( - # 渲染标题 - rx.hstack( - rx.text( - "对话历史", - margin_bottom="8px", - font_size="var(--devui-font-size-lg)", - font_weight="700", - white_space="nowrap", - ), - display="flex", - align_items="center", - justify_content="space-between", - ), - # 渲染列表 - rx.auto_scroll( - rx.foreach( - ConversationState.conversation_history, - lambda item, _: render_conversation_history_item( - item[0], item[1] - ), - ), - flex="1", - margin_top="8px", - width="100%", - align_items="stretch", - overflow="auto", - style={ - "&::-webkit-scrollbar": { - "display": "none", - }, - }, - ), - display="flex", - flex_direction="column", - padding="12px", - height="100%", - background_color="#f9f9f9cc", - backdrop_filter="blur(50px)", # 背景模糊 - gap="12px", - color="var(--devui-text)", - ), - width=rx.cond(is_open, "25%", "0px"), - min_width=rx.cond(is_open, "240px", "0px"), - max_width=rx.cond(is_open, "380px", "0px"), - height="100%", + # 渲染对话描述 + rx.text( + conversation.description, + flex=1, + height="22px", + line_height="22px", + font_size="var(--devui-font-size)", + padding_right="4px", overflow="hidden", - transition="all 0.3s ease-in-out", + text_overflow="ellipsis", + white_space="nowrap", ), - # 渲染对话区域 + # 渲染更多按钮 + rx.box( + rx.popover.root( + # 触发事件:点击更多按钮 + rx.popover.trigger( + rx.box( + rx.icon( + "ellipsis", size=14, color="var(--devui-text-weak)" + ), + cursor="pointer", + ) + ), + # 渲染气泡卡片 + rx.popover.content( + rx.vstack( + rx.box( + position="absolute", + top="-12px", + left="50%", + transform="translateX(-50%) rotate(45deg)", + width="8px", + height="8px", + background_color="#ffffff", + box_shadow="-2px -2px 4px rgba(0,0,0,0.05)", + ), + rx.popover.close( + rx.box( + "删除", + width="100%", + height="24px", + padding="4px", + border_radius="4px", + line_height="16px", + font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", + font_size="12px", + color="#252b3a", + style={ + "_hover": { + "background_color": "#f2f2f3", + } + }, + # 点击事件:删除对话 + on_click=lambda: ConversationState.delete_conversation( + conversation_id + ), + ) + ), + position="relative", + width="100%", + ), + position="relative", + align="center", + padding="8px", + border_radius="4px", + background_color="#ffffff", + box_shadow="0 2px 12px rgba(0,0,0,0.1)", + overflow="visible", + side="bottom", + side_offset=9, + ), + open_delay=0, + ), + min_width="14px", + cursor="pointer", + style={ + "opacity": rx.cond( + is_conversation_actived, "1", "0" + ), # 若当前会话已激活则不透明,否则透明 + "pointer_events": rx.cond( + is_conversation_actived, "auto", "none" + ), # 若当前会话已激活则可点击,否则不可点击 + "transition": "opacity 0.18s ease", + }, + ), + display="flex", + align_items="center", + width="100%", + margin_bottom="8px", + ), + width="100%", + padding="16px", + margin_bottom="8px", + border_radius="8px", + line_height="1.5", + color="var(--devui-text-weak)", + cursor="pointer", + style={ + "background": rx.cond( + is_conversation_actived, + "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", + "var(--devui-base-bg)", + ), + "box_shadow": rx.cond( + is_conversation_actived, "2px 2px 8px #e9e9e9", "none" + ), + "&:hover": { + "background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", + "box_shadow": "2px 2px 8px #e9e9e9", + }, # 鼠标悬停渲染背景颜色和阴影 + "&:hover > div > div:last-child": { + "opacity": "1 !important", + "pointer_events": "auto !important", + }, # 鼠标悬停强制显示右侧三点按钮 + }, + # 点击事件:将指定对话唯一标识设置为当前对话唯一标识 + on_click=ConversationState.switch_conversation(conversation_id), + ) + + +def render_conversation_history_collapse(is_collapse_expanded: bool) -> rx.Component: + """ + 渲染对话历史折叠面板 + :param is_collapse_expanded: 对话历史折叠面板展开状态 + :return: Component + """ + return rx.box( + rx.vstack( + # 渲染标题 + rx.hstack( + rx.text( + "会话历史", + margin_bottom="8px", + font_size="var(--devui-font-size-lg)", + font_weight="700", + white_space="nowrap", + ), + display="flex", + align_items="center", + justify_content="space-between", + ), + # 渲染列表 + rx.auto_scroll( + rx.foreach( + ConversationState.conversation_history, + lambda item, _: render_conversation_history_item(item[0], item[1]), + ), + flex="1", + margin_top="8px", + width="100%", + align_items="stretch", + overflow="auto", + style={ + "&::-webkit-scrollbar": { + "display": "none", + }, + }, + ), + display="flex", + flex_direction="column", + height="100%", + padding="12px", + background_color="#f9f9f9cc", + backdrop_filter="blur(50px)", + gap="12px", + color="var(--prismui-color-text)", + ), + width=rx.cond(is_collapse_expanded, "25%", "0px"), + min_width=rx.cond(is_collapse_expanded, "240px", "0px"), + max_width=rx.cond(is_collapse_expanded, "380px", "0px"), + height="100%", + overflow="hidden", + transition="all 0.3s ease-in-out", + ) + + +def render_greeting() -> rx.Component: + """ + 渲染问候 + :return: Component + """ + return rx.vstack( + rx.vstack( + # 品牌图标、名称和介绍 + rx.vstack( + rx.hstack( + rx.image( + src="/logo.png", + width="64px", + height="64px", + object_fit="contain", + ), + rx.box( + "棱镜球", + font_size="32px", + font_weight="700", + letter_spacing="1px", + ), + display="flex", + align_items="center", + gap="8px", + ), + rx.vstack( + rx.text( + "产品汪的棱镜球,精准捕捉各类业务需求", + width="100%", + text_align="center", + ), + rx.text( + "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", + width="100%", + text_align="center", + ), + width="100%", + line_height="1.5", + font_size="var(--devui-font-size)", + ), + display="flex", + flex_direction="column", + width="100%", + align_items="center", + gap="12px", + color="var(--prismui-color-text)", + ), + # 猜你想问 + rx.vstack( + rx.hstack( + rx.text( + "猜你想问", + line_height="24px", + font_size="16px", + font_weight="700", + ), + display="flex", + width="100%", + justify_content="space-between", + align_items="center", + margin_bottom="16px", + ), + rx.hstack( + rx.box( + "需求梳理", + padding="10px 16px", + background_color="var(--devui-dividing-line)", + border_radius="var(--devui-border-radius-full)", + font_size="var(--devui-font-size)", + color="var(--devui-aide-text)", + cursor="pointer", + ), + display="flex", + flex_wrap="wrap", + align_items="center", + gap="12px", + ), + width="100%", + padding="24px", + background_color="var(--devui-base-bg)", + border_radius="24px", + ), + display="flex", + flex_direction="column", + width="100%", + min_height="0", + margin="auto 0", + align_items="center", + gap="24px", + color="var(--prismui-color-text)", + ), + display="flex", + flex="1", + flex_direction="column", + width="100%", + max_width="1200px", + padding="0 12px", + justify_content="flex-start", + gap="24px", + overflow="auto", + style={ + "&::-webkit-scrollbar": { + "display": "none", + }, + }, + ) + + +def render_reasoning_thought_node(thought_node_id: int, thought_node: ThoughtNode): + """ + 渲染思考节点 + :param thought_node_id: 思考节点唯一标识 + :param thought_node: 思考节点实例 + :return: Component + """ + return rx.hstack( + rx.vstack( + rx.box( + width="8px", + height="8px", + background_color=rx.color("blue", 7), + ), + rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)), + align_items="center", + spacing="0", + height="100%", + ), + rx.markdown(thought_node.content, color=rx.color("gray", 11), padding_y="4px"), + align_items="flex-start", + spacing="2", + width="100%", + key=thought_node_id, + ) + + +def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: + """ + 渲染对话历史条目 + :param run: 对话实例 + :return: Component + """ + # 推理状态 + is_thinking = dialog.is_thinking + # 思考节点 + thought_nodes = dialog.thought_nodes + + # 推理折叠面板展开状态 + is_reasoning_panel_open = dialog.is_collapse_expanded + + return rx.vstack( + # 渲染问题和再次发送按钮 + rx.hstack( + rx.spacer(), + rx.vstack( + # 渲染问题 + rx.markdown( + dialog.question, + max_width="600px", + padding="12px 16px", + background_color="var(--devui-global-bg)", + border_radius="12px", + word_wrap="break-word", + word_break="break-all", + color="var(--prismui-color-text)", + style={ + "& p.rt-Text": { + "padding": "0", + "margin": "0 !important", + }, + }, + ), + # 再次发送按钮 + rx.hstack( + rx.box( + rx.icon( + "rotate-ccw", + width="14px", + height="14px", + color="var(--devui-text-weak)", + ), + padding="4px", + border_radius="4px", + cursor="pointer", + ), + width="100%", + margin_top="8px", + ), + ), + width="100%", + margin_top="8px", + align_items="flex-start", + gap="4px", + font_size="var(--devui-font-size)", + ), + # 渲染智能体思考链面板和回答 + rx.vstack( + + rx.auto_scroll( + rx.foreach( + thought_nodes, + lambda i, _: render_reasoning_thought_node(i[0], i[1]), + ), + width="100%", + padding="0 12px", + ), + # 渲染回答 + rx.markdown( + dialog.answer, + display="inline-block", + max_width="85%", + padding_x="1.25em", + padding_y="0.5em", + margin_left="auto", + margin_bottom="8px", + background_color=rx.color("gray", 2), + border_radius="12px", + color="var(--prismui-color-text)", + ), + text_align="left", + width="100%", + margin_bottom="8px", + ), + width="100%", + key=dialog_id, + ) + + +def render_dialog_history() -> rx.Component: + """ + 渲染对话历史 + :return: Component + """ + + # 获取当前会话的对话历史 + dialog_history = ConversationState.dialog_history + + # 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 + return rx.cond( + dialog_history.length() == 0, + # 渲染问候 + render_greeting(), + # 遍历渲染对话项 + rx.vstack( + rx.auto_scroll( + rx.foreach( + dialog_history, + lambda i, _: render_dialog_history_item(i[0], i[1]), + ), + width="100%", + padding="0 12px", + ), + flex="1", + width="100%", + max_width="1200px", + margin_x="auto", + padding_top="20px", + justify_content="flex-start", + overflow_x="hidden", + overflow_y="auto", + ), + ) + + +def render_create_conversation_button() -> rx.Component: + """ + 渲染新建对话按钮 + :return: Component + """ + return rx.hstack( + rx.el.style( + """ + .rt-TooltipArrow polygon { + fill: #ffffff !important; + } + .rt-TooltipText { + color: var(--prismui-color-text) !important; + opacity: 1 !important; + } + """ + ), + rx.spacer(), + rx.dialog.root( + # 触发事件:点击图标 + rx.dialog.trigger( + rx.box( + rx.tooltip( + rx.icon( + "plus", + size=14, + color="var(--prismui-color-text)", + style={ + "_hover": { + "color": "var(--devui-brand)", + } + }, + ), + content="新建对话", + background_color="#ffffff", + box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)", + color="#252b3a", + side="top", + side_offset=9, + ), + display="flex", + width="24px", + height="24px", + background_color="var(--devui-base-bg)", + box_shadow="0 1px 8px #1919190f", + border_radius="var(--devui-border-radius-full)", + justify_content="center", + align_items="center", + cursor="pointer", + ) + ), + rx.dialog.content( + rx.form( + rx.hstack( + rx.input( + name="description", + placeholder="请输入对话描述", + flex="auto", + min_width="20ch", + ), + rx.button("创建"), + spacing="2", + wrap="wrap", + width="100%", + ), + # 提交事件:新建对话 + on_submit=ConversationState.create_conversation, + ), + background_color=rx.color("mauve", 1), + ), + open=CreateConversationState.is_modal_open, + on_open_change=CreateConversationState.toggle_modal, + ), + display="flex", + width="100%", + max_width="1200px", + height="39px", + padding="0 12px", + justify_content="flex-end", + align_items="center", + gap="4px", + ) + + +def render_input() -> rx.Component: + """ + 渲染输入框 + :return: Component + """ + return rx.vstack( + # 渲染自定义输入组件 + rx.form( rx.box( rx.vstack( - # 渲染对话框 - render_conversation_box(), - # 渲染新建对话按钮 - render_create_conversation_button(), - # 渲染输入框 - render_input_box(), - display="flex", - flex_flow="column", - width="100%", - height="100%", - gap="8px", - align_items="center", + rx.hstack( + rx.text_area( + name="question", + placeholder="请输入您的问题,按Enter换行", + vertical_align="middle", + width="100%", + height="64px", + padding="4px 0", + background_color="var(--devui-base-bg)", + font_size="var(--devui-font-size)", + color="var(--prismui-color-text)", + style={ + "border": "none !important", + "outline": "none !important", + "boxShadow": "none !important", + }, + ), + width="100%", + padding="0 16px", + ), + rx.hstack( + rx.spacer(), + rx.button( + rx.icon( + "send", + margin_right="4px", + width="12px", + height="12px", + ), + rx.text("发送"), + position="relative", + display="inline-flex", + padding="0 12px", + background_color="var(--devui-primary)", + border="none", + border_radius="20px", + align_items="center", + justify_content="center", + white_space="nowrap", + inline_height="1.5", + font_size="var(--devui-font-size)", + color="var(--devui-light-text)", + overflow="hidden", + cursor="pointer", + type="submit", + loading=ConversationState.running_status, + disabled=ConversationState.running_status, + ), + width="100%", + justify_content="flex-end", + align_items="center", + height="32px", + padding="0 16px", + ), ), - position="relative", - display="flex", - flex="1", - width="0", - height="100%", - background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)", + style={ + "* textarea::placeholder": { + "fontFamily": "var(--font-family)", + "fontSize": "var(--devui-font-size)", + "color": "var(--placeholder)", + "opacity": 1, + }, + }, ), + display="flex", + flex_direction="column", + width="100%", + padding="12px 0", + background_color="var(--devui-base-bg)", + border="none", + border_radius="16px", + box_shadow="0 1px 8px 0 var(--mc-box-shadow)", + on_submit=ConversationState.run, + reset_on_submit=True, + ), + # 渲染底部文案 + rx.text( + "内容由大模型生成,无法确保准确性和完整性,仅供参考", + margin_top="8px", + text_align="center", + font_size="12px", + color="var(--devui-aide-text)", + ), + width="100%", + max_width="1200px", + padding="0 12px 12px", + align_items="center", + ) + + +def render_conversation_workplace() -> rx.Component: + """ + 渲染会话工作区 + :return: Component + """ + return rx.box( + rx.vstack( + # 渲染对话历史 + render_dialog_history(), + # 渲染新建对话按钮 + render_create_conversation_button(), + # 渲染输入框 + render_input(), + display="flex", + flex_flow="column", + width="100%", + height="100%", + gap="8px", + align_items="center", + ), + position="relative", + display="flex", + flex="1", + width="0", + height="100%", + background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)", + ) + + +def render_conversation_history_collapse_button( + is_collapse_expanded: bool, +) -> rx.Component: + """ + 渲染会话历史折叠面板的打开/关闭按钮 + :param is_collapse_expanded: 会话历史折叠面板展开状态 + :return: Component + """ + return rx.button( + rx.cond( + is_collapse_expanded, + rx.icon( + "chevron-left", + width="16px", + height="16px", + color="var(--devui-icon-fill-weak)", + ), + rx.icon( + "chevron-right", + width="16px", + height="16px", + color="var(--devui-icon-fill-weak)", + ), + ), + position="absolute", + top="50%", + left=rx.cond(is_collapse_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"), + z_index=99, + width="16px", + height="40px", + background="var(--prismui-background-color)", + box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)", + border_radius=rx.cond(is_collapse_expanded, "6px", "0 6px 6px 0"), + transition="all 0.3s ease-in-out", + cursor="pointer", + # 点击事件:打开/关闭对话历史折叠面板 + on_click=ConversationHistoryState.toggle_collapse, + ) + + +def render_conversation() -> rx.Component: + """ + 渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框 + """ + # 会话历史折叠面板展开状态 + is_collapse_expanded = ConversationHistoryState.is_collapse_expanded + + return rx.box( + # 渲染对话历史折叠面板 + rx.hstack( + # 渲染对话历史折叠面板 + render_conversation_history_collapse(is_collapse_expanded), + # 渲染会话工作区 + render_conversation_workplace(), position="relative", display="flex", flex="1", @@ -104,40 +743,13 @@ def conversation() -> rx.Component: overflow="hidden", transition="all 0.3s ease-in-out", ), - # 渲染打开/关闭对话历史折叠面板按钮 - rx.button( - rx.cond( - is_open, - rx.icon( - "chevron-left", - width="16px", - height="16px", - color="var(--devui-icon-fill-weak)", - ), - rx.icon( - "chevron-right", - width="16px", - height="16px", - color="var(--devui-icon-fill-weak)", - ), - ), - position="absolute", - top="50%", - left=rx.cond(is_open, "calc(clamp(240px, 25%, 380px) - 8px)", "0"), - z_index=99, - width="16px", - height="40px", - background="var(--devui-base-bg)", - box_shadow="var(--mc-float-block-shadow)", - border_radius=rx.cond(is_open, "6px", "0 6px 6px 0"), - transition="all 0.3s ease-in-out", - cursor="pointer", - # 点击事件:打开/关闭对话历史折叠面板 - on_click=ConversationHistoryCollapseState.toggle, - ), + # 渲染对话历史折叠面板打开/关闭按钮 + render_conversation_history_collapse_button(is_collapse_expanded), position="relative", width="100%", height="100%", border_radius="12px", - overflow="hidden", # 溢出隐藏 + overflow="hidden", + # 挂载事件:初始化会话历史 + on_mount=ConversationState.on_mount, ) diff --git a/agent/application/pages/index.py b/agent/application/pages/index.py index 11adbc3..fb95435 100644 --- a/agent/application/pages/index.py +++ b/agent/application/pages/index.py @@ -5,11 +5,12 @@ import reflex as rx from application.components import sidebar -from application.pages import conversation, library +from application.pages import render_conversation, render_library +from application.pages.conversation import render_conversation from application.states import AuthState, SidebarState -def index() -> rx.Component: +def render_index() -> rx.Component: """ 首页 """ @@ -21,9 +22,9 @@ def index() -> rx.Component: rx.match( SidebarState.get_activated_button, # 渲染知识库页面 - ("library", library()), + ("library", render_library()), # 渲染会话页面 - conversation(), + render_conversation(), ), width="100%", height="100vh", @@ -32,5 +33,5 @@ def index() -> rx.Component: box_sizing="border-box", background="var(--mc-global-bg)", # 挂载事件:因测试需模拟已认证 - on_mount=AuthState.authenticate(), + on_mount=AuthState.authenticate, ) diff --git a/agent/application/pages/library.py b/agent/application/pages/library.py index 12f5f5e..4691ba3 100644 --- a/agent/application/pages/library.py +++ b/agent/application/pages/library.py @@ -5,7 +5,7 @@ import reflex as rx -def library() -> rx.Component: +def render_library() -> rx.Component: """ 知识库页面 """ diff --git a/agent/application/states/__init__.py b/agent/application/states/__init__.py index 358a795..01e9582 100644 --- a/agent/application/states/__init__.py +++ b/agent/application/states/__init__.py @@ -1,27 +1,17 @@ # -*- coding: utf-8 -*- from application.states.auth import AuthState from application.states.conversation import ConversationState -from application.states.create_conversation import CreateConversationState from application.states.conversation_history import ConversationHistoryState -from application.states.database import ( - DatabaseState, - Conversations, - ConversationMemory as Conversation, - DialogMemory as Dialog, - ThoughtNodeMemory as ThoughtNode, -) +from application.states.create_conversation import CreateConversationState +from application.states.database import DatabaseState from application.states.sidebar import SidebarState __all__ = [ "AuthState", - "ConversationState", - "Conversations", "ConversationHistoryState", + "ConversationState", "CreateConversationState", "DatabaseState", "SidebarState", - "Conversation", - "Dialog", - "ThoughtNode", ] diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 5ba25d7..0e31683 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -11,7 +11,7 @@ class AuthState(rx.State): """ # 当前登录的用户唯一标识(通过本地存储同步) - user_id = rx.LocalStorage("user_id", sync=True) + user_id: str = rx.LocalStorage("user_id", sync=True) @rx.event async def authenticate(self) -> None: diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 23977ca..f38467f 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ -智能体状态 +会话状态 """ from typing import Any, AsyncGenerator, Dict -from typing import Dict, List +from typing import Dict from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai.messages import ( @@ -24,15 +24,10 @@ from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.run import AgentRunResultEvent import reflex as rx -from application.states import ( - AuthState, - Conversation, - Conversations, - CreateConversationState, - DatabaseState, - Dialog, - ThoughtNode, -) +from application.models import Conversation, Dialog, ThoughtNode +from application.states.auth import AuthState +from application.states.create_conversation import CreateConversationState +from application.states.database import DatabaseState instructions: str = """ @@ -74,7 +69,7 @@ class ConversationState(rx.State): # 用户唯一标识 user_id: str = "" # 会话字典 - conversations: Conversations = {} + conversations: Dict[str, Conversation] = {} # 当前会话唯一标识 conversation_id: str = "" @@ -96,7 +91,6 @@ class ConversationState(rx.State): self.conversations = await database_state.get_conversations( user_id=self.user_id ) - # 若会话字典为空则新建会话 if not self.conversations: # 先在数据库新建会话,再在会话字典新建会话 @@ -109,7 +103,7 @@ class ConversationState(rx.State): self.conversation_id = next(reversed(self.conversations.keys())) @rx.var - def conversation_history(self) -> Conversations: + def conversation_history(self) -> Dict[str, Conversation]: """ 获取会话历史,用于前端渲染会话历史 :return: 会话历史 @@ -384,6 +378,12 @@ class ConversationState(rx.State): # ========== 智能体运行结果事件 ========== case AgentRunResultEvent(result=result): + # 完成对话 + await database_state.complete_dialog( + dialog_id=dialog_id, + thought_nodes=dialog.thought_nodes, + answer=dialog.answer, + ) # 保存新增消息 await database_state.save_new_messages( conversation_id=self.conversation_id, @@ -401,4 +401,4 @@ class ConversationState(rx.State): """ # 指定运行 dialog = self.conversations[self.conversation_id].dialogs[dialog_id] - dialog.is_collapse_open = not dialog.is_collapse_open + dialog.is_collapse_expanded = not dialog.is_collapse_expanded diff --git a/agent/application/states/conversation_history.py b/agent/application/states/conversation_history.py index ce39be6..7ecbaf6 100644 --- a/agent/application/states/conversation_history.py +++ b/agent/application/states/conversation_history.py @@ -10,8 +10,8 @@ class ConversationHistoryState(rx.State): 会话历史状态 """ - # 会话历史折叠面板打开状态,True表示打开,False表示关闭 - is_collapse_open: bool = False + # 会话历史折叠面板展开状态,True表示展开,False表示收起 + is_collapse_expanded: bool = False @rx.event def toggle_collapse(self) -> None: @@ -19,4 +19,4 @@ class ConversationHistoryState(rx.State): 打开/关闭会话历史折叠面板 :return: None """ - self.is_collapse_open = not self.is_collapse_open + self.is_collapse_expanded = not self.is_collapse_expanded diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 67c8dbb..43365b8 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -2,66 +2,27 @@ """ 数据库状态 """ - from datetime import datetime -from typing import Dict, List, cast, TypeAlias +from typing import Dict, List, cast -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import TypeAdapter from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter import reflex as rx from sqlalchemy import ColumnElement, desc from sqlmodel import select -from application.models import Conversation, Dialog, RunResult +from application.models import ( + Conversation, + Conversations, + Dialog, + Dialogs, + RunResults, + ThoughtNode, +) -class ConversationMemory(BaseModel): - """ - 会话内存模型 - """ - - description: str = Field(default="新会话", description="会话描述") - is_running: bool = Field( - default=False, description="运行状态,True 表示运行中,False 表示运行完成" - ) - dialogs: Dict[str, DialogMemory] = Field( - default_factory=dict, description="对话字典" - ) - created_at: datetime = Field(default_factory=datetime.now, description="创建时间") - - -class DialogMemory(BaseModel): - """ - 对话内存模型 - """ - - question: str = Field(..., description="问题") - is_thinking: bool = Field( - default=False, description="思考状态,True 表示思考中,False 表示思考完成" - ) - is_collapse_expanded: bool = Field( - default=False, description="思考折叠面板展开状态,True 表示展开,False 表示折叠" - ) - thought_nodes: list[ThoughtNodeMemory] = Field( - default_factory=list, description="思考节点列表" - ) - answer: str = Field(default="", description="回答") - - -class ThoughtNodeMemory(BaseModel): - """ - 思考节点内存模型 - """ - - kind: str - content: str - - -# 会话字典类型别名 -Conversations: TypeAlias = Dict[str, ConversationMemory] - # 思考节点内存模型类型适配器 -ThoughtNodeMemoryTypeAdapter = TypeAdapter(list[ThoughtNodeMemory]) +ThoughtNodeTypeAdapter = TypeAdapter(dict[int, ThoughtNode]) class DatabaseState(rx.State): @@ -69,33 +30,33 @@ class DatabaseState(rx.State): 数据库状态 """ - async def get_conversations(self, user_id: str) -> Conversations: + async def get_conversations(self, user_id: str) -> Dict[str, Conversation]: """ 获取会话字典 :param user_id: 用户唯一标识 :return: 会话字典 """ - conversations: Conversations = {} + conversations: Dict[str, Conversation] = {} async with rx.asession() as session: result = await session.exec( - select(Conversation, Dialog) + select(Conversations, Dialogs) .outerjoin( - Dialog, + Dialogs, cast( ColumnElement[bool], - Conversation.id == Dialog.conversation_id, + Conversations.id == Dialogs.conversation_id, ), ) .where( - Conversation.user_id == user_id, - Conversation.is_deleted == False, + Conversations.user_id == user_id, + Conversations.is_deleted == False, ) - .order_by(Conversation.id, Dialog.id) + .order_by(Conversations.id, Dialogs.id) ) for conversation, dialog in result.all(): conversation_memory = conversations.setdefault( conversation.id, - ConversationMemory( + Conversation( description=conversation.description, created_at=conversation.created_at, ), @@ -103,11 +64,11 @@ class DatabaseState(rx.State): if not dialog: continue - conversation_memory.dialogs[dialog.id] = DialogMemory( + conversation_memory.dialogs[dialog.id] = Dialog( question=dialog.question, - thought_nodes=ThoughtNodeMemoryTypeAdapter.validate_python( + thought_nodes=ThoughtNodeTypeAdapter.validate_python( dialog.thought_nodes - ), # 反序列化为思考节点内存实例列表 + ), # 反序列化为思考节点内存实例字典 answer=dialog.answer, ) return conversations @@ -122,7 +83,7 @@ class DatabaseState(rx.State): :return: 新建会话唯一标识、创建时间 """ async with rx.asession() as session: - conversation = Conversation(user_id=user_id, description=description) + conversation = Conversations(user_id=user_id, description=description) session.add(conversation) await session.commit() await session.refresh(conversation) @@ -135,7 +96,7 @@ class DatabaseState(rx.State): :return: None """ async with rx.asession() as session: - conversation = await session.get(Conversation, conversation_id) + conversation = await session.get(Conversations, conversation_id) if not conversation: return conversation.is_deleted = True @@ -149,7 +110,7 @@ class DatabaseState(rx.State): :return: 新建对话唯一标识 """ async with rx.asession() as session: - dialog = Dialog( + dialog = Dialogs( conversation_id=conversation_id, question=question, ) @@ -161,7 +122,7 @@ class DatabaseState(rx.State): async def complete_dialog( self, dialog_id: str, - thought_nodes: list[ThoughtNodeMemory], + thought_nodes: dict[int, ThoughtNode], answer: str, ) -> None: """ @@ -172,11 +133,11 @@ class DatabaseState(rx.State): :return: None """ async with rx.asession() as session: - dialog = await session.get(Dialog, dialog_id) + dialog = await session.get(Dialogs, dialog_id) if not dialog: return - dialog.thought_nodes = ThoughtNodeMemoryTypeAdapter.dump_python( - thought_nodes + dialog.thought_nodes = ThoughtNodeTypeAdapter.dump_python( + {k: v for k, v in thought_nodes.items()} ) dialog.answer = answer await session.commit() @@ -190,9 +151,9 @@ class DatabaseState(rx.State): message_history: List[ModelMessage] = [] async with rx.asession() as session: result = await session.exec( - select(RunResult) - .where(RunResult.conversation_id == conversation_id) - .order_by(desc(RunResult.dialog_id)) + select(RunResults) + .where(RunResults.conversation_id == conversation_id) + .order_by(desc(RunResults.dialog_id)) ) for record in result.all(): message_history.extend( @@ -215,7 +176,7 @@ class DatabaseState(rx.State): """ async with rx.asession() as session: session.add( - RunResult( + RunResults( conversation_id=conversation_id, dialog_id=dialog_id, new_messages=ModelMessagesTypeAdapter.dump_json( diff --git a/agent/application/states/sidebar.py b/agent/application/states/sidebar.py index 685619b..e0b3dce 100644 --- a/agent/application/states/sidebar.py +++ b/agent/application/states/sidebar.py @@ -4,7 +4,7 @@ """ import reflex as rx -from application.states import AuthState +from application.states.auth import AuthState class SidebarState(rx.State): @@ -24,7 +24,7 @@ class SidebarState(rx.State): return f"sidebar_{user_id}" # 侧边栏中激活的图标导航按钮 - activated_button = rx.LocalStorage(generate_storage_key, sync=True) + activated_button: str = rx.LocalStorage(generate_storage_key, sync=True) @rx.var def get_activated_button(self) -> str: diff --git a/agent/reflex.lock/package.json b/agent/reflex.lock/package.json index d15ac2e..8873239 100644 --- a/agent/reflex.lock/package.json +++ b/agent/reflex.lock/package.json @@ -1 +1,43 @@ -{"name": "reflex", "type": "module", "scripts": {"dev": "react-router dev --host", "export": "react-router build"}, "dependencies": {"@radix-ui/react-form": "0.1.8", "@radix-ui/themes": "3.3.0", "@react-router/node": "7.15.0", "isbot": "5.1.40", "lucide-react": "1.14.0", "react": "19.2.6", "react-dom": "19.2.6", "react-error-boundary": "6.1.1", "react-helmet": "6.1.0", "react-markdown": "10.1.0", "react-router": "7.15.0", "react-router-dom": "7.15.0", "react-syntax-highlighter": "16.1.1", "rehype-katex": "7.0.1", "rehype-raw": "7.0.0", "rehype-unwrap-images": "1.0.0", "remark-gfm": "4.0.1", "remark-math": "6.0.0", "socket.io-client": "4.8.3", "sonner": "2.0.7", "universal-cookie": "7.2.2"}, "devDependencies": {"@emotion/react": "11.14.0", "@react-router/dev": "7.15.0", "@react-router/fs-routes": "7.15.0", "autoprefixer": "10.5.0", "postcss": "8.5.14", "postcss-import": "16.1.1", "vite": "8.0.16"}, "overrides": {"cookie": "1.1.1"}} \ No newline at end of file +{ + "name": "reflex", + "type": "module", + "scripts": { + "dev": "react-router dev --host", + "export": "react-router build" + }, + "dependencies": { + "@radix-ui/react-form": "0.1.8", + "@radix-ui/themes": "3.3.0", + "@react-router/node": "7.15.0", + "isbot": "5.1.40", + "lucide-react": "1.14.0", + "react": "19.2.6", + "react-dom": "19.2.6", + "react-error-boundary": "6.1.1", + "react-helmet": "6.1.0", + "react-markdown": "10.1.0", + "react-router": "7.15.0", + "react-router-dom": "7.15.0", + "react-syntax-highlighter": "16.1.1", + "rehype-katex": "7.0.1", + "rehype-raw": "7.0.0", + "rehype-unwrap-images": "1.0.0", + "remark-gfm": "4.0.1", + "remark-math": "6.0.0", + "socket.io-client": "4.8.3", + "sonner": "2.0.7", + "universal-cookie": "7.2.2" + }, + "devDependencies": { + "@emotion/react": "11.14.0", + "@react-router/dev": "7.15.0", + "@react-router/fs-routes": "7.15.0", + "autoprefixer": "10.5.0", + "postcss": "8.5.14", + "postcss-import": "16.1.1", + "vite": "8.0.16" + }, + "overrides": { + "cookie": "1.1.1" + } +} \ No newline at end of file