From 50e3479ab54ac5862bd0b34294c08976edc4f93a Mon Sep 17 00:00:00 2001 From: liubiren Date: Sun, 26 Jul 2026 23:20:36 +0800 Subject: [PATCH] 1 --- agent/alembic.ini | 149 ------------ agent/alembic/README | 1 - agent/alembic/env.py | 78 ------ agent/alembic/script.py.mako | 28 --- agent/alembic/versions/1595751865c1_.py | 67 ----- agent/alembic/versions/32268fce88c5_.py | 36 --- agent/alembic/versions/51f049b860bb_.py | 67 ----- agent/application/application.py | 4 +- agent/application/domain_models.py | 9 +- agent/application/pages/__init__.py | 6 +- agent/application/pages/conversation.py | 220 ++++++++--------- agent/application/pages/index.py | 73 +++--- .../pages/{library.py => knowledge_base.py} | 2 +- agent/application/states/auth.py | 14 +- agent/application/states/conversation.py | 229 +++++++++--------- agent/application/states/database.py | 67 ++--- .../{library.svg => knowledge_base.svg} | 0 17 files changed, 298 insertions(+), 752 deletions(-) delete mode 100644 agent/alembic.ini delete mode 100644 agent/alembic/README delete mode 100644 agent/alembic/env.py delete mode 100644 agent/alembic/script.py.mako delete mode 100644 agent/alembic/versions/1595751865c1_.py delete mode 100644 agent/alembic/versions/32268fce88c5_.py delete mode 100644 agent/alembic/versions/51f049b860bb_.py rename agent/application/pages/{library.py => knowledge_base.py} (77%) rename agent/assets/{library.svg => knowledge_base.svg} (100%) diff --git a/agent/alembic.ini b/agent/alembic.ini deleted file mode 100644 index 807ded2..0000000 --- a/agent/alembic.ini +++ /dev/null @@ -1,149 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts. -# this is typically a path given in POSIX (e.g. forward slashes) -# format, relative to the token %(here)s which refers to the location of this -# ini file -script_location = %(here)s/alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s -# Or organize into date-based subdirectories (requires recursive_version_locations = true) -# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. for multiple paths, the path separator -# is defined by "path_separator" below. -prepend_sys_path = . - - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the tzdata library which can be installed by adding -# `alembic[tz]` to the pip requirements. -# string value is passed to ZoneInfo() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to /versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "path_separator" -# below. -# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions - -# path_separator; This indicates what character is used to split lists of file -# paths, including version_locations and prepend_sys_path within configparser -# files such as alembic.ini. -# The default rendered in new alembic.ini files is "os", which uses os.pathsep -# to provide os-dependent path splitting. -# -# Note that in order to support legacy alembic.ini files, this default does NOT -# take place if path_separator is not present in alembic.ini. If this -# option is omitted entirely, fallback logic is as follows: -# -# 1. Parsing of the version_locations option falls back to using the legacy -# "version_path_separator" key, which if absent then falls back to the legacy -# behavior of splitting on spaces and/or commas. -# 2. Parsing of the prepend_sys_path option falls back to the legacy -# behavior of splitting on spaces, commas, or colons. -# -# Valid values for path_separator are: -# -# path_separator = : -# path_separator = ; -# path_separator = space -# path_separator = newline -# -# Use os.pathsep. Default configuration used for new projects. -path_separator = os - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# database URL. This is consumed by the user-maintained env.py script only. -# other means of configuring database URLs may be customized within the env.py -# file. -sqlalchemy.url = driver://user:pass@localhost/dbname - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module -# hooks = ruff -# ruff.type = module -# ruff.module = ruff -# ruff.options = check --fix REVISION_SCRIPT_FILENAME - -# Alternatively, use the exec runner to execute a binary found on your PATH -# hooks = ruff -# ruff.type = exec -# ruff.executable = ruff -# ruff.options = check --fix REVISION_SCRIPT_FILENAME - -# Logging configuration. This is also consumed by the user-maintained -# env.py script only. -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARNING -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARNING -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/agent/alembic/README b/agent/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/agent/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/agent/alembic/env.py b/agent/alembic/env.py deleted file mode 100644 index 36112a3..0000000 --- a/agent/alembic/env.py +++ /dev/null @@ -1,78 +0,0 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = None - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/agent/alembic/script.py.mako b/agent/alembic/script.py.mako deleted file mode 100644 index 1101630..0000000 --- a/agent/alembic/script.py.mako +++ /dev/null @@ -1,28 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - """Upgrade schema.""" - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - """Downgrade schema.""" - ${downgrades if downgrades else "pass"} diff --git a/agent/alembic/versions/1595751865c1_.py b/agent/alembic/versions/1595751865c1_.py deleted file mode 100644 index 041533a..0000000 --- a/agent/alembic/versions/1595751865c1_.py +++ /dev/null @@ -1,67 +0,0 @@ -"""empty message - -Revision ID: 1595751865c1 -Revises: 51f049b860bb -Create Date: 2026-07-17 16:28:58.025029 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - -# revision identifiers, used by Alembic. -revision: str = '1595751865c1' -down_revision: Union[str, Sequence[str], None] = '51f049b860bb' -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('captchas', - sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('captcha', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('is_valid', sa.Boolean(), nullable=False), - sa.Column('is_unused', sa.Boolean(), nullable=False), - sa.Column('expired_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('email', 'expired_at') - ) - with op.batch_alter_table('captchas', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_captchas_captcha'), ['captcha'], unique=False) - batch_op.create_index(batch_op.f('ix_captchas_is_unused'), ['is_unused'], unique=False) - batch_op.create_index(batch_op.f('ix_captchas_is_valid'), ['is_valid'], unique=False) - - op.create_table('users', - sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_users_email'), ['email'], unique=False) - - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_conversations_is_deleted'), ['is_deleted'], unique=False) - - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_conversations_is_deleted')) - - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_users_email')) - - op.drop_table('users') - with op.batch_alter_table('captchas', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_captchas_is_valid')) - batch_op.drop_index(batch_op.f('ix_captchas_is_unused')) - batch_op.drop_index(batch_op.f('ix_captchas_captcha')) - - op.drop_table('captchas') - # ### end Alembic commands ### diff --git a/agent/alembic/versions/32268fce88c5_.py b/agent/alembic/versions/32268fce88c5_.py deleted file mode 100644 index 72cf34c..0000000 --- a/agent/alembic/versions/32268fce88c5_.py +++ /dev/null @@ -1,36 +0,0 @@ -"""empty message - -Revision ID: 32268fce88c5 -Revises: 1595751865c1 -Create Date: 2026-07-17 16:31:01.794850 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '32268fce88c5' -down_revision: Union[str, Sequence[str], None] = '1595751865c1' -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! ### - with op.batch_alter_table('captchas', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_captchas_captcha')) - - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('captchas', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_captchas_captcha'), ['captcha'], unique=False) - - # ### end Alembic commands ### diff --git a/agent/alembic/versions/51f049b860bb_.py b/agent/alembic/versions/51f049b860bb_.py deleted file mode 100644 index 0c62370..0000000 --- a/agent/alembic/versions/51f049b860bb_.py +++ /dev/null @@ -1,67 +0,0 @@ -"""empty message - -Revision ID: 51f049b860bb -Revises: -Create Date: 2026-07-13 22:23:49.155469 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - -# revision identifiers, used by Alembic. -revision: str = '51f049b860bb' -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 5741c47..eba6182 100644 --- a/agent/application/application.py +++ b/agent/application/application.py @@ -3,7 +3,7 @@ 应用主入口 """ import reflex as rx -from application.pages.index import render_index +from application.pages.index import index app = rx.App( theme=rx.theme(), @@ -35,4 +35,4 @@ app = rx.App( }, # 自定义主题颜色 ) # 此处变量名需使用 app ,具体原因目前尚不清楚 # 注册首页路由 -app.add_page(component=render_index(), route="/") +app.add_page(component=index(), route="/") diff --git a/agent/application/domain_models.py b/agent/application/domain_models.py index 8284f4b..2864271 100644 --- a/agent/application/domain_models.py +++ b/agent/application/domain_models.py @@ -6,6 +6,7 @@ from datetime import datetime from typing import Dict, List from pydantic_ai._uuid import uuid7 from pydantic import BaseModel, Field +from dataclasses import dataclass class Thought(BaseModel): @@ -13,7 +14,6 @@ class Thought(BaseModel): 思考节点领域模型 """ - id: str = Field(..., description="思考唯一标识") type: str = Field(..., description="思考类型") content: str = Field(..., description="思考内容") @@ -23,9 +23,9 @@ class Dialog(BaseModel): 对话领域模型 """ - id: str = Field(..., description="对话唯一标识") + id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识") user_prompt: str = Field(..., description="用户提示词") - thoughts: List[Thought] = Field(default_factory=list, description="思考列表") + thoughts: List[Thought] = Field(default_factory=List, description="思考列表") result_output: str = Field(default="", description="结果输出") is_thinking: bool = Field( default=False, description="正在思考,True 表示正在思考,False 表示未正在思考" @@ -45,7 +45,7 @@ class Conversation(BaseModel): is_running: bool = Field( default=False, description="正在运行,True 表示正在运行,False 表示未正在运行" ) - dialogs: List[Dialog] = Field(default_factory=list, description="对话列表") + dialogs: Dict[str, Dialog] = Field(default_factory=Dict, description="对话列表") created_at: datetime = Field(default_factory=datetime.now, description="创建时间") @@ -53,6 +53,7 @@ class ConversationHistoryItem(BaseModel): """ 会话历史项领域模型 """ + id: str = Field(..., description="会话唯一标识") description: str = Field(..., description="会话描述") created_at: str = Field(..., description="会话创建时间") diff --git a/agent/application/pages/__init__.py b/agent/application/pages/__init__.py index 5b5299e..a7471d0 100644 --- a/agent/application/pages/__init__.py +++ b/agent/application/pages/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from application.pages.conversation import render_conversation -from application.pages.library import render_library +from application.pages.conversation import conversation +from application.pages.knowledge_base import knowledge_base -__all__ = ["render_conversation", "render_library"] +__all__ = ["conversation", "knowledge_base"] diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 93a153b..904627e 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -4,31 +4,33 @@ """ import reflex as rx -from application.models import Conversation, Dialog, ThoughtNode -from application.states import ConversationState -from reflex_base.vars.object import ObjectVar +from application.domain_models import ( + Conversation, + Dialog, + Thought, + ConversationHistoryItem, +) +from application.states import ConversationState, AuthState -def render_conversation_history_item( - item: rx.Var[tuple], +def conversation_history_item( + item: ConversationHistoryItem, ) -> rx.Component: """ - 渲染会话历史条目 - :param item: 会话历史条目 + 会话历史项 + :param item: 会话历史项 :return: Component """ - conversation_id: rx.Var[str] = item[0] - conversation: ObjectVar[Conversation] = item[1].to(Conversation) # 会话激活状态 - is_conversation_actived = conversation_id == ConversationState.conversation_id + is_actived: bool = item.id == ConversationState.conversation_id - return rx.box( + return rx.list.item( rx.vstack( rx.hstack( # 对话描述 rx.text( - conversation.description, + item.description, flex=1, height="22px", line_height="22px", @@ -82,7 +84,7 @@ def render_conversation_history_item( }, # 点击事件:删除对话 on_click=lambda: ConversationState.delete_conversation( - conversation_id + item.id ), ), ), @@ -105,12 +107,10 @@ def render_conversation_history_item( cursor="pointer", style={ "opacity": rx.cond( - is_conversation_actived, "1", "0" + is_actived, "1", "0" ), # 若当前会话已激活则不透明,否则透明 "transition": "opacity 0.18s ease", - "pointer_events": rx.cond( - is_conversation_actived, "auto", "none" - ), + "pointer_events": rx.cond(is_actived, "auto", "none"), }, ), display="flex", @@ -132,39 +132,39 @@ def render_conversation_history_item( padding="16px", margin_bottom="8px", background=rx.cond( - is_conversation_actived, + is_actived, "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", "var(--prismui-background-color-1)", ), border_radius="var(--prismui-border-radius-3)", - box_shadow=rx.cond(is_conversation_actived, "2px 2px 8px #e9e9e9", "none"), + box_shadow=rx.cond(is_actived, "2px 2px 8px #e9e9e9", "none"), style={ "&: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", }, # 鼠标悬停强制显示右侧三点按钮 }, cursor="pointer", - # 点击事件:将指定对话唯一标识设置为当前对话唯一标识 - on_click=ConversationState.switch_conversation(conversation_id), + # 点击事件:切换会话 + on_click=ConversationState.switch_conversation(item.id), ) -def conversation_history_collapse( +def conversation_history( is_conversation_history_shown: bool, ) -> rx.Component: """ - 会话历史折叠面板 + 会话历史 :param is_conversation_history_shown: 会话历史展示状态 :return: Component """ return rx.box( rx.vstack( - # 渲染标题 + # 标题 rx.hstack( rx.text( "会话历史", @@ -175,11 +175,11 @@ def conversation_history_collapse( align_items="center", justify_content="space-between", ), - # 渲染会话历史列表 + # 会话历史列表 rx.auto_scroll( rx.foreach( - ConversationState.conversation_history, - render_conversation_history_item, + ConversationState.conversation_history_items, + conversation_history_item, ), flex="1", align_items="stretch", @@ -209,9 +209,9 @@ def conversation_history_collapse( ) -def render_greeting() -> rx.Component: +def greeting() -> rx.Component: """ - 渲染问候 + 欢迎 :return: Component """ return rx.vstack( @@ -318,20 +318,20 @@ def render_greeting() -> rx.Component: ) -def render_thought_node(thought_node_id: int, thought_node: ThoughtNode): +def thought(thought: Thought): """ - 渲染思考节点 - :param thought_node_id: 思考节点唯一标识 - :param thought_node: 思考节点实例 + 思考节点 + + :param thought: 思考节点实例 :return: Component """ return rx.vstack( rx.match( - thought_node.kind, + thought.type, ( "thinking", rx.text( - thought_node.content, + thought.content, line_height="22px", font_size="var(--prismui-font-size-2)", color="var(--prismui-color-3)", @@ -341,32 +341,30 @@ def render_thought_node(thought_node_id: int, thought_node: ThoughtNode): ), align_items="flex-start", width="100%", - key=thought_node_id, ) -def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: +def dialog(dialog: Dialog) -> rx.Component: """ - 渲染对话历史条目 - :param dialog_id: 对话唯一标识 + 对话 :param dialog: 对话实例 :return: Component """ # 推理状态 is_thinking = dialog.is_thinking # 思考折叠面板展开状态 - is_collapse_expanded = dialog.is_collapse_expanded + is_expanded = dialog.is_expanded # 思考节点 - thought_nodes = dialog.thought_nodes + thoughts = dialog.thoughts return rx.vstack( - # 渲染问题和操作栏 + # 问题和操作栏 rx.hstack( rx.spacer(), rx.vstack( - # 渲染问题 + # 问题 rx.text( - dialog.question, + dialog.user_prompt, max_width="600px", padding="12px 16px", background_color="var(--prismui-background-color-3)", @@ -404,7 +402,7 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: gap="4px", font_size="var(--devui-font-size)", ), - # 渲染思考折叠面板 + # 思考折叠面板 rx.vstack( # 标题栏 rx.box( @@ -421,7 +419,7 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: color="var(--prismui-color-3)", style={ "transform": rx.cond( - is_collapse_expanded, "rotate(90deg)", "rotate(0deg)" + is_expanded, "rotate(90deg)", "rotate(0deg)" ), "transition": "transform 0.2s ease-out", }, @@ -433,14 +431,14 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: ), cursor="pointer", # 点击事件,展开/折叠思考折叠面板 - on_click=lambda: ConversationState.toggle_collapse(dialog_id), + on_click=lambda: ConversationState.toggle_collapse(dialog.id), ), rx.box( rx.box( rx.auto_scroll( rx.foreach( - thought_nodes, - lambda i, _: render_thought_node(i[0], i[1]), + thoughts, + thought, ), width="100%", margin_top="8px", @@ -452,16 +450,16 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: style={ "display": "grid", "overflow": "hidden", - "grid_template_rows": rx.cond(is_collapse_expanded, "1fr", "0fr"), - "opacity": rx.cond(is_collapse_expanded, "1", "0"), + "grid_template_rows": rx.cond(is_expanded, "1fr", "0fr"), + "opacity": rx.cond(is_expanded, "1", "0"), # 双属性同步过渡,和千问一致 "transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out", }, ), ), - # 渲染回答 + # 结果输出 rx.markdown( - dialog.answer, + dialog.result_output, component_map={ "p": lambda text: rx.text( text, @@ -501,30 +499,30 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: ), padding="0 16px", width="100%", - key=dialog_id, + key=dialog.id, ) -def render_dialog_history() -> rx.Component: +def dialogs() -> rx.Component: """ - 渲染对话历史 + 对话列表 :return: Component """ # 获取当前会话的对话历史 - dialog_history = ConversationState.dialog_history + dialogs = ConversationState.dialogs - # 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 + # 若运行字典为空则品牌图标、名称和介绍、预设用户提示词等,否则遍历运行项 return rx.cond( - dialog_history.length() == 0, - # 渲染问候 - render_greeting(), - # 遍历渲染对话项 + dialogs.length() == 0, + # 欢迎 + greeting(), + # 遍历对话项 rx.vstack( rx.auto_scroll( rx.foreach( - dialog_history, - lambda i, _: render_dialog_history_item(i[0], i[1]), + dialogs, + dialog, ), width="100%", padding="0 12px", @@ -541,9 +539,9 @@ def render_dialog_history() -> rx.Component: ) -def render_create_conversation_button() -> rx.Component: +def create_conversation_button() -> rx.Component: """ - 渲染新建会话按钮 + 新建会话按钮 :return: Component """ return rx.hstack( @@ -625,13 +623,13 @@ def render_create_conversation_button() -> rx.Component: ) -def render_input() -> rx.Component: +def operation() -> rx.Component: """ - 渲染输入框 + 操作区 :return: Component """ return rx.vstack( - # 渲染自定义输入组件 + # 自定义输入组件 rx.form( rx.box( rx.vstack( @@ -710,7 +708,7 @@ def render_input() -> rx.Component: on_submit=ConversationState.run, reset_on_submit=True, ), - # 渲染底部文案 + # 底部文案 rx.text( "内容由大模型生成,无法确保准确性和完整性,仅供参考", margin_top="8px", @@ -725,40 +723,11 @@ def render_input() -> rx.Component: ) -def workbench() -> 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( +def conversation_history_collapse_button( is_conversation_history_shown: bool, ) -> rx.Component: """ - 渲染会话历史折叠面板按钮 + 会话历史折叠面板按钮 :param is_conversation_history_shown: 会话历史展示状态 :return: Component """ @@ -792,26 +761,39 @@ def render_conversation_history_collapse_button( ) -def render_conversation() -> rx.Component: +def conversation() -> rx.Component: """ - 渲染会话页面:布局参考 MetaChat,左侧为折叠面板,右侧为工作区。其中,工作区包含展示区和操作区。若对话历史为空则展示欢迎文案,否则展示对话历史 + 会话页面:布局参考 MetaChat,左侧为折叠面板,右侧为工作区。其中,工作区包含展示区和操作区。若对话历史为空则展示欢迎文案,否则展示对话历史 """ # 会话历史展示状态 is_conversation_history_shown = ConversationState.is_conversation_history_shown return rx.box( rx.hstack( -<<<<<<< HEAD - # 会话历史折叠面板 - conversation_history_collapse(is_conversation_history_shown), - # 工作台 - workbench(), -======= - # 渲染折叠面板 - render_collapse(is_conversation_history_shown), - # 渲染工作区 - render_workplace(), ->>>>>>> 7f6a992830604a43946173a1d6573aa63f835d55 + # 会话历史 + conversation_history(is_conversation_history_shown), + rx.box( + rx.vstack( + # 对话历史 + dialogs(), + # 新建对话按钮 + create_conversation_button(), + # 操作区 + operation(), + 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%)", + ), position="relative", display="flex", flex="1", @@ -820,13 +802,13 @@ def render_conversation() -> rx.Component: overflow="hidden", transition="all 0.3s ease-in-out", ), - # 渲染会话历史折叠面板按钮 - render_conversation_history_collapse_button(is_conversation_history_shown), + # 会话历史折叠面板按钮 + conversation_history_collapse_button(is_conversation_history_shown), position="relative", width="100%", height="100%", border_radius="var(--prismui-border-radius-4)", overflow="hidden", - # 挂载事件:同步用户唯一标识 - on_mount=AuthState.sync_user_id, + # 挂载事件:恢复当前用户会话状态 + on_mount=AuthState.resume_conversation_state, ) diff --git a/agent/application/pages/index.py b/agent/application/pages/index.py index e6c5100..c9dde18 100644 --- a/agent/application/pages/index.py +++ b/agent/application/pages/index.py @@ -4,15 +4,14 @@ """ import reflex as rx -from application.pages import render_conversation, render_library -from application.pages.conversation import render_conversation +from application.pages import conversation, knowledge_base from application.states import AuthState # 导航按钮字典 nav_buttons: dict = { "conversation": {"src": "/conversation.svg", "text": "会话"}, - "library": {"src": "/library.svg", "text": "知识库"}, + "knowledge_base": {"src": "/knowledge_base.svg", "text": "知识库"}, } """ @@ -21,19 +20,19 @@ nav_buttons: dict = { """ -def render_nav_button( +def nav_button( nav_button: str, ) -> rx.Component: """ - 渲染导航按钮 + 导航按钮 :param nav_button: 指定的导航按钮 :return : Component """ # 导航按钮激活状态 - is_nav_button_actived = nav_button == AuthState.activated_nav_button + is_actived = nav_button == AuthState.activated_nav_button return rx.vstack( - # 渲染图标 + # 图标 rx.image( nav_buttons[nav_button]["src"], width="36px", @@ -41,16 +40,14 @@ def render_nav_button( object_fit="contain", padding="6px", background_color=rx.cond( - is_nav_button_actived, + is_actived, "var(--prismui-background-color-1)", "transparent", ), border_radius="var(--prismui-border-radius-3)", - box_shadow=rx.cond( - is_nav_button_actived, "var(--prismui-box-shadow-2)", "none" - ), + box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"), ), - # 渲染标签 + # 标签 rx.text( nav_buttons[nav_button]["text"], line_height="20px", @@ -65,9 +62,9 @@ def render_nav_button( ) -def render_settings_button(): +def settings_button(): """ - 渲染设置按钮 + 设置按钮 :return: Component """ return rx.box( @@ -92,12 +89,12 @@ def render_settings_button(): style={ "&:hover": { "background": "var(--prismui-background-color-5)", - }, # 鼠标悬停渲染背景颜色 + }, # 鼠标悬停背景颜色 }, cursor="pointer", ) ), - # 渲染悬停卡片 + # 悬停卡片 rx.hover_card.content( rx.box( rx.box( @@ -151,9 +148,9 @@ def render_settings_button(): ) -def render_sidebar() -> rx.Component: +def sidebar() -> rx.Component: """ - 渲染侧边栏 + 侧边栏 :return: Component """ @@ -177,7 +174,7 @@ def render_sidebar() -> rx.Component: align_items="center", gap="4px", ), - # 渲染分隔线 + # 分隔线 rx.divider( width="32px", height="1px", @@ -185,10 +182,10 @@ def render_sidebar() -> rx.Component: background_color="var(--prismui-background-color-4)", ), rx.vstack( - # 渲染会话导航按钮 - render_nav_button("conversation"), - # 渲染知识库导航按钮 - render_nav_button("library"), + # 会话导航按钮 + nav_button("conversation"), + # 知识库导航按钮 + nav_button("knowledge_base"), gap="8px", ), align_items="center", @@ -197,8 +194,8 @@ def render_sidebar() -> rx.Component: ), rx.vstack( rx.vstack( - # 渲染设置按钮 - render_settings_button(), + # 设置按钮 + settings_button(), ), padding_bottom="16px", gap="8px", @@ -213,9 +210,9 @@ def render_sidebar() -> rx.Component: ) -def render_login_window() -> rx.Component: +def login_window() -> rx.Component: """ - 渲染登录窗 + 登录窗 :return: Component """ @@ -423,22 +420,22 @@ def render_login_window() -> rx.Component: ) -def render_index() -> rx.Component: +def index() -> rx.Component: """ - 渲染首页 + 首页(默认显示会话页面) """ return rx.fragment( rx.hstack( - # 渲染侧边栏 - render_sidebar(), - # 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局) + # 侧边栏 + sidebar(), + # 根据侧边栏状态中激活的导航按钮相应页面(本项目采用卡片布局) rx.match( AuthState.activated_nav_button, - # 渲染知识库页面 - ("library", render_library()), - # 渲染会话页面 - render_conversation(), + # 知识库页面 + ("knowledge_base", knowledge_base()), + # 会话页面 + conversation(), ), width="100%", height="100vh", @@ -446,10 +443,10 @@ def render_index() -> rx.Component: padding="8px 8px 8px 0", background="var(--prismui-background-1)", ), - # 若用户唯一标识为空则渲染登录窗 + # 若用户唯一标识为空则登录窗 rx.cond( AuthState.user_id, rx.fragment(), - render_login_window(), + login_window(), ), ) diff --git a/agent/application/pages/library.py b/agent/application/pages/knowledge_base.py similarity index 77% rename from agent/application/pages/library.py rename to agent/application/pages/knowledge_base.py index 4691ba3..f5c74db 100644 --- a/agent/application/pages/library.py +++ b/agent/application/pages/knowledge_base.py @@ -5,7 +5,7 @@ import reflex as rx -def render_library() -> rx.Component: +def knowledge_base() -> rx.Component: """ 知识库页面 """ diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 48996d9..6373624 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -188,13 +188,13 @@ class AuthState(rx.State): self.is_policies_agreed = not self.is_policies_agreed @rx.event - async def sync_user_id(self): + async def resume_conversation_state(self): """ - 同步用户唯一标识 + 恢复当前用户会话状态 """ - # 同步会话状态中用户唯一标识 + # 恢复当前用户会话状态 conversation_state = await self.get_state(ConversationState) - await conversation_state.set_user_id(user_id=self.user_id) + await conversation_state.resume(user_id=self.user_id) @rx.event async def login(self) -> None: @@ -235,9 +235,9 @@ class AuthState(rx.State): # 创建用户记录 user_id = await database_state.create_user_record(email=self.email) - # 就会话状态设置用户唯一标识 + # 恢复当前用户会话状态 conversation_state = await self.get_state(ConversationState) - await conversation_state.set_user_id(user_id=user_id) + await conversation_state.resume(user_id=user_id) self.user_id = user_id @@ -268,7 +268,7 @@ class AuthState(rx.State): conversation_state = await self.get_state(ConversationState) conversation_state.user_id = "" - conversation_state.conversations = [] + conversation_state.conversations = {} conversation_state.conversation_id = "" conversation_state.is_conversation_history_shown = False conversation_state.is_conversation_creating = False diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index e6f9f89..6af05dd 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -24,7 +24,12 @@ from pydantic_ai.run import AgentRunResultEvent import reflex as rx from application.states.database import DatabaseState -from application.domain_models import Conversation, Dialog, ConversationHistoryItem +from application.domain_models import ( + Conversation, + Dialog, + Thought, + ConversationHistoryItem, +) instructions: str = """ # 角色 @@ -76,9 +81,9 @@ class ConversationState(rx.State): # 会话创建状态,True表示正在创建,False表示未正在创建 is_conversation_creating: bool = False - async def set_user_id(self, user_id: str) -> None: + async def resume(self, user_id: str) -> None: """ - 设置用户唯一标识 + 恢复当前用户会话状态 :param user_id: 用户唯一标识 :return: None """ @@ -108,11 +113,11 @@ class ConversationState(rx.State): self.is_conversation_history_shown = not self.is_conversation_history_shown @staticmethod - def format_created_at(created_at: datetime) -> str: + def format_conversation_created_at(created_at: datetime) -> str: """ - 格式化创建时间 - :param created_at: 创建时间 - :return: 格式化后的创建时间 + 格式化会话创建时间 + :param created_at: 会话创建时间 + :return: 格式化后的会话创建时间 """ match (datetime.now().date() - created_at.date()).days: case 0: @@ -137,17 +142,50 @@ class ConversationState(rx.State): ConversationHistoryItem( id=conversation.id, description=conversation.description, - created_at=self.format_created_at( + created_at=self.format_conversation_created_at( created_at=conversation.created_at ), ) ) return items + @rx.event + async def delete_conversation(self, conversation_id: str) -> None: + """ + 删除会话 + :param conversation_id: 需删除的会话唯一标识 + :return: None + """ + # 先设置会话记录为已删除再在会话字典中删除会话实例 + database_state = await self.get_state(DatabaseState) + await database_state.set_conversations_record_deleted( + conversation_id=conversation_id + ) + del self.conversations[conversation_id] + + # 删除后,若会话字典为空则先创建会话记录再添加会话实例 + if not self.conversations: + self.conversations.update( + await database_state.create_conversations_record(user_id=self.user_id) + ) + + # 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识 + if self.conversation_id not in self.conversations: + self.conversation_id = next(reversed(self.conversations)) + + @rx.event + def switch_conversation(self, conversation_id: str) -> None: + """ + 切换会话 + :param conversation_id: 需切换的会话唯一标识 + :return: None + """ + self.conversation_id = conversation_id + @rx.event async def create_conversation(self, form_data: Dict[str, Any]) -> None: """ - 新建会话 + 创建会话 :param form_data: 表单数据 :return: None """ @@ -167,58 +205,13 @@ class ConversationState(rx.State): # 会话创建状态设置为未正在创建 self.is_conversation_creating = False - @rx.event - async def delete_conversation(self, conversation_id: str) -> None: - """ - 删除指定会话 - :param conversation_id: 指定会话唯一标识 - :return: None - """ - # 先逻辑删除会话记录再在会话列表删除会话实例 - database_state = await self.get_state(DatabaseState) - await database_state.delete_conversations_record( - conversation_id=conversation_id - ) - del self.conversations[idx] - - # 删除后,若会话列表为空则先创建会话记录再添加会话实例 - if not self.conversations: - self.conversations.append( - await database_state.create_conversations_record(user_id=self.user_id) - ) - - # 删除后,若当前会话唯一标识不存在则将最后一个会话的唯一标识设置为当前会话唯一标识 - if self.conversation_id not in self.conversations: - self.conversation_id = next(reversed(self.conversations.keys())) - - @rx.event - def switch_conversation(self, conversation_id: str) -> None: - """ - 将指定会话的唯一标识设置为当前会话唯一标识 - :param conversation_id: 指定会话唯一标识 - :return: None - """ - if conversation_id not in self.conversations: - return - self.conversation_id = conversation_id - @rx.event def toggle_conversation_creating(self) -> None: """ - 切换正在新建会话状态 + 切换会话创建状态 """ self.is_conversation_creating = not self.is_conversation_creating - @rx.var - def dialog_history(self) -> Dict[str, Dialog]: - """ - 获取当前会话的对话历史,用于前端渲染对话历史 - :return: 当前会话的对话历史 - """ - # 当前会话 - conversation = self.conversations.get(self.conversation_id) - return conversation.dialogs if conversation else {} - @rx.var def running_status(self) -> bool: """ @@ -231,6 +224,18 @@ class ConversationState(rx.State): return False return conversation.is_running + @rx.var + def dialogs(self) -> List[Dialog]: + """ + 获取当前会话的对话列表 + :return: 当前会话的对话列表 + """ + # 当前会话 + conversation = self.conversations.get(self.conversation_id) + if not conversation: + return [] + return list(conversation.dialogs.values()) + @rx.event async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]: """ @@ -238,41 +243,39 @@ class ConversationState(rx.State): :param form_data: 表单数据 :return: AsyncGenerator """ - # 获取问题 - question = form_data.get("question", "").strip() - if not question: + # 解析用户提示词 + user_prompt = form_data["user_prompt"].strip() + if not user_prompt: return # 当前会话 - conversation = self.conversations.get(self.conversation_id) - if not conversation: - return - + conversation = self.conversations[self.conversation_id] # 将当前会话的运行状态设置为正在运行 conversation.is_running = True + yield # 通知前端渲染 # 获取数据库状态 database_state = await self.get_state(DatabaseState) - # 检索运行结果记录并作为消息历史 - message_history = await database_state.retrieve_message_history( + # 获取消息历史 + message_history = await database_state.get_message_history( conversation_id=self.conversation_id ) - # 先创建对话记录再在当前对话字典中新增对话 - dialog_id = await database_state.create_dialogs_record( - conversation_id=self.conversation_id, question=question + # 创建对话记录再添加对话实例 + conversation.dialogs.update( + await database_state.create_dialog_record( + conversation_id=self.conversation_id, user_prompt=user_prompt + ) ) - dialog = conversation.dialogs.setdefault(dialog_id, Dialog(question=question)) + # 将最后一个会话作为当前会话 + dialog = next(reversed(conversation.dialogs.values())) # 初始化工具调用唯一标识和片段索引映射字典 tool_call_ids: Dict[str, int] = {} - - yield # 通知前端渲染 - async with agent.run_stream_events( conversation_id=self.conversation_id, - user_prompt=question, + user_prompt=user_prompt, message_history=message_history, ) as events: async for event in events: @@ -286,51 +289,45 @@ class ConversationState(rx.State): match part: # 思考分片开始事件 case ThinkingPart(content=content): - # 若上一分片种类为空则将思考状态设置为正在思考、思考折叠面板展开状态设置为展开 + # 若上一分片种类为空则将正在思考设置为 True、思考折叠面板展开状态设置为展开 if not previous_part_kind: dialog.is_thinking = True - dialog.is_collapse_expanded = True - - dialog.thought_nodes[index] = ThoughtNode( - kind="thinking", content=content + dialog.is_expanded = True + dialog.thoughts[index] = Thought( + type="thinking", content=content ) - yield # 工具检索分片开始事件 case ToolSearchCallPart(tool_call_id=tool_call_id): # 创建工具调用唯一标识与片段索引映射 tool_call_ids[tool_call_id] = index - dialog.thought_nodes[index] = ThoughtNode( - kind="tool_search", + dialog.thoughts[index] = Thought( + type="tool_search", content="正在生成检索关键词", ) - yield # 能力加载分片开始事件 case LoadCapabilityCallPart(tool_call_id=tool_call_id): tool_call_ids[tool_call_id] = index - dialog.thought_nodes[index] = ThoughtNode( - kind="capability_load", + dialog.thoughts[index] = Thought( + type="capability_load", content="正在生成加载参数", ) - yield # 工具调用分片开始事件 case ToolCallPart(tool_call_id=tool_call_id): tool_call_ids[tool_call_id] = index - dialog.thought_nodes[index] = ThoughtNode( - kind="tool_call", + dialog.thoughts[index] = Thought( + type="tool_call", content="正在生成调用参数", ) - yield # 文本分片开始事件 case TextPart(content=content): - dialog.answer = content - yield + dialog.result_output = content # ========== 增量事件 ========== case PartDeltaEvent(index=index, delta=delta): @@ -339,17 +336,13 @@ class ConversationState(rx.State): case ThinkingPartDelta( content_delta=content_delta, ): - dialog.thought_nodes[index].content += ( - content_delta or "" - ) - yield + dialog.thoughts[index].content += content_delta or "" # 文本分片增量事件 case TextPartDelta( content_delta=content_delta, ): - dialog.answer += content_delta - yield + dialog.result_output += content_delta # ========== 结束事件 ========== case PartEndEvent( @@ -363,32 +356,28 @@ class ConversationState(rx.State): # 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠 if next_part_kind == "text": dialog.is_thinking = False - dialog.is_collapse_expanded = False - yield + dialog.is_expanded = False # ========== 函数工具调用事件 ========== case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part): # 获取分片索引 index = tool_call_ids[tool_call_id] - match dialog.thought_nodes[index].kind: + match dialog.thoughts[index].type: # 工具检索 case "tool_search": - dialog.thought_nodes[index].content = "正在检索" - yield + dialog.thoughts[index].content = "正在检索" # 能力加载 case "capability_load": - dialog.thought_nodes[index].content = ( + dialog.thoughts[index].content = ( f"正在加载能力 {part.tool_name}" ) - yield # 工具调用 case "tool_call": - dialog.thought_nodes[index].content = ( + dialog.thoughts[index].content = ( f"正在调用工具 {part.tool_name}" ) - yield # ========== 函数工具结果事件 ========== case FunctionToolResultEvent( @@ -396,41 +385,39 @@ class ConversationState(rx.State): content=content, ): index = tool_call_ids[tool_call_id] - match dialog.thought_nodes[index].kind: + match dialog.thoughts[index].type: # 工具检索 case "tool_search": - dialog.thought_nodes[index].content = ( + dialog.thoughts[index].content = ( content if isinstance(content, str) else "" ) # 暂仅考虑文本内容 - yield # 能力加载 case "capability_load": - dialog.thought_nodes[index].content = "已加载" - yield + dialog.thoughts[index].content = "已加载" # 工具调用 case "tool_call": - dialog.thought_nodes[index].content = f"已调用" - yield + dialog.thoughts[index].content = f"已调用" # ========== 智能体运行结果事件 ========== case AgentRunResultEvent(result=result): - # 更新对话记录 - await database_state.update_dialog_record( - dialog_id=dialog_id, - thought_nodes=dialog.thought_nodes, - answer=dialog.answer, + # 补全对话记录 + await database_state.complete_dialog_record( + id=dialog.id, + thoughts=dialog.thoughts, + result_output=dialog.result_output, ) - # 创建运行结果记录 - await database_state.create_run_result_record( + # 创建结果记录 + await database_state.create_result_record( conversation_id=self.conversation_id, - dialog_id=dialog_id, + dialog_id=dialog.id, new_messages=result.new_messages(), ) # 将当前会话的运行状态设置为运行完成 conversation.is_running = False - yield + + yield @rx.event def toggle_collapse(self, dialog_id: str) -> None: @@ -439,4 +426,4 @@ class ConversationState(rx.State): """ # 指定运行 dialog = self.conversations[self.conversation_id].dialogs[dialog_id] - dialog.is_collapse_expanded = not dialog.is_collapse_expanded + dialog.is_expanded = not dialog.is_expanded diff --git a/agent/application/states/database.py b/agent/application/states/database.py index 63fbb7f..5cf22dc 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -112,8 +112,11 @@ class DialogRecord(SQLModel, table=True): result_output: str = Field(default="", description="结果输出") -# 运行结果表 -class RunResultRecord(SQLModel, table=True): +class ResultRecord(SQLModel, table=True): + """ + 结果记录 + """ + conversation_id: str = Field(primary_key=True, description="会话唯一标识") dialog_id: str = Field(primary_key=True, description="对话唯一标识") new_messages: str = Field(description="新增消息") @@ -228,15 +231,17 @@ class DatabaseState(rx.State): ) if not dialog_record: continue - record.dialogs.append( - Dialog( - id=dialog_record.id, - user_prompt=dialog_record.user_prompt, - thoughts=ThoughtTypeAdapter.validate_python( - dialog_record.thoughts - ), - result_output=dialog_record.result_output, - ) + record.dialogs.update( + { + dialog_record.id: Dialog( + id=dialog_record.id, + user_prompt=dialog_record.user_prompt, + thoughts=ThoughtTypeAdapter.validate_python( + dialog_record.thoughts + ), + result_output=dialog_record.result_output, + ) + } ) return records @@ -262,9 +267,9 @@ class DatabaseState(rx.State): ) } - async def delete_conversations_record(self, conversation_id: str) -> None: + async def set_conversations_record_deleted(self, conversation_id: str) -> None: """ - 逻辑删除会话记录(将会话已删除设置为 True) + 设置会话记录为已删除 :param conversation_id: 指定会话唯一标识 :return: None """ @@ -275,52 +280,54 @@ class DatabaseState(rx.State): record.is_deleted = True await session.commit() - async def create_dialogs_record(self, conversation_id: str, question: str) -> str: + async def create_dialog_record( + self, conversation_id: str, user_prompt: str + ) -> Dialog: """ 创建对话记录 :param conversation_id: 会话唯一标识 - :param question: 问题 + :param user_prompt: 用户提示词 :return: 创建对话记录的唯一标识 """ async with rx.asession() as session: record = DialogRecord( conversation_id=conversation_id, - user_prompt=question, + user_prompt=user_prompt, ) session.add(record) await session.commit() await session.refresh(record) - return record.id + return Dialog(id=record.id, user_prompt=user_prompt) - async def update_dialog_record( + async def complete_dialog_record( self, - dialog_id: str, + id: str, thoughts: List[Thought], result_output: str, ) -> None: """ - 更新对话记录 - :param dialog_id: 对话唯一标识 + 补全对话记录 + :param id: 对话唯一标识 :param thoughts: 思考列表 :param result_output: 结果输出 :return: None """ async with rx.asession() as session: - record = await session.get(DialogRecord, dialog_id) + record = await session.get(DialogRecord, id) # 通过主键查询记录 if not record: return record.thoughts = ThoughtTypeAdapter.dump_python(thoughts) record.result_output = result_output await session.commit() - async def create_run_result_record( + async def create_result_record( self, conversation_id: str, dialog_id: str, new_messages: List[ModelMessage], ) -> None: """ - 创建运行结果记录 + 创建结果记录 :param conversation_id: 会话唯一标识 :param dialog_id: 对话唯一标识 :param new_messages: 新增消息 @@ -328,7 +335,7 @@ class DatabaseState(rx.State): """ async with rx.asession() as session: session.add( - RunResultRecord( + ResultRecord( conversation_id=conversation_id, dialog_id=dialog_id, new_messages=ModelMessagesTypeAdapter.dump_json( @@ -340,9 +347,7 @@ class DatabaseState(rx.State): ) await session.commit() - async def retrieve_message_history( - self, conversation_id: str - ) -> List[ModelMessage]: + async def get_message_history(self, conversation_id: str) -> List[ModelMessage]: """ 获取消息历史 :param conversation_id: 会话唯一标识 @@ -351,9 +356,9 @@ class DatabaseState(rx.State): records: List[ModelMessage] = [] async with rx.asession() as session: result = await session.exec( - select(RunResultRecord) - .where(RunResultRecord.conversation_id == conversation_id) - .order_by(desc(RunResultRecord.dialog_id)) + select(ResultRecord) + .where(ResultRecord.conversation_id == conversation_id) + .order_by(desc(ResultRecord.dialog_id)) ) for record in result.all(): records.extend( diff --git a/agent/assets/library.svg b/agent/assets/knowledge_base.svg similarity index 100% rename from agent/assets/library.svg rename to agent/assets/knowledge_base.svg