This commit is contained in:
liubiren 2026-07-26 23:20:36 +08:00
parent e80c19a1c5
commit 50e3479ab5
17 changed files with 298 additions and 752 deletions

View File

@ -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 <script_location>/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

View File

@ -1 +0,0 @@
Generic single-database configuration.

View File

@ -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()

View File

@ -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"}

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -3,7 +3,7 @@
应用主入口 应用主入口
""" """
import reflex as rx import reflex as rx
from application.pages.index import render_index from application.pages.index import index
app = rx.App( app = rx.App(
theme=rx.theme(), theme=rx.theme(),
@ -35,4 +35,4 @@ app = rx.App(
}, # 自定义主题颜色 }, # 自定义主题颜色
) # 此处变量名需使用 app ,具体原因目前尚不清楚 ) # 此处变量名需使用 app ,具体原因目前尚不清楚
# 注册首页路由 # 注册首页路由
app.add_page(component=render_index(), route="/") app.add_page(component=index(), route="/")

View File

@ -6,6 +6,7 @@ from datetime import datetime
from typing import Dict, List from typing import Dict, List
from pydantic_ai._uuid import uuid7 from pydantic_ai._uuid import uuid7
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from dataclasses import dataclass
class Thought(BaseModel): class Thought(BaseModel):
@ -13,7 +14,6 @@ class Thought(BaseModel):
思考节点领域模型 思考节点领域模型
""" """
id: str = Field(..., description="思考唯一标识")
type: str = Field(..., description="思考类型") type: str = Field(..., description="思考类型")
content: 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="用户提示词") 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="结果输出") result_output: str = Field(default="", description="结果输出")
is_thinking: bool = Field( is_thinking: bool = Field(
default=False, description="正在思考True 表示正在思考False 表示未正在思考" default=False, description="正在思考True 表示正在思考False 表示未正在思考"
@ -45,7 +45,7 @@ class Conversation(BaseModel):
is_running: bool = Field( is_running: bool = Field(
default=False, description="正在运行True 表示正在运行False 表示未正在运行" 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="创建时间") created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
@ -53,6 +53,7 @@ class ConversationHistoryItem(BaseModel):
""" """
会话历史项领域模型 会话历史项领域模型
""" """
id: str = Field(..., description="会话唯一标识") id: str = Field(..., description="会话唯一标识")
description: str = Field(..., description="会话描述") description: str = Field(..., description="会话描述")
created_at: str = Field(..., description="会话创建时间") created_at: str = Field(..., description="会话创建时间")

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from application.pages.conversation import render_conversation from application.pages.conversation import conversation
from application.pages.library import render_library from application.pages.knowledge_base import knowledge_base
__all__ = ["render_conversation", "render_library"] __all__ = ["conversation", "knowledge_base"]

View File

@ -4,31 +4,33 @@
""" """
import reflex as rx import reflex as rx
from application.models import Conversation, Dialog, ThoughtNode from application.domain_models import (
from application.states import ConversationState Conversation,
from reflex_base.vars.object import ObjectVar Dialog,
Thought,
ConversationHistoryItem,
)
from application.states import ConversationState, AuthState
def render_conversation_history_item( def conversation_history_item(
item: rx.Var[tuple], item: ConversationHistoryItem,
) -> rx.Component: ) -> rx.Component:
""" """
渲染会话历史条目 会话历史项
:param item: 会话历史条目 :param item: 会话历史
:return: Component :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.vstack(
rx.hstack( rx.hstack(
# 对话描述 # 对话描述
rx.text( rx.text(
conversation.description, item.description,
flex=1, flex=1,
height="22px", height="22px",
line_height="22px", line_height="22px",
@ -82,7 +84,7 @@ def render_conversation_history_item(
}, },
# 点击事件:删除对话 # 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation( on_click=lambda: ConversationState.delete_conversation(
conversation_id item.id
), ),
), ),
), ),
@ -105,12 +107,10 @@ def render_conversation_history_item(
cursor="pointer", cursor="pointer",
style={ style={
"opacity": rx.cond( "opacity": rx.cond(
is_conversation_actived, "1", "0" is_actived, "1", "0"
), # 若当前会话已激活则不透明,否则透明 ), # 若当前会话已激活则不透明,否则透明
"transition": "opacity 0.18s ease", "transition": "opacity 0.18s ease",
"pointer_events": rx.cond( "pointer_events": rx.cond(is_actived, "auto", "none"),
is_conversation_actived, "auto", "none"
),
}, },
), ),
display="flex", display="flex",
@ -132,39 +132,39 @@ def render_conversation_history_item(
padding="16px", padding="16px",
margin_bottom="8px", margin_bottom="8px",
background=rx.cond( background=rx.cond(
is_conversation_actived, is_actived,
"linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"var(--prismui-background-color-1)", "var(--prismui-background-color-1)",
), ),
border_radius="var(--prismui-border-radius-3)", 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={ style={
"&:hover": { "&:hover": {
"background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", "background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"box_shadow": "2px 2px 8px #e9e9e9", "box_shadow": "2px 2px 8px #e9e9e9",
}, # 鼠标悬停渲染背景颜色和阴影 }, # 鼠标悬停背景颜色和阴影
"&:hover > div > div:last-child": { "&:hover > div > div:last-child": {
"opacity": "1 !important", "opacity": "1 !important",
"pointer_events": "auto !important", "pointer_events": "auto !important",
}, # 鼠标悬停强制显示右侧三点按钮 }, # 鼠标悬停强制显示右侧三点按钮
}, },
cursor="pointer", 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, is_conversation_history_shown: bool,
) -> rx.Component: ) -> rx.Component:
""" """
会话历史折叠面板 会话历史
:param is_conversation_history_shown: 会话历史展示状态 :param is_conversation_history_shown: 会话历史展示状态
:return: Component :return: Component
""" """
return rx.box( return rx.box(
rx.vstack( rx.vstack(
# 渲染标题 # 标题
rx.hstack( rx.hstack(
rx.text( rx.text(
"会话历史", "会话历史",
@ -175,11 +175,11 @@ def conversation_history_collapse(
align_items="center", align_items="center",
justify_content="space-between", justify_content="space-between",
), ),
# 渲染会话历史列表 # 会话历史列表
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
ConversationState.conversation_history, ConversationState.conversation_history_items,
render_conversation_history_item, conversation_history_item,
), ),
flex="1", flex="1",
align_items="stretch", align_items="stretch",
@ -209,9 +209,9 @@ def conversation_history_collapse(
) )
def render_greeting() -> rx.Component: def greeting() -> rx.Component:
""" """
渲染问候 欢迎
:return: Component :return: Component
""" """
return rx.vstack( 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: Component
""" """
return rx.vstack( return rx.vstack(
rx.match( rx.match(
thought_node.kind, thought.type,
( (
"thinking", "thinking",
rx.text( rx.text(
thought_node.content, thought.content,
line_height="22px", line_height="22px",
font_size="var(--prismui-font-size-2)", font_size="var(--prismui-font-size-2)",
color="var(--prismui-color-3)", color="var(--prismui-color-3)",
@ -341,32 +341,30 @@ def render_thought_node(thought_node_id: int, thought_node: ThoughtNode):
), ),
align_items="flex-start", align_items="flex-start",
width="100%", 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: 对话实例 :param dialog: 对话实例
:return: Component :return: Component
""" """
# 推理状态 # 推理状态
is_thinking = dialog.is_thinking 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( return rx.vstack(
# 渲染问题和操作栏 # 问题和操作栏
rx.hstack( rx.hstack(
rx.spacer(), rx.spacer(),
rx.vstack( rx.vstack(
# 渲染问题 # 问题
rx.text( rx.text(
dialog.question, dialog.user_prompt,
max_width="600px", max_width="600px",
padding="12px 16px", padding="12px 16px",
background_color="var(--prismui-background-color-3)", 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", gap="4px",
font_size="var(--devui-font-size)", font_size="var(--devui-font-size)",
), ),
# 渲染思考折叠面板 # 思考折叠面板
rx.vstack( rx.vstack(
# 标题栏 # 标题栏
rx.box( rx.box(
@ -421,7 +419,7 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
color="var(--prismui-color-3)", color="var(--prismui-color-3)",
style={ style={
"transform": rx.cond( "transform": rx.cond(
is_collapse_expanded, "rotate(90deg)", "rotate(0deg)" is_expanded, "rotate(90deg)", "rotate(0deg)"
), ),
"transition": "transform 0.2s ease-out", "transition": "transform 0.2s ease-out",
}, },
@ -433,14 +431,14 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
), ),
cursor="pointer", cursor="pointer",
# 点击事件,展开/折叠思考折叠面板 # 点击事件,展开/折叠思考折叠面板
on_click=lambda: ConversationState.toggle_collapse(dialog_id), on_click=lambda: ConversationState.toggle_collapse(dialog.id),
), ),
rx.box( rx.box(
rx.box( rx.box(
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
thought_nodes, thoughts,
lambda i, _: render_thought_node(i[0], i[1]), thought,
), ),
width="100%", width="100%",
margin_top="8px", margin_top="8px",
@ -452,16 +450,16 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
style={ style={
"display": "grid", "display": "grid",
"overflow": "hidden", "overflow": "hidden",
"grid_template_rows": rx.cond(is_collapse_expanded, "1fr", "0fr"), "grid_template_rows": rx.cond(is_expanded, "1fr", "0fr"),
"opacity": rx.cond(is_collapse_expanded, "1", "0"), "opacity": rx.cond(is_expanded, "1", "0"),
# 双属性同步过渡,和千问一致 # 双属性同步过渡,和千问一致
"transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out", "transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out",
}, },
), ),
), ),
# 渲染回答 # 结果输出
rx.markdown( rx.markdown(
dialog.answer, dialog.result_output,
component_map={ component_map={
"p": lambda text: rx.text( "p": lambda text: rx.text(
text, text,
@ -501,30 +499,30 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
), ),
padding="0 16px", padding="0 16px",
width="100%", width="100%",
key=dialog_id, key=dialog.id,
) )
def render_dialog_history() -> rx.Component: def dialogs() -> rx.Component:
""" """
渲染对话历史 对话列表
:return: Component :return: Component
""" """
# 获取当前会话的对话历史 # 获取当前会话的对话历史
dialog_history = ConversationState.dialog_history dialogs = ConversationState.dialogs
# 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 # 若运行字典为空则品牌图标、名称和介绍、预设用户提示词等,否则遍历运行项
return rx.cond( return rx.cond(
dialog_history.length() == 0, dialogs.length() == 0,
# 渲染问候 # 欢迎
render_greeting(), greeting(),
# 遍历渲染对话项 # 遍历对话项
rx.vstack( rx.vstack(
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
dialog_history, dialogs,
lambda i, _: render_dialog_history_item(i[0], i[1]), dialog,
), ),
width="100%", width="100%",
padding="0 12px", 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: Component
""" """
return rx.hstack( 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: Component
""" """
return rx.vstack( return rx.vstack(
# 渲染自定义输入组件 # 自定义输入组件
rx.form( rx.form(
rx.box( rx.box(
rx.vstack( rx.vstack(
@ -710,7 +708,7 @@ def render_input() -> rx.Component:
on_submit=ConversationState.run, on_submit=ConversationState.run,
reset_on_submit=True, reset_on_submit=True,
), ),
# 渲染底部文案 # 底部文案
rx.text( rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考", "内容由大模型生成,无法确保准确性和完整性,仅供参考",
margin_top="8px", margin_top="8px",
@ -725,40 +723,11 @@ def render_input() -> rx.Component:
) )
def workbench() -> rx.Component: def conversation_history_collapse_button(
"""
工作台
: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_conversation_history_shown: bool, is_conversation_history_shown: bool,
) -> rx.Component: ) -> rx.Component:
""" """
渲染会话历史折叠面板按钮 会话历史折叠面板按钮
:param is_conversation_history_shown: 会话历史展示状态 :param is_conversation_history_shown: 会话历史展示状态
:return: Component :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 is_conversation_history_shown = ConversationState.is_conversation_history_shown
return rx.box( return rx.box(
rx.hstack( rx.hstack(
<<<<<<< HEAD # 会话历史
# 会话历史折叠面板 conversation_history(is_conversation_history_shown),
conversation_history_collapse(is_conversation_history_shown), rx.box(
# 工作台 rx.vstack(
workbench(), # 对话历史
======= dialogs(),
# 渲染折叠面板 # 新建对话按钮
render_collapse(is_conversation_history_shown), create_conversation_button(),
# 渲染工作区 # 操作区
render_workplace(), operation(),
>>>>>>> 7f6a992830604a43946173a1d6573aa63f835d55 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", position="relative",
display="flex", display="flex",
flex="1", flex="1",
@ -820,13 +802,13 @@ def render_conversation() -> rx.Component:
overflow="hidden", overflow="hidden",
transition="all 0.3s ease-in-out", 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", position="relative",
width="100%", width="100%",
height="100%", height="100%",
border_radius="var(--prismui-border-radius-4)", border_radius="var(--prismui-border-radius-4)",
overflow="hidden", overflow="hidden",
# 挂载事件:同步用户唯一标识 # 挂载事件:恢复当前用户会话状态
on_mount=AuthState.sync_user_id, on_mount=AuthState.resume_conversation_state,
) )

View File

@ -4,15 +4,14 @@
""" """
import reflex as rx import reflex as rx
from application.pages import render_conversation, render_library from application.pages import conversation, knowledge_base
from application.pages.conversation import render_conversation
from application.states import AuthState from application.states import AuthState
# 导航按钮字典 # 导航按钮字典
nav_buttons: dict = { nav_buttons: dict = {
"conversation": {"src": "/conversation.svg", "text": "会话"}, "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, nav_button: str,
) -> rx.Component: ) -> rx.Component:
""" """
渲染导航按钮 导航按钮
:param nav_button: 指定的导航按钮 :param nav_button: 指定的导航按钮
:return : Component :return : Component
""" """
# 导航按钮激活状态 # 导航按钮激活状态
is_nav_button_actived = nav_button == AuthState.activated_nav_button is_actived = nav_button == AuthState.activated_nav_button
return rx.vstack( return rx.vstack(
# 渲染图标 # 图标
rx.image( rx.image(
nav_buttons[nav_button]["src"], nav_buttons[nav_button]["src"],
width="36px", width="36px",
@ -41,16 +40,14 @@ def render_nav_button(
object_fit="contain", object_fit="contain",
padding="6px", padding="6px",
background_color=rx.cond( background_color=rx.cond(
is_nav_button_actived, is_actived,
"var(--prismui-background-color-1)", "var(--prismui-background-color-1)",
"transparent", "transparent",
), ),
border_radius="var(--prismui-border-radius-3)", border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond( box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"),
is_nav_button_actived, "var(--prismui-box-shadow-2)", "none"
),
), ),
# 渲染标签 # 标签
rx.text( rx.text(
nav_buttons[nav_button]["text"], nav_buttons[nav_button]["text"],
line_height="20px", line_height="20px",
@ -65,9 +62,9 @@ def render_nav_button(
) )
def render_settings_button(): def settings_button():
""" """
渲染设置按钮 设置按钮
:return: Component :return: Component
""" """
return rx.box( return rx.box(
@ -92,12 +89,12 @@ def render_settings_button():
style={ style={
"&:hover": { "&:hover": {
"background": "var(--prismui-background-color-5)", "background": "var(--prismui-background-color-5)",
}, # 鼠标悬停渲染背景颜色 }, # 鼠标悬停背景颜色
}, },
cursor="pointer", cursor="pointer",
) )
), ),
# 渲染悬停卡片 # 悬停卡片
rx.hover_card.content( rx.hover_card.content(
rx.box( rx.box(
rx.box( rx.box(
@ -151,9 +148,9 @@ def render_settings_button():
) )
def render_sidebar() -> rx.Component: def sidebar() -> rx.Component:
""" """
渲染侧边栏 侧边栏
:return: Component :return: Component
""" """
@ -177,7 +174,7 @@ def render_sidebar() -> rx.Component:
align_items="center", align_items="center",
gap="4px", gap="4px",
), ),
# 渲染分隔线 # 分隔线
rx.divider( rx.divider(
width="32px", width="32px",
height="1px", height="1px",
@ -185,10 +182,10 @@ def render_sidebar() -> rx.Component:
background_color="var(--prismui-background-color-4)", background_color="var(--prismui-background-color-4)",
), ),
rx.vstack( rx.vstack(
# 渲染会话导航按钮 # 会话导航按钮
render_nav_button("conversation"), nav_button("conversation"),
# 渲染知识库导航按钮 # 知识库导航按钮
render_nav_button("library"), nav_button("knowledge_base"),
gap="8px", gap="8px",
), ),
align_items="center", align_items="center",
@ -197,8 +194,8 @@ def render_sidebar() -> rx.Component:
), ),
rx.vstack( rx.vstack(
rx.vstack( rx.vstack(
# 渲染设置按钮 # 设置按钮
render_settings_button(), settings_button(),
), ),
padding_bottom="16px", padding_bottom="16px",
gap="8px", gap="8px",
@ -213,9 +210,9 @@ def render_sidebar() -> rx.Component:
) )
def render_login_window() -> rx.Component: def login_window() -> rx.Component:
""" """
渲染登录窗 登录窗
:return: 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( return rx.fragment(
rx.hstack( rx.hstack(
# 渲染侧边栏 # 侧边栏
render_sidebar(), sidebar(),
# 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局) # 根据侧边栏状态中激活的导航按钮相应页面(本项目采用卡片布局)
rx.match( rx.match(
AuthState.activated_nav_button, AuthState.activated_nav_button,
# 渲染知识库页面 # 知识库页面
("library", render_library()), ("knowledge_base", knowledge_base()),
# 渲染会话页面 # 会话页面
render_conversation(), conversation(),
), ),
width="100%", width="100%",
height="100vh", height="100vh",
@ -446,10 +443,10 @@ def render_index() -> rx.Component:
padding="8px 8px 8px 0", padding="8px 8px 8px 0",
background="var(--prismui-background-1)", background="var(--prismui-background-1)",
), ),
# 若用户唯一标识为空则渲染登录窗 # 若用户唯一标识为空则登录窗
rx.cond( rx.cond(
AuthState.user_id, AuthState.user_id,
rx.fragment(), rx.fragment(),
render_login_window(), login_window(),
), ),
) )

View File

@ -5,7 +5,7 @@
import reflex as rx import reflex as rx
def render_library() -> rx.Component: def knowledge_base() -> rx.Component:
""" """
知识库页面 知识库页面
""" """

View File

@ -188,13 +188,13 @@ class AuthState(rx.State):
self.is_policies_agreed = not self.is_policies_agreed self.is_policies_agreed = not self.is_policies_agreed
@rx.event @rx.event
async def sync_user_id(self): async def resume_conversation_state(self):
""" """
同步用户唯一标识 恢复当前用户会话状态
""" """
# 同步会话状态中用户唯一标识 # 恢复当前用户会话状态
conversation_state = await self.get_state(ConversationState) 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 @rx.event
async def login(self) -> None: async def login(self) -> None:
@ -235,9 +235,9 @@ class AuthState(rx.State):
# 创建用户记录 # 创建用户记录
user_id = await database_state.create_user_record(email=self.email) user_id = await database_state.create_user_record(email=self.email)
# 就会话状态设置用户唯一标识 # 恢复当前用户会话状态
conversation_state = await self.get_state(ConversationState) 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 self.user_id = user_id
@ -268,7 +268,7 @@ class AuthState(rx.State):
conversation_state = await self.get_state(ConversationState) conversation_state = await self.get_state(ConversationState)
conversation_state.user_id = "" conversation_state.user_id = ""
conversation_state.conversations = [] conversation_state.conversations = {}
conversation_state.conversation_id = "" conversation_state.conversation_id = ""
conversation_state.is_conversation_history_shown = False conversation_state.is_conversation_history_shown = False
conversation_state.is_conversation_creating = False conversation_state.is_conversation_creating = False

View File

@ -24,7 +24,12 @@ from pydantic_ai.run import AgentRunResultEvent
import reflex as rx import reflex as rx
from application.states.database import DatabaseState 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 = """ instructions: str = """
# 角色 # 角色
@ -76,9 +81,9 @@ class ConversationState(rx.State):
# 会话创建状态True表示正在创建False表示未正在创建 # 会话创建状态True表示正在创建False表示未正在创建
is_conversation_creating: bool = 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: 用户唯一标识 :param user_id: 用户唯一标识
:return: None :return: None
""" """
@ -108,11 +113,11 @@ class ConversationState(rx.State):
self.is_conversation_history_shown = not self.is_conversation_history_shown self.is_conversation_history_shown = not self.is_conversation_history_shown
@staticmethod @staticmethod
def format_created_at(created_at: datetime) -> str: def format_conversation_created_at(created_at: datetime) -> str:
""" """
格式化创建时间 格式化会话创建时间
:param created_at: 创建时间 :param created_at: 会话创建时间
:return: 格式化后的创建时间 :return: 格式化后的会话创建时间
""" """
match (datetime.now().date() - created_at.date()).days: match (datetime.now().date() - created_at.date()).days:
case 0: case 0:
@ -137,17 +142,50 @@ class ConversationState(rx.State):
ConversationHistoryItem( ConversationHistoryItem(
id=conversation.id, id=conversation.id,
description=conversation.description, description=conversation.description,
created_at=self.format_created_at( created_at=self.format_conversation_created_at(
created_at=conversation.created_at created_at=conversation.created_at
), ),
) )
) )
return items 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 @rx.event
async def create_conversation(self, form_data: Dict[str, Any]) -> None: async def create_conversation(self, form_data: Dict[str, Any]) -> None:
""" """
新建会话 建会话
:param form_data: 表单数据 :param form_data: 表单数据
:return: None :return: None
""" """
@ -167,58 +205,13 @@ class ConversationState(rx.State):
# 会话创建状态设置为未正在创建 # 会话创建状态设置为未正在创建
self.is_conversation_creating = False 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 @rx.event
def toggle_conversation_creating(self) -> None: def toggle_conversation_creating(self) -> None:
""" """
切换正在新建会话状态 切换会话创建状态
""" """
self.is_conversation_creating = not self.is_conversation_creating 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 @rx.var
def running_status(self) -> bool: def running_status(self) -> bool:
""" """
@ -231,6 +224,18 @@ class ConversationState(rx.State):
return False return False
return conversation.is_running 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 @rx.event
async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]: async def run(self, form_data: dict[str, Any]) -> AsyncGenerator[None]:
""" """
@ -238,41 +243,39 @@ class ConversationState(rx.State):
:param form_data: 表单数据 :param form_data: 表单数据
:return: AsyncGenerator :return: AsyncGenerator
""" """
# 获取问题 # 解析用户提示词
question = form_data.get("question", "").strip() user_prompt = form_data["user_prompt"].strip()
if not question: if not user_prompt:
return return
# 当前会话 # 当前会话
conversation = self.conversations.get(self.conversation_id) conversation = self.conversations[self.conversation_id]
if not conversation:
return
# 将当前会话的运行状态设置为正在运行 # 将当前会话的运行状态设置为正在运行
conversation.is_running = True conversation.is_running = True
yield # 通知前端渲染
# 获取数据库状态 # 获取数据库状态
database_state = await self.get_state(DatabaseState) 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 conversation_id=self.conversation_id
) )
# 先创建对话记录再在当前对话字典中新增对话 # 创建对话记录再添加对话实例
dialog_id = await database_state.create_dialogs_record( conversation.dialogs.update(
conversation_id=self.conversation_id, question=question 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] = {} tool_call_ids: Dict[str, int] = {}
yield # 通知前端渲染
async with agent.run_stream_events( async with agent.run_stream_events(
conversation_id=self.conversation_id, conversation_id=self.conversation_id,
user_prompt=question, user_prompt=user_prompt,
message_history=message_history, message_history=message_history,
) as events: ) as events:
async for event in events: async for event in events:
@ -286,51 +289,45 @@ class ConversationState(rx.State):
match part: match part:
# 思考分片开始事件 # 思考分片开始事件
case ThinkingPart(content=content): case ThinkingPart(content=content):
# 若上一分片种类为空则将思考状态设置为正在思考、思考折叠面板展开状态设置为展开 # 若上一分片种类为空则将正在思考设置为 True、思考折叠面板展开状态设置为展开
if not previous_part_kind: if not previous_part_kind:
dialog.is_thinking = True dialog.is_thinking = True
dialog.is_collapse_expanded = True dialog.is_expanded = True
dialog.thoughts[index] = Thought(
dialog.thought_nodes[index] = ThoughtNode( type="thinking", content=content
kind="thinking", content=content
) )
yield
# 工具检索分片开始事件 # 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id): case ToolSearchCallPart(tool_call_id=tool_call_id):
# 创建工具调用唯一标识与片段索引映射 # 创建工具调用唯一标识与片段索引映射
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
dialog.thought_nodes[index] = ThoughtNode( dialog.thoughts[index] = Thought(
kind="tool_search", type="tool_search",
content="正在生成检索关键词", content="正在生成检索关键词",
) )
yield
# 能力加载分片开始事件 # 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id): case LoadCapabilityCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
dialog.thought_nodes[index] = ThoughtNode( dialog.thoughts[index] = Thought(
kind="capability_load", type="capability_load",
content="正在生成加载参数", content="正在生成加载参数",
) )
yield
# 工具调用分片开始事件 # 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id): case ToolCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index tool_call_ids[tool_call_id] = index
dialog.thought_nodes[index] = ThoughtNode( dialog.thoughts[index] = Thought(
kind="tool_call", type="tool_call",
content="正在生成调用参数", content="正在生成调用参数",
) )
yield
# 文本分片开始事件 # 文本分片开始事件
case TextPart(content=content): case TextPart(content=content):
dialog.answer = content dialog.result_output = content
yield
# ========== 增量事件 ========== # ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta): case PartDeltaEvent(index=index, delta=delta):
@ -339,17 +336,13 @@ class ConversationState(rx.State):
case ThinkingPartDelta( case ThinkingPartDelta(
content_delta=content_delta, content_delta=content_delta,
): ):
dialog.thought_nodes[index].content += ( dialog.thoughts[index].content += content_delta or ""
content_delta or ""
)
yield
# 文本分片增量事件 # 文本分片增量事件
case TextPartDelta( case TextPartDelta(
content_delta=content_delta, content_delta=content_delta,
): ):
dialog.answer += content_delta dialog.result_output += content_delta
yield
# ========== 结束事件 ========== # ========== 结束事件 ==========
case PartEndEvent( case PartEndEvent(
@ -363,32 +356,28 @@ class ConversationState(rx.State):
# 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠 # 若下一分片种类为文本则将思考状态设置为思考完成、思考面板展开状态设置为折叠
if next_part_kind == "text": if next_part_kind == "text":
dialog.is_thinking = False dialog.is_thinking = False
dialog.is_collapse_expanded = False dialog.is_expanded = False
yield
# ========== 函数工具调用事件 ========== # ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part): case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
# 获取分片索引 # 获取分片索引
index = tool_call_ids[tool_call_id] index = tool_call_ids[tool_call_id]
match dialog.thought_nodes[index].kind: match dialog.thoughts[index].type:
# 工具检索 # 工具检索
case "tool_search": case "tool_search":
dialog.thought_nodes[index].content = "正在检索" dialog.thoughts[index].content = "正在检索"
yield
# 能力加载 # 能力加载
case "capability_load": case "capability_load":
dialog.thought_nodes[index].content = ( dialog.thoughts[index].content = (
f"正在加载能力 {part.tool_name}" f"正在加载能力 {part.tool_name}"
) )
yield
# 工具调用 # 工具调用
case "tool_call": case "tool_call":
dialog.thought_nodes[index].content = ( dialog.thoughts[index].content = (
f"正在调用工具 {part.tool_name}" f"正在调用工具 {part.tool_name}"
) )
yield
# ========== 函数工具结果事件 ========== # ========== 函数工具结果事件 ==========
case FunctionToolResultEvent( case FunctionToolResultEvent(
@ -396,41 +385,39 @@ class ConversationState(rx.State):
content=content, content=content,
): ):
index = tool_call_ids[tool_call_id] index = tool_call_ids[tool_call_id]
match dialog.thought_nodes[index].kind: match dialog.thoughts[index].type:
# 工具检索 # 工具检索
case "tool_search": case "tool_search":
dialog.thought_nodes[index].content = ( dialog.thoughts[index].content = (
content if isinstance(content, str) else "" content if isinstance(content, str) else ""
) # 暂仅考虑文本内容 ) # 暂仅考虑文本内容
yield
# 能力加载 # 能力加载
case "capability_load": case "capability_load":
dialog.thought_nodes[index].content = "已加载" dialog.thoughts[index].content = "已加载"
yield
# 工具调用 # 工具调用
case "tool_call": case "tool_call":
dialog.thought_nodes[index].content = f"已调用" dialog.thoughts[index].content = f"已调用"
yield
# ========== 智能体运行结果事件 ========== # ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result): case AgentRunResultEvent(result=result):
# 更新对话记录 # 补全对话记录
await database_state.update_dialog_record( await database_state.complete_dialog_record(
dialog_id=dialog_id, id=dialog.id,
thought_nodes=dialog.thought_nodes, thoughts=dialog.thoughts,
answer=dialog.answer, result_output=dialog.result_output,
) )
# 创建运行结果记录 # 创建结果记录
await database_state.create_run_result_record( await database_state.create_result_record(
conversation_id=self.conversation_id, conversation_id=self.conversation_id,
dialog_id=dialog_id, dialog_id=dialog.id,
new_messages=result.new_messages(), new_messages=result.new_messages(),
) )
# 将当前会话的运行状态设置为运行完成 # 将当前会话的运行状态设置为运行完成
conversation.is_running = False conversation.is_running = False
yield
yield
@rx.event @rx.event
def toggle_collapse(self, dialog_id: str) -> None: 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 = self.conversations[self.conversation_id].dialogs[dialog_id]
dialog.is_collapse_expanded = not dialog.is_collapse_expanded dialog.is_expanded = not dialog.is_expanded

View File

@ -112,8 +112,11 @@ class DialogRecord(SQLModel, table=True):
result_output: str = Field(default="", description="结果输出") result_output: str = Field(default="", description="结果输出")
# 运行结果表 class ResultRecord(SQLModel, table=True):
class RunResultRecord(SQLModel, table=True): """
结果记录
"""
conversation_id: str = Field(primary_key=True, description="会话唯一标识") conversation_id: str = Field(primary_key=True, description="会话唯一标识")
dialog_id: str = Field(primary_key=True, description="对话唯一标识") dialog_id: str = Field(primary_key=True, description="对话唯一标识")
new_messages: str = Field(description="新增消息") new_messages: str = Field(description="新增消息")
@ -228,15 +231,17 @@ class DatabaseState(rx.State):
) )
if not dialog_record: if not dialog_record:
continue continue
record.dialogs.append( record.dialogs.update(
Dialog( {
id=dialog_record.id, dialog_record.id: Dialog(
user_prompt=dialog_record.user_prompt, id=dialog_record.id,
thoughts=ThoughtTypeAdapter.validate_python( user_prompt=dialog_record.user_prompt,
dialog_record.thoughts thoughts=ThoughtTypeAdapter.validate_python(
), dialog_record.thoughts
result_output=dialog_record.result_output, ),
) result_output=dialog_record.result_output,
)
}
) )
return records 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: 指定会话唯一标识 :param conversation_id: 指定会话唯一标识
:return: None :return: None
""" """
@ -275,52 +280,54 @@ class DatabaseState(rx.State):
record.is_deleted = True record.is_deleted = True
await session.commit() 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 conversation_id: 会话唯一标识
:param question: 问题 :param user_prompt: 用户提示词
:return: 创建对话记录的唯一标识 :return: 创建对话记录的唯一标识
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = DialogRecord( record = DialogRecord(
conversation_id=conversation_id, conversation_id=conversation_id,
user_prompt=question, user_prompt=user_prompt,
) )
session.add(record) session.add(record)
await session.commit() await session.commit()
await session.refresh(record) 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, self,
dialog_id: str, id: str,
thoughts: List[Thought], thoughts: List[Thought],
result_output: str, result_output: str,
) -> None: ) -> None:
""" """
更新对话记录 补全对话记录
:param dialog_id: 对话唯一标识 :param id: 对话唯一标识
:param thoughts: 思考列表 :param thoughts: 思考列表
:param result_output: 结果输出 :param result_output: 结果输出
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
record = await session.get(DialogRecord, dialog_id) record = await session.get(DialogRecord, id) # 通过主键查询记录
if not record: if not record:
return return
record.thoughts = ThoughtTypeAdapter.dump_python(thoughts) record.thoughts = ThoughtTypeAdapter.dump_python(thoughts)
record.result_output = result_output record.result_output = result_output
await session.commit() await session.commit()
async def create_run_result_record( async def create_result_record(
self, self,
conversation_id: str, conversation_id: str,
dialog_id: str, dialog_id: str,
new_messages: List[ModelMessage], new_messages: List[ModelMessage],
) -> None: ) -> None:
""" """
创建运行结果记录 创建结果记录
:param conversation_id: 会话唯一标识 :param conversation_id: 会话唯一标识
:param dialog_id: 对话唯一标识 :param dialog_id: 对话唯一标识
:param new_messages: 新增消息 :param new_messages: 新增消息
@ -328,7 +335,7 @@ class DatabaseState(rx.State):
""" """
async with rx.asession() as session: async with rx.asession() as session:
session.add( session.add(
RunResultRecord( ResultRecord(
conversation_id=conversation_id, conversation_id=conversation_id,
dialog_id=dialog_id, dialog_id=dialog_id,
new_messages=ModelMessagesTypeAdapter.dump_json( new_messages=ModelMessagesTypeAdapter.dump_json(
@ -340,9 +347,7 @@ class DatabaseState(rx.State):
) )
await session.commit() await session.commit()
async def retrieve_message_history( async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
self, conversation_id: str
) -> List[ModelMessage]:
""" """
获取消息历史 获取消息历史
:param conversation_id: 会话唯一标识 :param conversation_id: 会话唯一标识
@ -351,9 +356,9 @@ class DatabaseState(rx.State):
records: List[ModelMessage] = [] records: List[ModelMessage] = []
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(RunResultRecord) select(ResultRecord)
.where(RunResultRecord.conversation_id == conversation_id) .where(ResultRecord.conversation_id == conversation_id)
.order_by(desc(RunResultRecord.dialog_id)) .order_by(desc(ResultRecord.dialog_id))
) )
for record in result.all(): for record in result.all():
records.extend( records.extend(

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB