基于reflex的智能体初步实现

This commit is contained in:
liubiren 2026-06-18 19:58:46 +08:00
parent 4e65dc783f
commit f07cf989ce
10 changed files with 174 additions and 179 deletions

View File

@ -1,3 +1,4 @@
{ {
"python.languageServer": "None" "python.languageServer": "None",
"python.analysis.extraPaths": ["${workspaceFolder}"]
} }

View File

@ -4,53 +4,52 @@
""" """
# 列举导入模块 # 列举导入模块
from asyncio import Queue, QueueEmpty, Task, Task, create_task, sleep
from pathlib import Path from pathlib import Path
import time import time
from typing import AsyncGenerator, List, Optional, Dict, Literal from typing import AsyncGenerator, Dict, List, Literal, Optional, Union
from uuid import uuid4 from uuid import uuid4
from pydantic_ai.messages import ModelMessage
from pydantic import Field
from pydantic import BaseModel
from pydantic_ai import Agent as PydanticAIAgent, ModelMessage from pydantic_ai import Agent as PydanticAIAgent, ModelMessage
from pydantic_ai.capabilities import AgentCapability from pydantic_ai.capabilities import AgentCapability
from asyncio import Task, create_task, sleep, Queue, QueueEmpty, Task from pydantic_ai.run import AgentRunResultEvent
from pydantic import Field
from pydantic import BaseModel
from pydantic_ai.messages import ( from pydantic_ai.messages import (
ModelMessagesTypeAdapter, ModelMessage,
ModelResponse,
FinalResultEvent, FinalResultEvent,
ModelMessageEvent, LoadCapabilityCallPart,
PartStartEvent, AgentStreamEvent,
ModelMessagesTypeAdapter,
NativeToolCallPart,
NativeToolReturnPart,
NativeToolSearchCallPart,
PartDeltaEvent, PartDeltaEvent,
PartEndEvent, PartEndEvent,
PartStartEvent,
TextPart, TextPart,
ToolSearchCallPart,
NativeToolSearchCallPart,
LoadCapabilityCallPart,
ThinkingPart,
ToolCallPart,
NativeToolCallPart,
TextPartDelta, TextPartDelta,
ThinkingPart,
ThinkingPartDelta, ThinkingPartDelta,
ModelResponseStreamEvent, ToolCallPartDelta,
ToolCallPart,
ToolReturnPart, ToolReturnPart,
AgentRunResultEvent, ToolSearchCallPart,
NativeToolReturnPart,
) )
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.output import OutputSpec from pydantic_ai.output import OutputSpec
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
from .sqlite import SQLite from sys import path
from pathlib import Path
path.append(Path(__file__).parent.as_posix())
from sqlite import SQLite
class AgentMemory(SQLite): class Memory(SQLite):
""" """
智能体记忆体支持 记忆体
create新增对话消息
read查询会话历史消息
""" """
def __init__(self): def __init__(self):
@ -58,7 +57,7 @@ class AgentMemory(SQLite):
初始化 初始化
""" """
# 构建数据库路径 # 构建数据库路径
super().__init__(database=Path(__file__).parent.resolve() / "agent_memory.db") super().__init__(database=Path(__file__).parent.resolve() / "memory.db")
try: try:
with self: with self:
@ -80,14 +79,12 @@ class AgentMemory(SQLite):
except Exception as exception: except Exception as exception:
raise RuntimeError(f"初始化数据库发生异常:{str(exception)}") from exception raise RuntimeError(f"初始化数据库发生异常:{str(exception)}") from exception
def create_new_messages( def save(self, session_id: str, new_messages: List[ModelMessage]) -> bool:
self, session_id: str, new_messages: List[ModelMessage]
) -> bool:
""" """
新增新对话消息 将本次对话消息保存至数据库
:param session_id: 会话唯一标识 :param session_id: 会话唯一标识
:param new_messages: 新对话消息 :param new_messages: 新对话消息
:return: 新增是否成功 :return: 保存是否成功
""" """
try: try:
with self: with self:
@ -107,7 +104,7 @@ class AgentMemory(SQLite):
def get_message_history(self, session_id: str) -> List[ModelMessage]: def get_message_history(self, session_id: str) -> List[ModelMessage]:
""" """
获取指定会话的消息历史 获取消息历史
:param session_id: 会话唯一标识 :param session_id: 会话唯一标识
:return: 消息历史 :return: 消息历史
""" """
@ -142,13 +139,13 @@ class Buffer(BaseModel):
part_type: Literal["thinking", "text"] = Field(..., description="片段类型") part_type: Literal["thinking", "text"] = Field(..., description="片段类型")
content_delta: str = Field(default="", description="片段内容增量") content_delta: str = Field(default="", description="片段内容增量")
task: Optional[Task] = Field(default=None, description="延迟刷新异步协程任务") task: Optional[Task] = Field(default=None, description="延迟刷新异步协程任务")
model_config = {"arbitrary_types_allowed": True}
class EventsDebouncer: class Debouncer:
""" """
模型消息事件防抖器 防抖器
使用 Pydantic-AI run_stream_events 方法时需就其返回的模型消息事件处理延迟刷新片段类型为思考或文本的片段增量事件以实现防抖 用于处理 run_stream_events 返回的模型消息事件
Pydantic-AI 中模型流式输出包含若干类型片段例如思考片段文本片段等每类型片段包含若干模型消息事件例如片段开始事件片段增量事件片段结束事件等
""" """
def __init__(self, delay: float = 0.2): def __init__(self, delay: float = 0.2):
@ -164,10 +161,11 @@ class EventsDebouncer:
self.pending_flush_queue = Queue() self.pending_flush_queue = Queue()
async def handle_event( async def handle_event(
self, event: ModelMessageEvent self, event: Union[AgentStreamEvent, AgentRunResultEvent]
) -> AsyncGenerator[ModelMessageEvent]: ) -> AsyncGenerator[Union[AgentStreamEvent, AgentRunResultEvent]]:
""" """
处理模型消息事件 处理模型消息事件
Pydantic-AI 中模型流式输出包含若干类型片段例如思考片段文本片段等每类型片段包含若干模型消息事件例如片段开始事件片段增量事件片段结束事件等
:param event: 模型消息事件 :param event: 模型消息事件
:yield: AsyncGenerator :yield: AsyncGenerator
""" """
@ -294,20 +292,22 @@ class Agent:
instructions: str, instructions: str,
output_type: OutputSpec = str, output_type: OutputSpec = str,
capabilities: Optional[List[AgentCapability]] = None, capabilities: Optional[List[AgentCapability]] = None,
retries: int = 1,
): ):
""" """
初始化智能体 初始化智能体
:param session_id: 会话唯一标识 :param session_id: 会话唯一标识
:param instructions: 指令 :param instructions: 指令
:param skills: 智能体技能列表默认为不使用技能 :param capabilities: 技能列表默认为不使用技能
:param output_type: 输出类型 :param output_type: 输出类型
:param retries: 重试次数默认为1次
:return: 智能体实例 :return: 智能体实例
""" """
# 会话唯一标识 # 会话唯一标识
self.session_id = session_id self.session_id = session_id
# 实例智能体的记忆体 # 实例智能体的记忆体
self.agent_memory = AgentMemory() self.memory = Memory()
# 实例智能体 # 实例智能体
self.agent = PydanticAIAgent( self.agent = PydanticAIAgent(
@ -321,7 +321,7 @@ class Agent:
instructions=instructions, instructions=instructions,
capabilities=capabilities, capabilities=capabilities,
output_type=output_type, output_type=output_type,
retries=1, retries=retries,
) )
self.agent.to_web() self.agent.to_web()
@ -337,62 +337,61 @@ class Agent:
""" """
"""定义一次会话session包含若干论对话turn每一轮对话由用户输入消息message和智能体输出消息组成""" """定义一次会话session包含若干论对话turn每一轮对话由用户输入消息message和智能体输出消息组成"""
# 获取指定会话的消息历史 # 获取指定会话的消息历史
message_history = self.agent_memory.get_message_history( message_history = self.memory.get_message_history(session_id=self.session_id)
session_id=self.session_id
)
# 实例模型消息事件防抖器 # 实例防抖器
event_debouncer = EventsDebouncer(delay=delay) debouncer = Debouncer(delay=delay)
async with self.agent.run_stream_events( async with self.agent.run_stream_events(
user_prompt=user_prompt, 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:
if isinstance(event, AgentRunResultEvent):
evnet : AgentRunResultEvent = event
new_messages = evnet.result
# 处理模型消息事件 # 处理模型消息事件
async for event_ in event_debouncer.handle_event(event): async for event in debouncer.handle_event(event):
match event_: # event_ 为处理后模型消息事件 match event:
case PartStartEvent(): # 片段开始事件
part = event_.part case PartStartEvent(part=part):
match part: match part:
case ThinkingPart(content=content): case ThinkingPart(content=content):
yield f"00:{content}" yield f"00:{content}"
case TextPart(content=content): case TextPart(content=content):
yield f"01:{content}" yield f"01:{content}"
case ( case (
NativeToolSearchCallPart() NativeToolSearchCallPart(tool_name=tool_name)
| NativeToolCallPart() | NativeToolCallPart(tool_name=tool_name)
| ToolSearchCallPart() | ToolSearchCallPart(tool_name=tool_name)
| ToolCallPart() | ToolCallPart(tool_name=tool_name)
| LoadCapabilityCallPart() | LoadCapabilityCallPart(tool_name=tool_name)
) as tool: ):
yield f"02:使用技能:{tool.tool_name}" yield f"02:正在使用技能:{tool_name}"
case ( case NativeToolReturnPart(
NativeToolReturnPart() | ToolReturnPart() content=content
) as tool_return: ) | ToolReturnPart(content=content):
yield f"03:技能返回:{tool_return.content}" yield f"04:技能返回:{content}"
case _: case _:
yield f"99:未知片段类型{type(part)}" yield f"06:未知片段类型{type(part).__name__}"
case PartDeltaEvent(): # 片段增量事件
delta = event_.delta case PartDeltaEvent(delta=delta):
match delta: match delta:
case ThinkingPartDelta(content_delta=content_delta): case ThinkingPartDelta(content_delta=content_delta):
yield f"00:{content_delta}" yield f"00:{content_delta}"
case TextPartDelta(content_delta=content_delta): case TextPartDelta(content_delta=content_delta):
yield f"01:{content_delta}" yield f"01:{content_delta}"
case ToolCallPartDelta(args_delta=args_delta):
yield f"03:技能参数:{args_delta}"
case _: case _:
yield f"99:未知片段类型{type(delta)}" yield f"06:未知片段类型{type(delta).__name__}"
# 片段结束事件无需处理 # 片段结束事件、最终结果事件无需处理
case PartEndEvent() | AgentRunResultEvent(): case PartEndEvent():
pass continue
case _: case FinalResultEvent():
yield f"99:未知事件类型{type(event_)}" continue
case AgentRunResultEvent(result=result):
self.agent_memory.create_new_messages( self.memory.save(
session_id=self.session_id, session_id=self.session_id,
new_messages=new_messages, new_messages=result.new_messages(),
) )
yield "05:FinalResultEvent"
case _:
yield f"06:未知事件类型{type(event).__name__}"

Binary file not shown.

BIN
utils/memory.db Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@
""" """
import reflex import reflex
from .pages.index import index from .page.index import index
app = reflex.App() app = reflex.App()
app.add_page(index) app.add_page(index)

View File

@ -3,22 +3,24 @@
应用状态管理模块 应用状态管理模块
""" """
from typing import Any, AsyncGenerator, Dict, List, Literal
from uuid import uuid4
from enum import StrEnum from enum import StrEnum
from uuid import uuid4
from typing import Any, AsyncGenerator, Dict, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import reflex import reflex
from pathlib import Path
from sys import path
path.append((Path(__file__).resolve().parent.parent.parent.as_posix())) from sys import path
from pathlib import Path
path.append(Path(__file__).parent.parent.parent.parent.as_posix())
from utils.agent import Agent from utils.agent import Agent
# 所有会话绑定的智能体 # 所有会话绑定的智能体
agents: Dict[str, Any] = {} agents: Dict[str, Agent] = {}
def retrieve_agent(state) -> Agent: def get_current_session_agent(state) -> Agent:
""" """
获取当前会话绑定的智能体 获取当前会话绑定的智能体
:return: 当前会话绑定的智能体 :return: 当前会话绑定的智能体
@ -26,58 +28,61 @@ def retrieve_agent(state) -> Agent:
current_session_name = state.current_session_name current_session_name = state.current_session_name
if current_session_name not in agents: if current_session_name not in agents:
agents[current_session_name] = Agent( agents[current_session_name] = Agent(
session_id=state.sessions[current_session_name].id, session_id=uuid4().hex,
instructions="You are a friendly chatbot", instructions="You are a friendly chatbot",
) )
return agents[current_session_name] return agents[current_session_name]
# 消息块类型 class MessageType(StrEnum):
class MessageBlockType(StrEnum): """消息类型类"""
content = "content" THINKING = "thinking"
thinking = "thinking" TEXT = "text"
tool_call = "tool_call" CALL = "call"
tool_result = "tool_result" TOOL_ARGS = "tool_args"
error = "error" TOOL_RETURN = "tool_return"
RESULT = "result"
ERROR = "error"
# 消息类型前缀映射 # 消息类型前缀映射
MESSAGE_BLOCK_TYPE_PREFIX_MAP = {f"{i:02d}:": m for i, m in enumerate(MessageBlockType)} MESSAGE_TYPE_PREFIX_MAP = {f"{i:02d}:": mt for i, mt in enumerate(MessageType)}
class MessageBlock(BaseModel): # 会话、对话、消息关系:一次会话包含若干轮对话,每轮对话包含若干条消息
"""消息块数据模型,包含类型和内容""" class Message(BaseModel):
"""消息类"""
type: MessageBlockType = Field(..., description="类型") id: str = Field(default_factory=lambda: uuid4().hex, description="消息唯一标识")
type_: MessageType = Field(..., description="类型")
content: str = Field(default="", description="内容") content: str = Field(default="", description="内容")
class Turn(BaseModel): class Turn(BaseModel):
"""对话数据模型,包含用户输入消息和智能体输出消息""" """对话"""
input: str = Field(..., description="用户输入的消息") id: str = Field(default_factory=lambda: uuid4().hex, description="对话唯一标识")
output: List[MessageBlock] = Field( input_: str = Field(..., description="输入消息")
default_factory=list, description="智能体输出的消息" output: List[Message] = Field(default_factory=list, description="输出消息")
)
class Session(BaseModel): class Session(BaseModel):
"""会话数据模型,包含会话唯一标识和会话对话列表""" """会话"""
id: str = Field(default_factory=lambda: uuid4().hex, description="会话唯一标识") is_processing: bool = Field(
is_processing: bool = Field(default=False, description="会话是否正在处理中") default=False,
turns: List[Turn] = Field(default_factory=list, description="会话对话列表") description="会话状态True 表示正在处理中False 表示未处理或处理完成",
)
turns: List[Turn] = Field(default_factory=list, description="对话列表")
# Reflex.State 统一管理应用数据与功能状态,作为前后端交互枢纽,借助响应式特性实现页面自动更新
class State(reflex.State): class State(reflex.State):
"""应用状态""" """统一管理应用数据与功能状态,作为前后端交互枢纽,借助响应式特性实现页面自动更新"""
# 当前会话名称 # 当前会话名称
current_session_name: str = "NewSession" current_session_name: str = "新会话"
# 会话列表(会话名称作为会话对象的唯一标识,不允许重复)
# 所有会话
sessions: Dict[str, Session] = {current_session_name: Session()} sessions: Dict[str, Session] = {current_session_name: Session()}
# 新建会话模态窗是否打开 # 新建会话模态窗是否打开
@ -86,8 +91,8 @@ class State(reflex.State):
@reflex.var @reflex.var
def get_session_names(self) -> List[str]: def get_session_names(self) -> List[str]:
""" """
获取所有会话名称 获取会话名称列表
:return: 所有会话名称 :return: 会话名称列表
""" """
return list(self.sessions) return list(self.sessions)
@ -104,7 +109,7 @@ class State(reflex.State):
def get_current_session_status(self) -> bool: def get_current_session_status(self) -> bool:
""" """
获取当前会话状态 获取当前会话状态
:return: 当前会话状态其中 True 表示正在处理中False 表示处理完成 :return: 当前会话状态
""" """
if self.current_session_name not in self.sessions: if self.current_session_name not in self.sessions:
return False return False
@ -114,7 +119,7 @@ class State(reflex.State):
def get_current_session_turns(self) -> List[Turn]: def get_current_session_turns(self) -> List[Turn]:
""" """
获取当前会话对话列表 获取当前会话对话列表
:return: 当前会话对话列表 :return: 对话列表
""" """
if self.current_session_name not in self.sessions: if self.current_session_name not in self.sessions:
return [] return []
@ -123,19 +128,19 @@ class State(reflex.State):
@reflex.event @reflex.event
def create_session(self, form_data: Dict[str, Any]) -> None: def create_session(self, form_data: Dict[str, Any]) -> None:
""" """
建会话 建会话
:param form_data: 建会话表单数据 :param form_data: 建会话表单数据
:return: None :return: None
""" """
session_name = form_data["session_name"].strip() session_name = form_data["session_name"].strip()
# 若创建会话名称为空则默认使用"NewSession"作为会话名称 # 若新建会话名称为空则默认使用"新会话"作为会话名称
if not session_name: if not session_name:
session_name = "NewSession" session_name = "新会话"
original_session_name = session_name original_session_name = session_name
counter = 1 counter = 1
# 若会话名称重复则在会话名称后面添加号至不重复 # 若会话名称重复则在会话名称后面添加号至不重复
while session_name in self.sessions: while session_name in self.sessions:
session_name = f"{original_session_name}({counter})" session_name = f"{original_session_name}({counter})"
counter += 1 counter += 1
@ -155,14 +160,13 @@ class State(reflex.State):
""" """
if session_name not in self.sessions: if session_name not in self.sessions:
return return
del self.sessions[session_name] del self.sessions[session_name]
# 若删除会话后所有会话为空则默认创建空白会话 # 若会话列表为空则新建会话(区别于 create_session 方法,此处为后台新建会话)
if not self.sessions: if not self.sessions:
self.sessions["NewSession"] = Session() self.sessions["新会话"] = Session()
# 删除会话后,若当前会话名称不存在则默认使用第一个会话名称 # 若当前会话名称不存在则默认使用第一个会话名称
if self.current_session_name not in self.sessions: if self.current_session_name not in self.sessions:
self.current_session_name = next(iter(self.sessions)) self.current_session_name = next(iter(self.sessions))
@ -178,63 +182,52 @@ class State(reflex.State):
@reflex.event @reflex.event
async def adapt_input(self, form_data: dict[str, Any]) -> AsyncGenerator: async def adapt_input(self, form_data: dict[str, Any]) -> AsyncGenerator:
""" """
适配用户输入 适配输入
:param form_data: 对话表单数据 :param form_data: 输入栏组件的表单数据
:return: AsyncGenerator :return: AsyncGenerator
""" """
input = form_data["input"].strip() input_ = form_data["input"].strip()
if not input: if not input_:
return return
async for value in self.process_input(input=input):
yield value
async def process_input(self, input: str) -> AsyncGenerator:
"""
处理用户输入
:param input: 用户输入
:return: AsyncGenerator
"""
# 当前会话 # 当前会话
current_session = self.sessions[self.current_session_name] current_session = self.sessions[self.current_session_name]
# 当前会话正在处理 # 当前会话正在处理
current_session.is_processing = True current_session.is_processing = True
# 将用户输入添加到当前会话对话列表 # 将输入添加到当前会话对话列表
current_session.turns.append( current_session.turns.append(
Turn( Turn(
input=input, input_=input_,
) )
) )
yield # 通知前端更新状态(显示用户输入) yield # 通知前端渲染输入消息
# 当前对话 # 当前对话
current_turn = current_session.turns[-1] current_turn = current_session.turns[-1]
# 获取当前会话绑定的智能体 # 获取当前会话绑定的智能体
agent = retrieve_agent(self) agent = get_current_session_agent(self)
async for chunk in agent.output_message_streamed(user_prompt=input): async for event in agent.stream_messages_events(user_prompt=input_):
# 跳过空分块 # 跳过空事件
if not chunk: if not event:
yield
continue continue
# 匹配消息类型 # 匹配消息类型
prefix_matched = next( prefix = next(
(t for t in MESSAGE_BLOCK_TYPE_PREFIX_MAP if chunk.startswith(t)), None (t for t in MESSAGE_TYPE_PREFIX_MAP if event.startswith(t)), None
) )
# 跳过未匹配分块 # 跳过未匹配事件
if not prefix_matched: if not prefix:
yield
continue continue
# 消息类型 # 消息类型
type = MESSAGE_BLOCK_TYPE_PREFIX_MAP[prefix_matched] type_ = MESSAGE_TYPE_PREFIX_MAP[prefix]
# 若当前对话输出为空或当前消息类型和上一个消息类型不一致则创建消息 # 若当前对话输出为空或当前消息类型和上一个消息类型不一致则创建消息
if not current_turn.output or current_turn.output[-1].type != type: if not current_turn.output or current_turn.output[-1].type_ != type_:
current_turn.output.append(MessageBlock(type=type)) current_turn.output.append(Message(type_=type_))
current_turn.output[-1].content += chunk.removeprefix(prefix_matched) current_turn.output[-1].content += event.removeprefix(prefix)
yield # 通知前端更新状态(打字机效果显示输出) yield # 通知前端渲染输出消息
# 当前会话处理完成 # 当前会话处理完成
current_session.is_processing = False current_session.is_processing = False

View File

@ -5,7 +5,7 @@
import reflex import reflex
from ..state import State from ..backend.state import State
def create_session_modal(trigger) -> reflex.Component: def create_session_modal(trigger) -> reflex.Component:

View File

@ -5,12 +5,12 @@
import reflex import reflex
from reflex.constants.colors import ColorType from reflex.constants.colors import ColorType
from ..state import State, MessageBlockType, MessageBlock, Turn from ..backend.state import State, MessageType, Message, Turn
def input_bubble(message: str, color: ColorType) -> reflex.Component: def input_bubble(message: str, color: ColorType) -> reflex.Component:
""" """
输入气泡组件 输入消息气泡组件
:param message: 消息 :param message: 消息
:param color: 颜色 :param color: 颜色
:return: Component :return: Component
@ -25,26 +25,26 @@ def input_bubble(message: str, color: ColorType) -> reflex.Component:
) )
def output_bubble(message_block: MessageBlock) -> reflex.Component: def output_bubble(message: Message) -> reflex.Component:
""" """
输出气泡组件 输出消息气泡组件
:param message_block: 消息 :param message: 消息
:return: 气泡组件 :return: 气泡组件
""" """
color = reflex.cond( color = reflex.cond(
message_block.type == MessageBlockType.content, message.type_ == MessageType.TEXT,
"accent", "accent",
reflex.cond( reflex.cond(
message_block.type == MessageBlockType.thinking, message.type_ == MessageType.THINKING,
"iris", "iris",
reflex.cond( reflex.cond(
message_block.type == MessageBlockType.tool_call, message.type_ == MessageType.CALL,
"orange", "orange",
reflex.cond( reflex.cond(
message_block.type == MessageBlockType.tool_result, message.type_ == MessageType.TOOL_RETURN,
"teal", "teal",
reflex.cond( reflex.cond(
message_block.type == MessageBlockType.error, message.type_ == MessageType.ERROR,
"red", "red",
"mauve", # 兜底 "mauve", # 兜底
), ),
@ -53,7 +53,7 @@ def output_bubble(message_block: MessageBlock) -> reflex.Component:
), ),
) )
return reflex.markdown( return reflex.markdown(
message_block.content, message.content,
color=reflex.color(color=color, shade=12), color=reflex.color(color=color, shade=12),
background_color=reflex.color(color=color, shade=4), background_color=reflex.color(color=color, shade=4),
display="inline-block", display="inline-block",
@ -61,18 +61,19 @@ def output_bubble(message_block: MessageBlock) -> reflex.Component:
padding_block="0.5em", padding_block="0.5em",
border_radius="8px", border_radius="8px",
margin_bottom="4px", margin_bottom="4px",
key=message.id,
) )
def turn(turn: Turn) -> reflex.Component: def turn(turn: Turn) -> reflex.Component:
""" """
对话组件 一轮对话组件
:param turn: 对话 :param turn: 对话
:return: Component :return: Component
""" """
return reflex.box( return reflex.box(
reflex.box( reflex.box(
input_bubble(message=turn.input, color="mauve"), input_bubble(message=turn.input_, color="mauve"),
text_align="right", text_align="right",
margin_bottom="8px", margin_bottom="8px",
), ),
@ -83,6 +84,7 @@ def turn(turn: Turn) -> reflex.Component:
), ),
max_width="50em", max_width="50em",
margin_inline="auto", margin_inline="auto",
key=turn.id,
) )

View File

@ -5,8 +5,8 @@
import reflex import reflex
from ..components.navigation_bar import navigation_bar from ..frontend.navigation_bar import navigation_bar
from ..components.session import session_area, input_bar from ..frontend.session import session_area, input_bar
def index() -> reflex.Component: def index() -> reflex.Component: