From a8dc684c20f448a5db4e80c91c3ad5ab56cf99c2 Mon Sep 17 00:00:00 2001 From: liubiren Date: Tue, 15 Sep 2026 20:21:37 +0800 Subject: [PATCH] 1 --- agent/application/pages/conversation.py | 138 ++++++++++++++-------- agent/application/states/conversation.py | 71 +++++------ agent/application/states/models.py | 3 + agent/application/workshop/book_flight.py | 50 ++++---- agent/assets/styles.css | 11 +- agent/database.db | Bin 159744 -> 192512 bytes 6 files changed, 161 insertions(+), 112 deletions(-) diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 236cd77..5cd1cd0 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -370,14 +370,12 @@ def thinking(item: MessageHistoryItem) -> rx.Component: rx.text( item.title, font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", class_name=rx.cond(item.is_running, "is_running", None), ), rx.icon( "chevron-right", width="14px", height="14px", - color="var(--prismui-color-3)", transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"), transition="transform 0.18s ease-in-out", ), @@ -442,57 +440,102 @@ def tool_call(item: MessageHistoryItem) -> rx.Component: margin_bottom="8px", line_height="22px", ), - # 内容 - rx.markdown( - item.content, - width="100%", - margin="4px 0", - line_height="1.6", - font_size="var(--prismui-font-size-1)", - ), - # 审批 - rx.cond( - item.has_approval, - rx.hstack( - rx.text_area( - placeholder="请输入", - # 值绑定审批用户提示词 - value=item.approval_user_prompt, - # 值变化事件:设置审批用户提示词 - on_change=lambda user_prompt: ConversationState.set_approval_user_prompt( - item.id, user_prompt - ), - flex=1, - height="100%", - padding="4px", - border_radius="var(--prismui-border-radius-4)", - line_height="16px", - border="none !important", + rx.hstack( + # 分割线 + rx.box( + width="1px", + margin="12px 7px", + background_color="var(--prismui-background-color-2)", + ), + rx.vstack( + # 内容 + rx.markdown( + item.content, + width="100%", + margin="4px 0", + line_height="1.6", + font_size="var(--prismui-font-size-1)", ), - rx.button( - "发送", - # 点击事件:审批 - on_click=lambda: ConversationState.approve(item.id, True), - # 是否禁用绑定验证码发送禁用状态 - margin="0 8px", - background_color="var(--prismui-background-color-1)", - line_height="16px", - # 若验证码发送禁用状态为是则颜色设置为 color-4,否则设置为 color-5(主题色) - color=rx.cond( - AuthState.is_captcha_code_sending_button_disabled, - "var(--prismui-color-4)", - "var(--prismui-color-5)", + # 审批 + rx.cond( + item.has_approval, + rx.vstack( + rx.text_area( + # 值绑定审批用户提示词 + value=item.approval_user_prompt, + # 值变化事件:设置审批用户提示词 + on_change=lambda user_prompt: ConversationState.set_approval_user_prompt( + item.id, user_prompt + ), + disabled=item.is_approval_user_prompt_input_disabled, + placeholder="请输入", + vertical_align="middle", + width="100%", + height="auto", + padding="4px 0", + outline="none !important", + box_shadow="none !important", + style={ + ".rt-TextAreaInput": { + "font-size": "var(--prismui-font-size-1) !important", + "border": "none !important", + "overflow-y": "hidden !important", + }, + ".rt-TextAreaInput:disabled": { + "background-color": "var(--prismui-background-color-1) !important", + "font-size": "var(--prismui-font-size-1) !important", + "cursor": "not-allowed !important", + }, + }, + ), + rx.hstack( + rx.button( + rx.cond( + item.is_approval_user_prompt_input_disabled, + "已提交", + "提交", + ), + # 点击事件:审批 + on_click=lambda: ConversationState.approve(item.id), + disabled=item.is_approval_user_prompt_input_disabled, + padding="0 12px", + # 是否禁用绑定审批用户提示词不可输出状态 + background_color=rx.cond( + item.is_approval_user_prompt_input_disabled, + "var(--prismui-background-color-8)", + "var(--prismui-background-color-7)", + ), + border_radius="var(--prismui-border-radius-3)", + line_height="18px", + font_size="var(--prismui-font-size-1)", + color="var(--prismui-color-9)", + cursor=rx.cond( + item.is_approval_user_prompt_input_disabled, + "not-allowed", + "pointer", + ), + ), + justify_content="flex-end", + width="100%", + ), + width="100%", + padding="12px 16px", + background_color="var(--prismui-background-color-1)", + border="var(--prismui-border-1)", + border_radius="var(--prismui-border-radius-4)", ), + rx.fragment(), ), width="100%", - height="45px", - align_items="center", - border="var(--prismui-border-1)", - border_radius="var(--prismui-border-radius-4)", + gap="4px", ), - rx.fragment(), + align_items="stretch", + width="100%", + margin_bottom="8px", + gap="4px", ), width="100%", + margin_bottom="8px", ) @@ -828,17 +871,16 @@ def user_prompt_sending() -> rx.Component: inline_height="1.5", font_size="var(--prismui-font-size-2)", white_space="nowrap", - overflow="hidden", cursor=rx.cond( ConversationState.is_user_prompt_sending_button_disabled, "not-allowed", "pointer", ), ), - width="100%", justify_content="flex-end", align_items="center", height="32px", + width="100%", ), width="100%", padding="12px 16px", diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 75cd840..2da1660 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -3,14 +3,9 @@ 会话状态 """ from datetime import datetime -from typing import AsyncGenerator, cast +from typing import AsyncGenerator -from pydantic_ai import ( - DeferredToolRequests, - DeferredToolResults, - ToolApproved, - ToolDenied, -) +from pydantic_ai import DeferredToolRequests, DeferredToolResults, ToolApproved from pydantic_ai.messages import ( FunctionToolResultEvent, PartDeltaEvent, @@ -110,6 +105,7 @@ class ConversationState(rx.State): ) # 将最后一个会话的唯一标识作为激活会话唯一标识 self.actived_conversation_id = next(reversed(self.conversations.keys())) + print(self.conversations[self.actived_conversation_id]) @rx.event def toggle_conversation_history_shown(self) -> None: @@ -193,6 +189,11 @@ class ConversationState(rx.State): **message.model_dump(), has_approval=isinstance(approval := message.approval, Approval), approval_user_prompt=getattr(approval, "user_prompt", ""), + is_approval_user_prompt_input_disabled=( + False + if (isinstance(approval, Approval) and approval.result is None) + else True + ), ) ) return message_history @@ -273,10 +274,6 @@ class ConversationState(rx.State): if not (user_prompt := approval.user_prompt): return - # 将等待流式输出设置为是 - conversation.awaiting_stream = True - yield # 通知前端更新渲染 - else: # 若用户提示词为空则直接返回 if not (user_prompt := conversation.user_prompt): @@ -368,19 +365,23 @@ class ConversationState(rx.State): case ToolCallPart( tool_call_id=tool_call_id, tool_name=tool_name ): - # 构建消息实例 - message = Message( - type=MessageType.TOOL_CALL, - title=f"正在{tool_name}", - is_running=True, - ) - # 添加至消息字典 - conversation.messages[message.id] = message - # 将消息实例唯一标识与工具调用唯一标识映射 - conversation.workflow.tool_call_id_map_to_message_id[ + if ( tool_call_id - ] = message.id - yield # 通知前端更新渲染 + not in conversation.workflow.tool_call_id_map_to_message_id + ): + # 构建消息实例 + message = Message( + type=MessageType.TOOL_CALL, + title=f"正在{tool_name}", + is_running=True, + ) + # 添加至消息字典 + conversation.messages[message.id] = message + # 将消息实例唯一标识与工具调用唯一标识映射 + conversation.workflow.tool_call_id_map_to_message_id[ + tool_call_id + ] = message.id + yield # 通知前端更新渲染 # 文本分片 case TextPart(content=content): @@ -458,8 +459,7 @@ class ConversationState(rx.State): ] message.is_running = False message.title = f"{tool_name}已完成" - # 若内容非空字符串则更新 - if content := message.content: + if isinstance(content, str) and content: message.content = content yield # 通知前端更新渲染 messages.append(message) @@ -510,6 +510,7 @@ class ConversationState(rx.State): conversation_id=conversation.id, new_messages=result.new_messages(), ) + # 批量创建消息记录 await db_state.create_message_records( conversation_id=conversation.id, @@ -569,11 +570,10 @@ class ConversationState(rx.State): message.approval.user_prompt = user_prompt @rx.event - async def approve(self, item_id: str, result: bool) -> None: + async def approve(self, item_id: str) -> AsyncGenerator[None, None]: """ 审批消息历史项 :param item_id: 消息历史项唯一标识 - :param result: 审批结果(True 表示审批通过,False 表示审批拒绝) """ # 当前会话 conversation = self.conversations.get(self.actived_conversation_id) @@ -585,16 +585,19 @@ class ConversationState(rx.State): ): return - # 更新审批结果 - approval.result = result + # 若会话未正在运行则将正在运行设置为是 + if not conversation.is_running: + conversation.is_running = True + # 将等待流式输出设置为是 + conversation.awaiting_stream = True + # 更新审批结果为审批通过 + approval.result = True + yield # 通知前端更新渲染 + # 运行 async for _ in self.run( approval, - DeferredToolResults( - approvals={ - approval.tool_call_id: ToolApproved() if result else ToolDenied() - } - ), + DeferredToolResults(approvals={approval.tool_call_id: ToolApproved()}), ): ... diff --git a/agent/application/states/models.py b/agent/application/states/models.py index 3b1fb33..ea9ced2 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -120,6 +120,9 @@ class MessageHistoryItem(Message): has_approval: bool = Field(default=False, description="包含审批实例") approval_user_prompt: str = Field(default="", description="审批用户提示词") + is_approval_user_prompt_input_disabled: bool = Field( + default=False, description="审批用户提示词不可输出状态" + ) # 工作流适配器 diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index d7280ae..5d3a4d6 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -8,6 +8,7 @@ from typing import Any, AsyncGenerator, cast from pydantic import BaseModel, Field, field_validator from pydantic_ai import ( Agent, + AgentRunResult, ApprovalRequired, DeferredToolRequests, DeferredToolResults, @@ -16,11 +17,8 @@ from pydantic_ai import ( RunContext, RunUsage, UsageLimits, - AgentRunResult, ) from pydantic_ai.messages import ( - PartDeltaEvent, - TextPart, AgentStreamEvent, FunctionToolResultEvent, PartStartEvent, @@ -29,12 +27,11 @@ from pydantic_ai.messages import ( ) from pydantic_ai.run import AgentRunResultEvent -from application.states.models import NoResult +from application.workshop.events import AgentPromptEvent from application.workshop.models import ( DEEPSEEK_V4_FLASH_MODEL, DEEPSEEK_V4_FLASH_MODEL_SETTINGS, ) -from application.workshop.events import AgentPromptEvent class Flight(BaseModel): @@ -76,15 +73,15 @@ class Deps(BaseModel): # 主智能体 -agent = Agent[Deps, Flight | NoResult | DeferredToolRequests]( +agent = Agent[Deps, Flight | DeferredToolRequests]( model=DEEPSEEK_V4_FLASH_MODEL, model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, deps_type=Deps, - output_type=Flight | NoResult | DeferredToolRequests, + output_type=Flight | DeferredToolRequests, system_prompt=( "你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**:", "1. 使用 search_flights 查询满足用户需求的航班;", - "2. 使用 book_flight 预定航班。若未选择到航班则返回 NoSelectedFlight;", + "2. 使用 book_flight 预定航班。当返回航班信息(Flight)时表示预定航班完成。", ), ) @@ -209,11 +206,11 @@ async def search_flights(ctx: RunContext[Deps]) -> list[Flight]: @agent.tool(name="预定航班") -async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult: +async def book_flight(ctx: RunContext[Deps]) -> Flight: """ 预定航班 """ - if ctx.deps.booked_flight: + if ctx.deps.booked_flight is not None: return ctx.deps.booked_flight if (searched_flights := ctx.deps.searched_flights) is None: @@ -232,12 +229,16 @@ async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult: ) ).output if not extracted_flight_number: - return NoResult() + raise ApprovalRequired( + metadata={ + "content": f"未提取到航班号。请确认后重新输入。", + } + ) if extracted_flight_number not in flight_numbers: raise ApprovalRequired( metadata={ - "content": f"提取到的航班号 {extracted_flight_number} 不在查询到的航班中。请确认后重新输入。" + "content": f"提取到的航班号 {extracted_flight_number} 不在查询到的航班中。请确认后重新输入。", } ) # 预定到的航班 @@ -252,12 +253,12 @@ async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult: @agent.output_validator async def validate_output( ctx: RunContext[Deps], - output: Flight | NoResult | DeferredToolRequests, -) -> Flight | NoResult | DeferredToolRequests: + output: Flight | DeferredToolRequests, +) -> Flight | DeferredToolRequests: """ 输出校验 """ - if isinstance(output, (NoResult, DeferredToolRequests)): + if isinstance(output, DeferredToolRequests): return output errors = [] @@ -291,13 +292,14 @@ async def run_stream_events( usage_limits: UsageLimits | None = None, deferred_tool_results: DeferredToolResults | None = None, ) -> AsyncGenerator[ - AgentStreamEvent - | AgentRunResultEvent[Flight | NoResult | DeferredToolRequests] - | None, + AgentStreamEvent | AgentRunResultEvent[Flight | DeferredToolRequests] | None, ]: """ 运行并流式输出事件 """ + if not isinstance(deps, Deps): + deps = Deps.model_validate(deps) + async with agent.run_stream_events( user_prompt=user_prompt, deps=deps, @@ -330,12 +332,14 @@ async def run_stream_events( searched_flights = cast(list[Flight], content) event.part.content = f"已查询到 **{len(searched_flights)}** 班航班:\n\n" for searched_flight in searched_flights: - event.part.content += f"{searched_flight.number} {searched_flight.airfare}$ 于 {searched_flight.date.strftime('%Y-%m-%d')} 从 {searched_flight.origin_airport_code} 到 {searched_flight.destination_airport_code}\n\n" + event.part.content += f"**{searched_flight.number}** | 💰 {searched_flight.airfare} | {searched_flight.date.strftime('%Y-%m-%d')} | {searched_flight.origin_airport_code} ✈️ {searched_flight.destination_airport_code}\n\n" yield event case "预定航班": if isinstance(deps.booked_flight, Flight): - event.part.content = "" + event.part.content = ( + "请输入航班号:\n\n" # 成功预定内容维持提示 + ) else: event.part.content = ( "未提取到航班号,请检查后重试\n\n" @@ -348,10 +352,6 @@ async def run_stream_events( match output: case Flight(): yield AgentPromptEvent( - content=f"已成功预定:\n\n{output.number} {output.airfare}$ 于 {output.date.strftime('%Y-%m-%d')} 从 {output.origin_airport_code} 到 {output.destination_airport_code}\n\n本次会话结束。" - ) - case NoResult(): - yield AgentPromptEvent( - content="预定失败\n\n本次会话结束。" + content=f"---\n\n✅ 预定机票成功:\n\n{output.number} | 💰 {output.airfare} | {output.date.strftime('%Y-%m-%d')} | {output.origin_airport_code} ✈️ {output.destination_airport_code}\n\n本次会话结束。" ) yield event diff --git a/agent/assets/styles.css b/agent/assets/styles.css index 54e044a..b542093 100644 --- a/agent/assets/styles.css +++ b/agent/assets/styles.css @@ -6,6 +6,7 @@ --prismui-background-color-5: #ffffff33; --prismui-background-color-6: #f2f2f3; --prismui-background-color-7: #5e7ce0; + --prismui-background-color-8: #beccfa; --prismui-background-1: linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%); --prismui-background-2: linear-gradient(180deg, #fffffff2, #f8fafff2 99%); --prismui-background-3: linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd); @@ -90,11 +91,11 @@ pre, pre code { /* 使用主题变量:普通文字色 --prismui-color-3,高光使用主题蓝色 --prismui-color-5 */ background: linear-gradient( 90deg, - var(--prismui-color-3) 0%, - var(--prismui-color-3) 35%, - var(--prismui-color-7) 50%, - var(--prismui-color-3) 65%, - var(--prismui-color-3) 100% + var(--prismui-color-1) 0%, + var(--prismui-color-1) 35%, + var(--prismui-color-3) 50%, + var(--prismui-color-1) 65%, + var(--prismui-color-1) 100% ); background-size: 200% 100%; background-clip: text; diff --git a/agent/database.db b/agent/database.db index a2ea2dde7818f54ad895b900a1c1f337a8d08eea..94864f02d0ae7568ddd7f653338ba24d77e4ff91 100644 GIT binary patch delta 29086 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 V?FfQy!_CMw@V#0GerIg${|7Cf^Wp#i delta 6192 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 fJR?_hlzCfV8oYl(&rtH%>dzJ*w8C(7b{2c1Q