From 02802615085f0149239587b00ac4c74bbbb685f8 Mon Sep 17 00:00:00 2001 From: liubiren Date: Fri, 7 Aug 2026 01:16:53 +0800 Subject: [PATCH] 1 --- agent/application/states/conversation.py | 13 +- agent/application/states/models.py | 43 ++--- agent/application/tasks/__init__.py | 53 +++-- agent/application/tasks/book_flight.py | 234 ++++++++++++----------- agent/database.db | Bin 106496 -> 126976 bytes 5 files changed, 166 insertions(+), 177 deletions(-) diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 60cb262..912c5ff 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -25,9 +25,7 @@ from application.states.database import DatabaseState from application.states.models import ( Conversation, Dialog, - Task, - TaskResultEvent, - TaskType, + TaskNodeResultEvent, Thought, thoughts_to_dict, usage_to_dict, @@ -52,8 +50,6 @@ class ConversationState(rx.State): # 显示更多的会话唯一标识 shown_more_conversation_id: str = "" - # 任务类型 - task_type: TaskType = TaskType.NONE # 用户提示词 user_prompt: str = "" @@ -237,7 +233,7 @@ class ConversationState(rx.State): tool_call_ids: dict[str, int] = {} # 获取运行流式输出事件 async for event in run_stream_events( - task_type=self.task_type, + task=conversation.task, user_prompt=dialog.user_prompt, message_history=message_history, ): @@ -362,7 +358,7 @@ class ConversationState(rx.State): case "tool_call": dialog.thoughts[index].content = f"已调用 {content}" - case TaskResultEvent(task=task, content=content): + case TaskNodeResultEvent(task=task, content=content): # 更新任务 conversation.task = task dialog.result_output += content @@ -373,7 +369,7 @@ class ConversationState(rx.State): await db_state.create_dialog_record( conversation_id=self.conversation_id, id=dialog.id, - user_prompt=self.user_prompt, + user_prompt=dialog.user_prompt, thoughts=thoughts_to_dict(dialog.thoughts), result_output=dialog.result_output, usage=usage_to_dict(result.usage), @@ -405,7 +401,6 @@ class ConversationState(rx.State): conversation = self.conversations[self.conversation_id] # 初始化预定航班任务 conversation.task = init_task() - self.user_prompt = f"帮我找一班从 {conversation.task.deps.origin} 到 {conversation.task.deps.destination} 在 {conversation.task.deps.date} 的航班" @rx.event diff --git a/agent/application/states/models.py b/agent/application/states/models.py index 315d4f5..9c57645 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -2,12 +2,12 @@ """ 面向 reflex.state 的类 """ -from datetime import datetime +from enum import StrEnum from typing import Any + from pydantic import BaseModel, Field, TypeAdapter from pydantic_ai import RunUsage from pydantic_ai._uuid import uuid7 -from enum import StrEnum from pydantic_ai.usage import UsageLimits @@ -53,14 +53,14 @@ class Dialog(BaseModel): UsageAdapter = TypeAdapter(RunUsage) -def usage_to_object(usage: dict) -> RunUsage: +def usage_to_object(usage: dict[str, Any]) -> RunUsage: """ Usage 转为对象 """ return UsageAdapter.validate_python(usage) if usage else RunUsage() -def usage_to_dict(usage: RunUsage) -> dict: +def usage_to_dict(usage: RunUsage) -> dict[str, Any]: """ Usage 转为字典 """ @@ -71,7 +71,7 @@ def usage_to_dict(usage: RunUsage) -> dict: UsageLimitsAdapter = TypeAdapter(UsageLimits) -def usage_limits_to_object(usage_limits: dict) -> UsageLimits: +def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits: """ UsageLimits 转为对象 """ @@ -82,7 +82,7 @@ def usage_limits_to_object(usage_limits: dict) -> UsageLimits: ) -def usage_limits_to_dict(usage_limits: UsageLimits) -> dict: +def usage_limits_to_dict(usage_limits: UsageLimits) -> dict[str, Any]: """ UsageLimits 转为字典 """ @@ -94,20 +94,9 @@ class TaskType(StrEnum): 任务类型枚举 """ - NONE = "none" BOOK_FLIGHT = "book_flight" -class TaskNode(StrEnum): - """ - 任务节点枚举 - """ - - EXECUTION = "execution" - PENDING_INPUT = "pending_input" - FINISH = "finish" - - class Task(BaseModel): """ 任务类 @@ -115,7 +104,7 @@ class Task(BaseModel): id: str = Field(default_factory=lambda: str(uuid7()), description="任务唯一标识") type: TaskType = Field(..., description="任务类型") - node: TaskNode = Field(default=TaskNode.EXECUTION, description="任务节点") + node: str | None = Field(default="", description="任务节点") deps: Any = Field(default=None, description="任务依赖项") usage: dict[str, Any] = Field(default_factory=dict, description="任务使用量") usage_limits: dict[str, Any] = Field( @@ -123,15 +112,6 @@ class Task(BaseModel): ) -class TaskResultEvent(BaseModel): - """ - 任务结果事件类 - """ - - task: Task = Field(..., description="任务实例") - content: str = Field(default="", description="任务结果内容") - - class Conversation(BaseModel): """ 会话类 @@ -149,3 +129,12 @@ class Conversation(BaseModel): ..., description="创建时间", ) + + +class TaskNodeResultEvent(BaseModel): + """ + 任务节点结果事件类 + """ + + task: Task | None = Field(..., description="任务实例") + content: str = Field(default="", description="任务结果内容") diff --git a/agent/application/tasks/__init__.py b/agent/application/tasks/__init__.py index 09f73f8..bd9108f 100644 --- a/agent/application/tasks/__init__.py +++ b/agent/application/tasks/__init__.py @@ -2,17 +2,15 @@ """ 任务模块 """ -from typing import Any, AsyncGenerator, Callable, Dict, Optional from typing import AsyncGenerator, List -from pydantic import BaseModel from pydantic_ai import Agent, ModelMessage -from pydantic_ai.models.openai import OpenAIChatModel -from pydantic_ai.providers.openai import OpenAIProvider +from pydantic_ai.messages import AgentStreamEvent +from pydantic_ai.run import AgentRunResultEvent -from application.states.models import Dialog, TaskType +from application.states.models import TaskNodeResultEvent, TaskType, Task from application.tasks.models import DEEPSEEK_V4_FLASH_MODEL -from application.states.models import TaskNode, Task + instruction = """ # 角色 @@ -32,32 +30,33 @@ instruction = """ async def run_stream_events( - task_type: TaskType, + task: Task | None, user_prompt: str, message_history: List[ModelMessage], -) -> AsyncGenerator: +) -> AsyncGenerator[AgentStreamEvent | AgentRunResultEvent | TaskNodeResultEvent, None]: """ 以流式事件模式运行 """ - match task_type: - case TaskType.NONE: - agent = Agent( - model=DEEPSEEK_V4_FLASH_MODEL, - instructions=instruction, - ) - async with agent.run_stream_events( - user_prompt=user_prompt, - message_history=message_history, - ) as events: - async for event in events: + if task: + match task.type: + case TaskType.BOOK_FLIGHT: + from application.tasks.book_flight import run_stream_events + + async for event in run_stream_events( + task=task, + user_prompt=user_prompt, + message_history=message_history, + ): yield event - case TaskType.BOOK_FLIGHT: - from application.tasks.book_flight import run_stream_events, init_task - - async for event in run_stream_events( - task=init_task(), - user_prompt=user_prompt, - message_history=message_history, - ): + else: + agent = Agent( + model=DEEPSEEK_V4_FLASH_MODEL, + instructions=instruction, + ) + async with agent.run_stream_events( + user_prompt=user_prompt, + message_history=message_history, + ) as events: + async for event in events: yield event diff --git a/agent/application/tasks/book_flight.py b/agent/application/tasks/book_flight.py index 512efe7..e891865 100644 --- a/agent/application/tasks/book_flight.py +++ b/agent/application/tasks/book_flight.py @@ -1,42 +1,40 @@ # -*- coding: utf-8 -*- """ -生成产品需求文档智能体 +预定航班任务 """ import datetime from typing import AsyncGenerator, Literal + from pydantic import BaseModel, Field -from pydantic_ai import ( - Agent, - ModelMessage, - ModelRetry, - RunContext, - UsageLimits, - ModelMessage, -) +from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits from pydantic_ai.run import AgentRunResultEvent -from application.tasks.models import DEEPSEEK_V4_FLASH_MODEL + from application.states.models import ( - TaskType, Task, - TaskNode, - usage_to_dict, - usage_to_object, - TaskResultEvent, + TaskNodeResultEvent, + TaskType, usage_limits_to_dict, usage_limits_to_object, + usage_to_dict, + usage_to_object, ) +from application.tasks.models import DEEPSEEK_V4_FLASH_MODEL class Deps(BaseModel): - flights: str = Field(..., description="航班信息") + """ + 依赖项类 + """ + + flight_info: str = Field(..., description="所有航班信息") date: datetime.date = Field(..., description="航班日期") origin: str = Field(..., description="出发机场") destination: str = Field(..., description="到达机场") -class FlightDetails(BaseModel): +class FlightDetail(BaseModel): """ - 航班详情 + 航班详情类 """ number: str = Field(description="航班号") @@ -48,83 +46,82 @@ class FlightDetails(BaseModel): class NoFlightFound(BaseModel): """ - 未查询到航班 + 未查询到航班类 """ -# 主控智能体 -master_agent = Agent[Deps, FlightDetails | NoFlightFound]( - model=DEEPSEEK_V4_FLASH_MODEL, - deps_type=Deps, - output_type=FlightDetails | NoFlightFound, - retries=2, - system_prompt=("你的工作是在给定日期为用户找到最便宜的航班"), -) - # 航班查询智能体 -search_agent = Agent( +flight_search_agent = Agent[Deps, FlightDetail | NoFlightFound]( model=DEEPSEEK_V4_FLASH_MODEL, - output_type=list[FlightDetails], - system_prompt="从给定文本中提取所有航班详细信息", + deps_type=Deps, + output_type=FlightDetail | NoFlightFound, + system_prompt="你的工作是在给定日期、出发机场和到达机场为用户找到最便宜的航班", +) + +# 所有航班详情提取智能体 +flight_details_extraction_agent = Agent( + model=DEEPSEEK_V4_FLASH_MODEL, + output_type=list[FlightDetail], + system_prompt="从给定文本中提取所有航班详细,包括航班号、航班日期、出发机场、到达机场和机票价格", ) -@master_agent.tool -async def search_flights(ctx: RunContext[Deps]) -> list[FlightDetails]: +@flight_search_agent.tool +async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]: """ - 查询并返回航班详情列表 + 工具:提取所有航班详情 """ - result = await search_agent.run(ctx.deps.flights, usage=ctx.usage) + result = await flight_details_extraction_agent.run( + ctx.deps.flight_info, usage=ctx.usage + ) return result.output -@master_agent.output_validator +@flight_search_agent.output_validator async def validate_output( - ctx: RunContext[Deps], output: FlightDetails | NoFlightFound -) -> FlightDetails | NoFlightFound: + ctx: RunContext[Deps], output: FlightDetail | NoFlightFound +) -> FlightDetail | NoFlightFound: """ - 校验主控智能体输出 + 输出校验:航班详情 """ if isinstance(output, NoFlightFound): return output - errors = "" + errors = [] if output.date != ctx.deps.date: - errors += f"航班日期应为 {ctx.deps.date}, 不是 {output.date}\n" + errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}") if output.origin != ctx.deps.origin: - errors += f"航班出发机场应为 {ctx.deps.origin}, 不是 {output.origin}\n" + errors.append(f"出发机场应为 {ctx.deps.origin}, 不是 {output.origin}") if output.destination != ctx.deps.destination: - errors += ( - f"航班到达机场应为 {ctx.deps.destination}, 不是 {output.destination}\n" - ) + errors.append(f"到达机场应为 {ctx.deps.destination}, 不是 {output.destination}") if errors: - raise ModelRetry(errors) - + raise ModelRetry("\n".join(errors)) return output class SeatPreference(BaseModel): - row: int = Field(ge=1, le=30) - seat: Literal["A", "B", "C", "D", "E", "F"] + """ + 座位偏好类 + """ + + row: int = Field(ge=1, le=30, description="座位行") + column: Literal["A", "B", "C", "D", "E", "F"] = Field(description="座位列") -class Failed(BaseModel): - """Unable to extract a seat selection.""" +class NoSeatExtracted(BaseModel): + """ + 未提取到座位偏好类 + """ -# 选座智能体 -seat_selection_agent = Agent[object, SeatPreference | Failed]( +# 提取座位偏好智能体(无依赖项) +seat_preference_extraction_agent = Agent[object, SeatPreference | NoSeatExtracted]( model=DEEPSEEK_V4_FLASH_MODEL, - output_type=SeatPreference | Failed, - system_prompt=( - "Extract the user's seat preference. " - "Seats A and F are window seats. " - "Row 1 is the front row and has extra leg room. " - "Rows 14, and 20 also have extra leg room. " - ), + output_type=SeatPreference | NoSeatExtracted, + system_prompt="提取用户的座位偏好。座位规则说明:A 座、F 座为靠窗座位;第 1 排是前排座位,腿部空间更大;14 排、20 排同样拥有加宽腿部空间", ) -flights = """ +flight_info = """ 1. Flight SFO-AK123 - Price: $350 - Origin: San Francisco International Airport (SFO) @@ -171,9 +168,9 @@ flights = """ def init_task() -> Task: return Task( type=TaskType.BOOK_FLIGHT, - node=TaskNode.EXECUTION, + node="flight_search", deps=Deps( - flights=flights, + flight_info=flight_info, date=datetime.date(2025, 1, 10), origin="SFO", destination="ANC", @@ -183,58 +180,21 @@ def init_task() -> Task: async def run_stream_events( - task: Task, - user_prompt: str | None = None, - message_history: list[ModelMessage] | None = None, + task: Task | None, + user_prompt: str, + message_history: list[ModelMessage], ) -> AsyncGenerator: - result = None while True: - if task.node == TaskNode.EXECUTION: - async with master_agent.run_stream_events( - user_prompt=user_prompt, - deps=task.deps, - message_history=message_history, - usage=usage_to_object(task.usage), - usage_limits=usage_limits_to_object(task.usage_limits), - ) as events: - async for event in events: - if not isinstance(event, AgentRunResultEvent): - yield event - else: - result = event.result - # 更新任务使用量 - task.usage = usage_to_dict(result.usage) - if isinstance(result.output, FlightDetails): - content = "\n---\n已查询到航班,请回复 buy 购票 / search 重新查询\n" - # 更新任务节点为待用户输入 - task.node = TaskNode.PENDING_INPUT - else: - content = "\n---\n未找到符合条件的航班,流程结束\n" - # 更新任务节点为结束 - task.node = TaskNode.FINISH - yield TaskResultEvent( - task=task, - content=content, - ) - yield event - return + if not task: + return - if task.node == TaskNode.PENDING_INPUT: - if user_prompt == "buy": - yield TaskResultEvent( - task=task, - content="请和我说下你的座位偏好吧:\nA、F 座位是靠窗位;1 排、14 排、20 排腿部空间更大、更舒展,你更想要靠窗座位,宽敞大空间座位", - ) - return - - elif user_prompt == "search": - # 更新任务节点为执行 - task.node = TaskNode.EXECUTION - - else: - async with seat_selection_agent.run_stream_events( + match task.node: + # 航班查询 + case "flight_search": + async with flight_search_agent.run_stream_events( user_prompt=user_prompt, + deps=task.deps, message_history=message_history, usage=usage_to_object(task.usage), usage_limits=usage_limits_to_object(task.usage_limits), @@ -246,11 +206,57 @@ async def run_stream_events( result = event.result # 更新任务使用量 task.usage = usage_to_dict(result.usage) - # 更新任务节点为结束 - task.node = TaskNode.FINISH - yield TaskResultEvent( + if isinstance(result.output, FlightDetail): + content = "\n---\n已查询到航班,请回复 buy 购票 或 search 重新查询\n" + # 更新任务节点为提取座位偏好 + task.node = "seat_preference_extraction" + else: + content = ( + "\n---\n未查询到满足您需求的航班,流程结束!\n" + ) + # 更新任务为空 + task = None + # 返回任务节点结果事件 + yield TaskNodeResultEvent( task=task, - content="已为您预定好座位,流程结束", + content=content, ) yield event return + # 提取座位偏好 + case "seat_preference_extraction": + if user_prompt == "buy": + # 返回任务节点结果事件 + yield TaskNodeResultEvent( + task=task, + content="请和我说下您的座位偏好:\nA、F 座位是靠窗位;1 排、14 排、20 排腿部空间更大、更舒展,你更想要靠窗座位,宽敞大空间座位", + ) + return + + elif user_prompt == "search": + # 更新任务节点为航班查询 + task.node = "flight_search" + + else: + async with seat_preference_extraction_agent.run_stream_events( + user_prompt=user_prompt, + message_history=message_history, + usage=usage_to_object(task.usage), + usage_limits=usage_limits_to_object(task.usage_limits), + ) as events: + async for event in events: + if not isinstance(event, AgentRunResultEvent): + yield event + else: + result = event.result + # 更新任务使用量 + task.usage = usage_to_dict(result.usage) + # 更新任务为空 + task = None + # 返回任务节点结果事件 + yield TaskNodeResultEvent( + task=task, + content="已帮您定好座位,流程结束!", + ) + yield event + return diff --git a/agent/database.db b/agent/database.db index b609ef33cd05e7becf0c1185501f0ab4c88d7565..e3772894668ce8c042796be81f6635dbd0d9d20f 100644 GIT binary patch delta 14168 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 au=1o-cswKhBze`ci^G`1@jySMZ{9C*WQynj