From 9e0c58743adeaca3fd8be705c1470cfae03dd61d Mon Sep 17 00:00:00 2001 From: liubiren Date: Tue, 11 Aug 2026 20:35:46 +0800 Subject: [PATCH] 1 --- .../{0d3f238840fe_.py => d52f1ee0f9c4_.py} | 46 ++-- agent/application/pages/conversation.py | 251 +++++++++--------- agent/application/states/auth.py | 9 +- agent/application/states/conversation.py | 76 +++--- agent/application/states/database.py | 37 ++- agent/application/states/models.py | 75 +----- agent/application/workshop/book_flight.py | 164 ++++++------ agent/application/workshop/models.py | 2 +- agent/assets/styles.css | 6 +- agent/database.db | Bin 131072 -> 135168 bytes agent/reflex.lock/bun.lock | 2 +- 11 files changed, 322 insertions(+), 346 deletions(-) rename agent/alembic/versions/{0d3f238840fe_.py => d52f1ee0f9c4_.py} (69%) diff --git a/agent/alembic/versions/0d3f238840fe_.py b/agent/alembic/versions/d52f1ee0f9c4_.py similarity index 69% rename from agent/alembic/versions/0d3f238840fe_.py rename to agent/alembic/versions/d52f1ee0f9c4_.py index 56c1673..02dfbd4 100644 --- a/agent/alembic/versions/0d3f238840fe_.py +++ b/agent/alembic/versions/d52f1ee0f9c4_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 0d3f238840fe +Revision ID: d52f1ee0f9c4 Revises: -Create Date: 2026-08-06 13:35:59.116161 +Create Date: 2026-08-11 14:59:37.246745 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ import sqlalchemy as sa import sqlmodel # revision identifiers, used by Alembic. -revision: str = '0d3f238840fe' +revision: str = 'd52f1ee0f9c4' down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -23,11 +23,11 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('captcharecord', sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('captcha', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('is_valid', sa.Boolean(), nullable=False), sa.Column('is_verified', sa.Boolean(), nullable=False), sa.Column('expired_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('email', 'captcha', 'expired_at') + sa.PrimaryKeyConstraint('email', 'code', 'expired_at') ) with op.batch_alter_table('captcharecord', schema=None) as batch_op: batch_op.create_index(batch_op.f('ix_captcharecord_is_valid'), ['is_valid'], unique=False) @@ -37,6 +37,7 @@ def upgrade() -> None: 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('usage', sa.JSON(), nullable=False), sa.Column('is_deleted', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('id') @@ -45,24 +46,26 @@ def upgrade() -> None: batch_op.create_index(batch_op.f('ix_conversationrecord_is_deleted'), ['is_deleted'], unique=False) batch_op.create_index(batch_op.f('ix_conversationrecord_user_id'), ['user_id'], unique=False) - op.create_table('dialogrecord', + op.create_table('messagerecord', sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('user_prompt', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('thoughts', sa.JSON(), nullable=False), - sa.Column('result_output', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('usage', sa.JSON(), nullable=False), + sa.Column('type', sa.Enum('USER_PROMPT', 'THINKING', 'TOOL_CALL', 'RESULT_OUTPUT', name='messagetype'), nullable=False), + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.PrimaryKeyConstraint('id') ) - with op.batch_alter_table('dialogrecord', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_dialogrecord_conversation_id'), ['conversation_id'], unique=False) + with op.batch_alter_table('messagerecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_messagerecord_conversation_id'), ['conversation_id'], unique=False) - op.create_table('resultrecord', + op.create_table('runrecord', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 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') + sa.Column('new_messages', sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint('id') ) + with op.batch_alter_table('runrecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_runrecord_conversation_id'), ['conversation_id'], unique=False) + op.create_table('userrecord', sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), @@ -81,11 +84,14 @@ def downgrade() -> None: batch_op.drop_index(batch_op.f('ix_userrecord_email')) op.drop_table('userrecord') - op.drop_table('resultrecord') - with op.batch_alter_table('dialogrecord', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_dialogrecord_conversation_id')) + with op.batch_alter_table('runrecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_runrecord_conversation_id')) - op.drop_table('dialogrecord') + op.drop_table('runrecord') + with op.batch_alter_table('messagerecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_messagerecord_conversation_id')) + + op.drop_table('messagerecord') with op.batch_alter_table('conversationrecord', schema=None) as batch_op: batch_op.drop_index(batch_op.f('ix_conversationrecord_user_id')) batch_op.drop_index(batch_op.f('ix_conversationrecord_is_deleted')) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index cef3424..f65697f 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -3,14 +3,14 @@ 会话页面 """ import reflex as rx -from typing import Tuple +from application.states import AuthState, ConversationState from application.states.models import ( + ConversationHistoryItem, MessageHistoryItem, MessageType, - ConversationHistoryItem, + WorkFlowType, ) -from application.states import ConversationState, AuthState def conversation_history_item( @@ -23,10 +23,9 @@ def conversation_history_item( """ # 高亮 - is_highlight = item.id in ( - ConversationState.is_conversation_history_item_popover_shown, - ConversationState.actived_conversation_id, - ) + is_highlight = ( + item.id == ConversationState.is_conversation_history_item_popover_shown + ) | (item.id == ConversationState.actived_conversation_id) return rx.list.item( rx.vstack( @@ -227,109 +226,86 @@ def conversation_history( ) -def guidance() -> rx.Component: +def use_guidance() -> rx.Component: """ - 引导 + 使用指引 :return: Component """ return rx.vstack( + # 品牌图标、名称和介绍 rx.vstack( - # 品牌图标、名称和介绍 + rx.hstack( + rx.image( + src="/logo.png", + width="64px", + height="64px", + ), + rx.text( + "棱镜球", + font_size="calc(var(--prismui-font-size-3) * 2)", + font_weight="var(--prismui-font-weight-3)", + letter_spacing="1px", + ), + align_items="center", + gap="8px", + ), rx.vstack( - rx.hstack( - rx.image( - src="/logo.png", - width="64px", - height="64px", - ), - rx.box( - "棱镜球", - font_size="calc(var(--prismui-font-size-3) * 2)", - font_weight="var(--prismui-font-weight-3)", - letter_spacing="1px", - ), - display="flex", - align_items="center", - gap="8px", - ), - rx.vstack( - rx.text( - "产品汪的棱镜球,精准捕捉各类业务需求", - width="100%", - text_align="center", - ), - rx.text( - "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", - width="100%", - text_align="center", - ), + rx.text( + "产品汪的棱镜球,精准捕捉各类业务需求", width="100%", - line_height="1.5", + text_align="center", ), - display="flex", - flex_direction="column", - width="100%", + rx.text( + "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", + width="100%", + text_align="center", + ), + line_height="1.5", + gap="4px", + ), + align_items="center", + gap="12px", + ), + # 演示案例 + rx.vstack( + rx.text( + "演示案例", + margin_bottom="16px", + line_height="24px", + font_size="var(--prismui-font-size-3)", + font_weight="var(--prismui-font-weight-3)", + ), + rx.hstack( + rx.box( + "预定航班", + on_click=lambda: ConversationState.init_work_flow( + WorkFlowType.BOOK_FLIGHT + ), + padding="10px 16px", + background_color="var(--prismui-background-color-6)", + border_radius="var(--prismui-border-radius-9)", + color="var(--prismui-color-10)", + cursor="pointer", + ), + flex_wrap="wrap", align_items="center", gap="12px", ), - # 演示案例 - rx.vstack( - rx.hstack( - rx.text( - "演示案例", - line_height="24px", - font_size="var(--prismui-font-size-3)", - font_weight="var(--prismui-font-weight-3)", - ), - display="flex", - justify_content="space-between", - align_items="center", - width="100%", - margin_bottom="16px", - ), - rx.hstack( - rx.box( - "生成产品需求文档", - on_click=ConversationState.generate_prd, - padding="10px 16px", - background_color="var(--prismui-background-color-6)", - border_radius="var(--prismui-border-radius-9)", - color="var(--prismui-color-10)", - cursor="pointer", - ), - display="flex", - flex_wrap="wrap", - align_items="center", - gap="12px", - ), - width="100%", - padding="24px", - background_color="var(--prismui-background-color-1)", - border_radius="var(--prismui-border-radius-7)", - ), - display="flex", - flex_direction="column", - align_items="center", - gap="24px", width="100%", - min_height="0", - margin="auto 0", - color="var(--prismui-color-text)", + padding="24px", + background_color="var(--prismui-background-color-1)", + border_radius="var(--prismui-border-radius-7)", ), - display="flex", flex="1", - flex_direction="column", - justify_content="flex-start", + justify_content="center", + align_items="center", gap="24px", width="100%", max_width="1200px", + height="100%", padding="0 12px", - overflow="auto", - style={ - "&::-webkit-scrollbar": { - "display": "none", - }, - }, + margin="auto", + color="var(--prismui-color-text)", ) @@ -415,13 +391,29 @@ def thinking(item: MessageHistoryItem) -> rx.Component: cursor="pointer", ), # 思考的内容 - rx.text( - item.content, - line_height="1.6", - font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", + rx.box( + rx.box( + rx.cond( + item.is_shown, + rx.text( + item.content, + width="100%", + margin="4px 0", + line_height="1.6", + font_size="var(--prismui-font-size-1)", + color="var(--prismui-color-3)", + ), + rx.fragment(), + ), + min_height="0", + ), + display="grid", + grid_template_rows=rx.cond(item.is_shown, "1fr", "0fr"), + width="100%", + opacity=rx.cond(item.is_shown, "1", "0"), + transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out", + overflow="hidden", ), - margin_bottom="8px", ) @@ -553,28 +545,11 @@ def message_history_item(item: MessageHistoryItem) -> rx.Component: """ return rx.vstack( - # 若等待流式输出则显示加载动效,否则根据消息类型渲染组件 - rx.cond( - ConversationState.awaiting_stream, - rx.box( - rx.box( - class_name="loadin", - flex_shrink="0", - ), - display="flex", - justify_content="center", - align_items="center", - width="32px", - height="18px", - margin_bottom="8px", - overflow="hidden", - ), - rx.match( - item.type, - (MessageType.USER_PROMPT, user_prompt(item)), - (MessageType.THINKING, thinking(item)), - (MessageType.RESULT_OUTPUT, result_output(item)), - ), + rx.match( + item.type, + (MessageType.USER_PROMPT, user_prompt(item)), + (MessageType.THINKING, thinking(item)), + (MessageType.RESULT_OUTPUT, result_output(item)), ), padding="0 16px", width="100%", @@ -587,15 +562,36 @@ def message_history() -> rx.Component: :return: Component """ return rx.box( - # 若消息历史为空则显示引导,否则渲染消息历史项 + # 若消息历史为空则显示使用指引,否则渲染消息历史项 rx.cond( ConversationState.message_history.length() == 0, - guidance(), + use_guidance(), rx.vstack( rx.auto_scroll( - rx.foreach( - ConversationState.message_history, - message_history_item, + rx.vstack( + rx.foreach( + ConversationState.message_history, + message_history_item, + ), + # 若等待流式输出则显示加载动效 + rx.cond( + ConversationState.awaiting_stream, + rx.box( + rx.box( + class_name="awaiting_stream", + flex_shrink="0", + ), + display="flex", + justify_content="flex‑start", + align_items="center", + width="100%", + padding="0 16px", + margin_bottom="8px", + overflow="hidden", + ), + rx.fragment(), + ), + width="100%", ), width="100%", padding="0 12px", @@ -616,6 +612,11 @@ def message_history() -> rx.Component: overflow_y="auto", ), ), + display="flex", + flex_direction="column", + width="100%", + height="100%", + min_height="0", ) diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 5fdd14b..a23d245 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -136,9 +136,10 @@ class AuthState(rx.State): 发送验证码(后台任务) :return: None """ - db_state = await self.get_db_state() async with self: + # 获取当前数据库状态 + db_state = await self.get_db_state() # 创建验证记录 captcha_code = await db_state.create_captcha_record(email=self.email) @@ -238,7 +239,7 @@ class AuthState(rx.State): self.is_logging_in = True - # 获取数据库状态 + # 获取当前数据库状态 db_state = await self.get_db_state() # 核验验证码 @@ -259,7 +260,7 @@ class AuthState(rx.State): self.user_id = user_id self.email = "" - self.captcha = "" + self.captcha_code = "" self.is_captcha_code_sent = False self.resend_captcha_code_countdown = 0 self.is_policies_agreed = True @@ -292,6 +293,6 @@ class AuthState(rx.State): conversation_state = await self.get_state(ConversationState) conversation_state.user_id = "" conversation_state.conversations = {} - conversation_state.conversation_id = "" + conversation_state.actived_conversation_id = "" conversation_state.is_conversation_history_shown = False self.user_id = "" diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 001ba8a..a9cdd0e 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -301,28 +301,30 @@ class ConversationState(rx.State): index_map_to_message_id: dict[int, str] = {} # 初始化工具调用唯一标识集合 tool_call_ids: set[str] = set() + + # 匹配工作流类型 + match conversation.work_flow: + # 预定航班 + case WorkFlowType.BOOK_FLIGHT: + from application.workshop.book_flight import run_stream_events + + # 非结构化对话 + case _: + from application.workshop.unstructured_dialogue import ( + run_stream_events, + ) + + # 消息列表 + messages: list[Message] = [] + # 运行并流式输出事件 + stream_events = run_stream_events( + user_prompt=user_prompt, + message_history=await db_state.get_message_history( + conversation_id=self.actived_conversation_id + ), + usage=usage_validate_python(dict(conversation.usage)), + ) try: - # 匹配工作流类型 - match conversation.work_flow: - # 预定航班 - case WorkFlowType.BOOK_FLIGHT: - from application.workshop.book_flight import run_stream_events - - # 非结构化对话 - case _: - from application.workshop.unstructured_dialogue import ( - run_stream_events, - ) - - # 运行并流式输出事件 - stream_events = run_stream_events( - user_prompt=user_prompt, - message_history=await db_state.get_message_history( - conversation_id=self.actived_conversation_id - ), - usage=usage_validate_python(conversation.usage), - ) - # 获取运行流式输出事件 async for event in stream_events: # 将等待流式输出设置为否 @@ -395,7 +397,15 @@ class ConversationState(rx.State): ] message.is_running = False message.title = "思考完成" + messages.append(message) + + case TextPart(content=content): + # 获取消息实例 + message = conversation.messages[ + index_map_to_message_id[index] + ] message.content = content + messages.append(message) # ========== 智能体运行结果事件 ========== case AgentRunResultEvent(result=result): @@ -409,15 +419,9 @@ class ConversationState(rx.State): new_messages=result.new_messages(), ) - if isinstance(message, Message): - # 强制更新会话并推送前端 - self.conversations[self.actived_conversation_id] = conversation - yield - # 创建对话记录 - await db_state.create_message_record( - conversation_id=conversation.id, - message=message, - ) + # 强制更新会话并推送前端 + self.conversations[self.actived_conversation_id] = conversation + yield except Exception as e: ... @@ -426,10 +430,15 @@ class ConversationState(rx.State): conversation.is_running = False # 将等待流式输出设置为否 conversation.awaiting_stream = False - self.conversations[self.actived_conversation_id] = conversation yield + # 批量创建消息记录 + await db_state.create_message_records( + conversation_id=conversation.id, + messages=messages, + ) + @rx.event async def init_work_flow(self, work_flow_type: WorkFlowType) -> None: """ @@ -441,13 +450,12 @@ class ConversationState(rx.State): return match work_flow_type: - # 预定航班 + # 预定航班工作流 case WorkFlowType.BOOK_FLIGHT: from application.workshop.book_flight import init_work_flow # 初始化预定航班任务 - conversation.work_flow = init_work_flow() - conversation.user_prompt = f"帮我找一班从 {conversation.work_flow.deps.origin} 到 {conversation.work_flow.deps.destination} 在 {conversation.work_flow.deps.date} 的航班" + conversation.work_flow = (work_flow := init_work_flow()) @rx.event def toggle_message_history_item_shown(self, message_id: str) -> None: diff --git a/agent/application/states/database.py b/agent/application/states/database.py index ea07715..8d4246c 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -5,21 +5,21 @@ from datetime import datetime, timedelta from random import choices from typing import Any + from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter, RunUsage from pydantic_ai._uuid import uuid7 import reflex as rx - from sqlmodel import Field, JSON, SQLModel, select, update from application.states.models import ( Conversation, - MessageType, Message, + MessageType, usage_dump_python, ) -class CaptchaRecord(SQLModel, table=True, table_name="captcha_code"): +class CaptchaRecord(SQLModel, table=True): """ 验证码记录 """ @@ -47,7 +47,7 @@ class CaptchaRecord(SQLModel, table=True, table_name="captcha_code"): ) -class UserRecord(SQLModel, table=True, table_name="user"): +class UserRecord(SQLModel, table=True): """ 用户记录 """ @@ -60,7 +60,7 @@ class UserRecord(SQLModel, table=True, table_name="user"): email: str = Field(..., index=True, description="邮箱") -class ConversationRecord(SQLModel, table=True, table_name="conversation"): +class ConversationRecord(SQLModel, table=True): """ 会话记录 """ @@ -85,7 +85,7 @@ class ConversationRecord(SQLModel, table=True, table_name="conversation"): ) -class MessageRecord(SQLModel, table=True, table_name="message"): +class MessageRecord(SQLModel, table=True): """ 消息记录 """ @@ -101,7 +101,7 @@ class MessageRecord(SQLModel, table=True, table_name="message"): content: str = Field(default="", description="消息内容") -class RunRecord(SQLModel, table=True, table_name="run"): +class RunRecord(SQLModel, table=True): """ 运行记录 """ @@ -310,6 +310,29 @@ class DatabaseState(rx.State): await session.refresh(record) return {message.id: message} + async def create_message_records( + self, + conversation_id: str, + messages: list[Message], + ) -> None: + """ + 创建消息记录 + :param conversation_id: 会话唯一标识 + :param message: 消息实例 + :return: 消息实例 + """ + async with rx.asession() as session: + for message in messages: + record = MessageRecord( + id=message.id, + conversation_id=conversation_id, + type=message.type, + title=message.title, + content=message.content, + ) + session.add(record) + await session.commit() + async def get_message_history(self, conversation_id: str) -> list[ModelMessage]: """ 获取消息历史列表 diff --git a/agent/application/states/models.py b/agent/application/states/models.py index f3b09b2..4edc7ec 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -2,9 +2,9 @@ """ 面向 reflex.state 的类 """ +from datetime import datetime from enum import StrEnum from typing import Any -from datetime import datetime from pydantic import BaseModel, Field, TypeAdapter from pydantic_ai import RunUsage @@ -40,30 +40,6 @@ class Message(BaseModel): ) -class RunStatus(StrEnum): - """ - 运行状态枚举 - """ - - RUNNING = "running" - FINISHED = "finished" - - -class Run(BaseModel): - """ - 运行类 - """ - - id: str = Field(default_factory=lambda: str(uuid7()), description="运行唯一标识") - messages: dict[str, Message] = Field(default_factory=dict, description="消息字典") - status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态") - usage: RunUsage = Field(default=RunUsage(), description="运行使用量") - usage_limits: UsageLimits | None = Field(default=None, description="运行使用量限制") - is_thinking: bool = Field( - default=False, description="正在思考,True 表示正在思考,False 表示未正在思考" - ) - - class WorkFlowType(StrEnum): """ 工作流类型枚举 @@ -77,7 +53,7 @@ class Deps(BaseModel): 依赖项类 """ - ... + pass class WorkFlow(BaseModel): @@ -86,7 +62,7 @@ class WorkFlow(BaseModel): """ type: WorkFlowType = Field(..., description="工作流类型") - deps: Deps | None = Field(default=None, description="工作流依赖项") + deps: Any = Field(default=None, description="工作流依赖项") usage: RunUsage = Field(default=RunUsage(), description="工作流使用量") usage_limits: UsageLimits | None = Field( default=None, @@ -109,7 +85,10 @@ class Conversation(BaseModel): is_running: bool = Field( default=False, description="正在运行,True 表示正在运行, False 表示运行结束" ) - awaiting_stream: bool = Field(default=False, description="等待流式输出,True 表示等待流式输出,False 表示已开始流式输出或已完成") + awaiting_stream: bool = Field( + default=False, + description="等待流式输出,True 表示等待流式输出,False 表示已开始流式输出或已完成", + ) class ConversationHistoryItem(BaseModel): @@ -139,28 +118,6 @@ class MessageHistoryItem(BaseModel): ) -# Dpes 适配器 -DepsAdapter = TypeAdapter(Deps) - - -def deps_to_object(deps: dict[str, Any]) -> Deps | None: - """ - Deps 转为对象 - """ - if not deps: - return None - return DepsAdapter.validate_python(deps) - - -def deps_to_dict(deps: Deps | None) -> dict[str, Any]: - """ - Deps 转为字典 - """ - if not deps: - return {} - return DepsAdapter.dump_python(deps) - - # Usage 适配器 UsageAdapter = TypeAdapter(RunUsage) @@ -185,21 +142,3 @@ def usage_dump_python(usage: RunUsage) -> dict[str, Any]: # UsageLimits 适配器 UsageLimitsAdapter = TypeAdapter(UsageLimits) - - -def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits | None: - """ - UsageLimits 转为对象 - """ - if not usage_limits: - return None - return UsageLimitsAdapter.validate_python(usage_limits) - - -def usage_limits_to_dict(usage_limits: UsageLimits | None) -> dict[str, Any]: - """ - UsageLimits 转为字典 - """ - if not usage_limits: - return {} - return UsageLimitsAdapter.dump_python(usage_limits) diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index 7028250..d862633 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -1,105 +1,48 @@ # -*- coding: utf-8 -*- """ -预定航班 +预定航班(范式) """ import datetime from typing import AsyncGenerator, Literal from pydantic import BaseModel, Field -from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage +from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage, DeferredToolRequests from pydantic_ai.run import AgentRunResultEvent from application.states.models import ( WorkFlow, WorkFlowType, - usage_limits_to_object, - usage_to_dict, - usage_to_object, - Deps, ) -from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS +from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS_DISABLED_THINKING -class Deps_(BaseModel): +class Dependences(BaseModel): """ - 依赖项类 + 依赖类 """ - flight_info: str = Field(..., description="所有航班信息") date: datetime.date = Field(..., description="航班日期") - origin: str = Field(..., description="出发机场") - destination: str = Field(..., description="到达机场") + origin: str = Field(..., description="航班出发机场") + destination: str = Field(..., description="航班到达机场") + source_material: str = Field(..., description="航班来源资料") - -class FlightDetail(BaseModel): +class Flight(BaseModel): """ - 航班详情类 + 航班类 """ - number: str = Field(description="航班号") - date: datetime.date = Field(description="航班日期") - origin: str = Field(description="出发机场") - destination: str = Field(description="到达机场") - price: int = Field(description="机票价格") + number: str = Field(..., description="航班号") + date: datetime.date = Field(..., description="航班日期") + origin: str = Field(..., description="航班出发机场") + destination: str = Field(..., description="航班到达机场") + airfare: int = Field(..., description="航班机票价格") -class NoFlightFound(BaseModel): +class FlightNoFound(BaseModel): """ 未查询到航班类 """ - -# 航班查询智能体 -flight_search_agent = Agent[Deps, FlightDetail | NoFlightFound]( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=MODEL_SETTINGS, - deps_type=Deps, - output_type=FlightDetail | NoFlightFound, - system_prompt="你的工作是在给定日期、出发机场和到达机场为用户找到最便宜的航班", -) - -# 所有航班详情提取智能体 -flight_details_extraction_agent = Agent( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=MODEL_SETTINGS, - output_type=list[FlightDetail], - system_prompt="从给定文本中提取所有航班详细,包括航班号、航班日期、出发机场、到达机场和机票价格", -) - - -@flight_search_agent.tool -async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]: - """ - 工具:提取所有航班详情 - """ - result = await flight_details_extraction_agent.run( - ctx.deps.flight_info, usage=ctx.usage - ) - return result.output - - -@flight_search_agent.output_validator -async def validate_output( - ctx: RunContext[Deps], output: FlightDetail | NoFlightFound -) -> FlightDetail | NoFlightFound: - """ - 输出校验:航班详情 - """ - if isinstance(output, NoFlightFound): - return output - - errors = [] - if output.date != ctx.deps.date: - errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}") - if output.origin != ctx.deps.origin: - errors.append(f"出发机场应为 {ctx.deps.origin}, 不是 {output.origin}") - if output.destination != ctx.deps.destination: - errors.append(f"到达机场应为 {ctx.deps.destination}, 不是 {output.destination}") - if errors: - raise ModelRetry("\n".join(errors)) - return output - - class SeatPreference(BaseModel): """ 座位偏好类 @@ -109,21 +52,77 @@ class SeatPreference(BaseModel): column: Literal["A", "B", "C", "D", "E", "F"] = Field(description="座位列") -class NoSeatExtracted(BaseModel): +class SeatPreferenceNoExtracted(BaseModel): """ 未提取到座位偏好类 """ -# 提取座位偏好智能体(无依赖项) -seat_preference_extraction_agent = Agent[object, SeatPreference | NoSeatExtracted]( +# 航班查询智能体 +flight_inquiry_agent = Agent[Dependences, Flight | FlightNoFound | DeferredToolRequests]( model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=MODEL_SETTINGS, - output_type=SeatPreference | NoSeatExtracted, - system_prompt="提取用户的座位偏好。座位规则说明:A 座、F 座为靠窗座位;第 1 排是前排座位,腿部空间更大;14 排、20 排同样拥有加宽腿部空间", + model_settings=MODEL_SETTINGS_DISABLED_THINKING, + deps_type=Dependences, + output_type=Flight | FlightNoFound | DeferredToolRequests, + system_prompt="你的任务是根据给定的日期、出发机场和到达机场帮助用户找到最便宜的航班。", ) -flight_info = """ +# 航班信息提取智能体 +flights_extraction_agent = Agent( + model=DEEPSEEK_V4_FLASH_MODEL, + model_settings=MODEL_SETTINGS_DISABLED_THINKING, + output_type=list[Flight], + system_prompt="你的任务是根据给定的航班来源资料提取出所有航班信息,包括航班号、航班日期、航班出发机场、航班到达机场和航班机票价格。禁止编造。") + + + +@flight_inquiry_agent.tool +async def extract_flight_details(ctx: RunContext[Dependences]) -> list[Flight]: + """ + 提取所有航班信息 + """ + result = await flights_extraction_agent.run( + ctx.deps.source_material, usage=ctx.usage + ) + return result.output + + +@flight_inquiry_agent.output_validator +async def validate_output( + ctx: RunContext[Dependences], output: Flight | FlightNoFound | DeferredToolRequests +) -> Flight | FlightNoFound | DeferredToolRequests: + """ + 输出校验:航班信息 + """ + # 不校验未查询到航班 + if isinstance(output, FlightNoFound): + return output + + # 不校验延迟工具请求 + if isinstance(output, DeferredToolRequests): + return output + + errors = [] + if output.date != ctx.deps.date: + errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}") + if output.origin != ctx.deps.origin: + errors.append(f"航班出发机场应为 {ctx.deps.origin}, 不是 {output.origin}") + if output.destination != ctx.deps.destination: + errors.append(f"航班到达机场应为 {ctx.deps.destination}, 不是 {output.destination}") + if errors: + raise ModelRetry("\n".join(errors)) + return output + + +# 座位偏好提取智能体 +seat_preference_extraction_agent = Agent[object, SeatPreference | SeatPreferenceNoExtracted]( + model=DEEPSEEK_V4_FLASH_MODEL, + model_settings=MODEL_SETTINGS_DISABLED_THINKING, + output_type=SeatPreference | SeatPreferenceNoExtracted, + system_prompt="你的任务是根据用户回答提取座位偏好。座位规则说明:A 座、F 座为靠窗座位;第 1 排是前排座位,腿部空间更大;14 排、20 排同样拥有加宽腿部空间", +) + +source_material = """ 1. Flight SFO-AK123 - Price: $350 - Origin: San Francisco International Airport (SFO) @@ -170,7 +169,7 @@ flight_info = """ def init_work_flow() -> WorkFlow: return WorkFlow( type=WorkFlowType.BOOK_FLIGHT, - deps=Deps( + deps=Deps_( flight_info=flight_info, date=datetime.date(2025, 1, 10), origin="SFO", @@ -197,8 +196,7 @@ async def run_stream_events( user_prompt=user_prompt, deps=task.deps, message_history=message_history, - usage=usage_to_object(dict(task.usage)), - usage_limits=usage_limits_to_object(dict(task.usage_limits)), + usage=usage, ) as events: async for event in events: if not isinstance(event, AgentRunResultEvent): diff --git a/agent/application/workshop/models.py b/agent/application/workshop/models.py index cd8e64c..a22a536 100644 --- a/agent/application/workshop/models.py +++ b/agent/application/workshop/models.py @@ -14,4 +14,4 @@ DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( ), ) -MODEL_SETTINGS = ModelSettings(extra_body={"thinking": {"type": "disabled"}}) +MODEL_SETTINGS_DISABLED_THINKING = ModelSettings(extra_body={"thinking": {"type": "disabled"}}) # 禁用思考模式 diff --git a/agent/assets/styles.css b/agent/assets/styles.css index 73a043b..015f25f 100644 --- a/agent/assets/styles.css +++ b/agent/assets/styles.css @@ -61,13 +61,13 @@ pre, pre code { font-family: Consolas, "Microsoft YaHei", sans-serif !important; } -.loadin { +.awaiting_stream { width: 6px; aspect-ratio: 1; border-radius: 50%; - animation: loadin 0.9s infinite ease-in-out; + animation: awaiting_stream 0.9s infinite ease-in-out; } -@keyframes loadin { +@keyframes awaiting_stream { 0% { box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9); background: var(--prismui-color-5); diff --git a/agent/database.db b/agent/database.db index 174c339cb93e9a95d654500c56804123056ea44e..061df7322d4068adb173635d850a67aa5d1d2881 100644 GIT binary patch literal 135168 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 |pd77wN?lD0O-K_y@&H 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 N^vvD4;<)?m{|{d%HRb>S diff --git a/agent/reflex.lock/bun.lock b/agent/reflex.lock/bun.lock index 25f70a0..978cde5 100644 --- a/agent/reflex.lock/bun.lock +++ b/agent/reflex.lock/bun.lock @@ -446,7 +446,7 @@ "@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], "arg": ["arg@5.0.2", "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],