From 5b5f41be49c5847ca6c0a7288c3ae95df3ee79a9 Mon Sep 17 00:00:00 2001 From: liubiren Date: Wed, 12 Aug 2026 20:14:26 +0800 Subject: [PATCH] 1 --- agent/application/states/conversation.py | 12 +- agent/application/workshop/book_flight.py | 313 ++++++++++++++-------- agent/application/workshop/models.py | 2 +- agent/database.db | Bin 135168 -> 163840 bytes 4 files changed, 206 insertions(+), 121 deletions(-) diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index a9cdd0e..aed9306 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -279,10 +279,10 @@ class ConversationState(rx.State): conversation.is_running = True # 将等待流式输出设置为是 conversation.awaiting_stream = True + yield # 通知前端更新渲染 # 获取数据库状态 db_state = await self.get_db_state() - # 先创建消息记录再添加消息实例 conversation.messages.update( await db_state.create_message_record( @@ -350,6 +350,7 @@ class ConversationState(rx.State): conversation.messages[message.id] = message # 将消息实例唯一标识与片段索引映射 index_map_to_message_id[index] = message.id + yield # 通知前端更新渲染 # 文本分片开始事件 case TextPart(content=content): @@ -360,6 +361,7 @@ class ConversationState(rx.State): conversation.messages[message.id] = message # 将消息实例唯一标识与片段索引映射 index_map_to_message_id[index] = message.id + yield # 通知前端更新渲染 # ========== 增量事件 ========== case PartDeltaEvent(index=index, delta=delta): @@ -373,6 +375,7 @@ class ConversationState(rx.State): conversation.messages[ index_map_to_message_id[index] ].content += content_delta + yield # 通知前端更新渲染 # 文本分片增量事件 case TextPartDelta( @@ -382,6 +385,7 @@ class ConversationState(rx.State): conversation.messages[ index_map_to_message_id[index] ].content += content_delta + yield # 通知前端更新渲染 # ========== 结束事件 ========== case PartEndEvent( @@ -397,6 +401,7 @@ class ConversationState(rx.State): ] message.is_running = False message.title = "思考完成" + yield # 通知前端更新渲染 messages.append(message) case TextPart(content=content): @@ -404,7 +409,6 @@ class ConversationState(rx.State): message = conversation.messages[ index_map_to_message_id[index] ] - message.content = content messages.append(message) # ========== 智能体运行结果事件 ========== @@ -419,10 +423,6 @@ class ConversationState(rx.State): new_messages=result.new_messages(), ) - # 强制更新会话并推送前端 - self.conversations[self.actived_conversation_id] = conversation - yield - except Exception as e: ... finally: diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index d862633..7c432b4 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -2,29 +2,52 @@ """ 预定航班(范式) """ +import asyncio import datetime -from typing import AsyncGenerator, Literal +from typing import AsyncGenerator -from pydantic import BaseModel, Field -from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage, DeferredToolRequests -from pydantic_ai.run import AgentRunResultEvent - -from application.states.models import ( - WorkFlow, - WorkFlowType, +from logfire_api.variables import ValueDoesNotEqual +from pydantic import BaseModel, Field, field_validator +from pydantic_ai import ( + Agent, + ApprovalRequired, + DeferredToolRequests, + DeferredToolResults, + ModelMessage, + ModelRetry, + ModelSettings, + RunContext, + RunUsage, + UsageLimits, ) -from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS_DISABLED_THINKING +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider +from sqlalchemy.sql.dml import ReturningDelete -class Dependences(BaseModel): +DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( + model_name="deepseek-v4-flash", + provider=OpenAIProvider( + base_url="https://tokenhub.tencentmaas.com/v1", + api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq", + ), +) + +MODEL_SETTINGS = ModelSettings( + temperature=0, extra_body={"thinking": {"type": "disabled"}} # 温度控制 +) # 禁用思考模式 + + +class Deps(BaseModel): """ 依赖类 """ - date: datetime.date = Field(..., description="航班日期") - origin: str = Field(..., description="航班出发机场") - destination: str = Field(..., description="航班到达机场") - source_material: str = Field(..., description="航班来源资料") + date: datetime.date = Field(..., description="日期") + origin_airport_code: str = Field(..., description="出发机场代码") + destination_airport_code: str = Field(..., description="到达机场代码") + source_material: str = Field(..., description="来源资料") + class Flight(BaseModel): """ @@ -32,10 +55,22 @@ class Flight(BaseModel): """ number: str = Field(..., description="航班号") - date: datetime.date = Field(..., description="航班日期") - origin: str = Field(..., description="航班出发机场") - destination: str = Field(..., description="航班到达机场") - airfare: int = Field(..., description="航班机票价格") + date: datetime.date = Field(..., description="日期") + origin_airport_code: str = Field(..., description="出发机场代码") + destination_airport_code: str = Field(..., description="到达机场代码") + airfare: int = Field(..., description="机票价格") + + @field_validator("origin_airport_code", "destination_airport_code") + @classmethod + def validate_airport_code(cls, airport_code: str) -> str: + """校验机场代码""" + if ( + not airport_code.isalpha() + or not airport_code.isupper() + or len(airport_code) != 3 + ): + raise ValueError(f"机场代码须为英文、大写、3位字符串") + return airport_code class FlightNoFound(BaseModel): @@ -43,85 +78,104 @@ class FlightNoFound(BaseModel): 未查询到航班类 """ -class SeatPreference(BaseModel): - """ - 座位偏好类 - """ - row: int = Field(ge=1, le=30, description="座位行") - column: Literal["A", "B", "C", "D", "E", "F"] = Field(description="座位列") +system_prompt = """你的任务是根据用户提供的日期、出发机场和到达机场选择航班。 +1. 使用 extract_flights 从来源资料提取所有航班信息; +2. 使用 select_flight 从所有航班信息中选择匹配航班; +""" - -class SeatPreferenceNoExtracted(BaseModel): - """ - 未提取到座位偏好类 - """ - - -# 航班查询智能体 -flight_inquiry_agent = Agent[Dependences, Flight | FlightNoFound | DeferredToolRequests]( +# 主智能体 +agent = Agent[Deps, Flight | FlightNoFound]( model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=MODEL_SETTINGS_DISABLED_THINKING, - deps_type=Dependences, - output_type=Flight | FlightNoFound | DeferredToolRequests, - system_prompt="你的任务是根据给定的日期、出发机场和到达机场帮助用户找到最便宜的航班。", + model_settings=MODEL_SETTINGS, + deps_type=Deps, + output_type=Flight | FlightNoFound, + system_prompt=system_prompt, ) # 航班信息提取智能体 -flights_extraction_agent = Agent( +extraction_agent = Agent( model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=MODEL_SETTINGS_DISABLED_THINKING, + model_settings=MODEL_SETTINGS, output_type=list[Flight], - system_prompt="你的任务是根据给定的航班来源资料提取出所有航班信息,包括航班号、航班日期、航班出发机场、航班到达机场和航班机票价格。禁止编造。") + system_prompt=( + "你的任务是根据给定的来源资料提取出所有航班信息,包括航班号、日期、出发机场代码、到达机场代码和机票价格。", + "其中,出发机场代码和到达机场代码须为英文、大写、3位字符串,例如 San Francisco International Airport (SFO) 的机场代码为 SFO。", + "若某航班信息不全则跳过该航班,若没有任何航班信息则返回空列表。", + "禁止编造。", + ), +) # 通过提示词约束输出 +# 航班选择智能体 +selection_agent = Agent( + model=DEEPSEEK_V4_FLASH_MODEL, + model_settings=MODEL_SETTINGS, + output_type=Flight | FlightNoFound, + system_prompt="你的任务是从所有航班信息中选择匹配用户提供的日期、出发机场和到达机场的航班:若有多个航班则选择最便宜的航班,选择完成需用户确认是否预定;若没有则返回 FlightNoFound。", +) -@flight_inquiry_agent.tool -async def extract_flight_details(ctx: RunContext[Dependences]) -> list[Flight]: +""" +如何提高提取准确率 +1. 系统提示词规则约束 +2. 降低模型温度 +3. 输出类型约定和输出模型校验 +4. 业务规则约束(例如,输出不可为空列表) +""" + + +@agent.tool +async def extract_flights(ctx: RunContext[Deps]) -> list[Flight]: """ 提取所有航班信息 """ - result = await flights_extraction_agent.run( - ctx.deps.source_material, usage=ctx.usage + result = await extraction_agent.run( + ctx.deps.source_material, + usage_limits=UsageLimits(request_limit=3), + usage=ctx.usage, ) + if len(output := result.output) != 8: + raise ModelRetry("提取到的所有航班信息应为 8 条") + return output + + +@agent.tool +async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound: + """ + 选择航班 + """ + result = await selection_agent.run(ctx.deps.source_material, usage=ctx.usage) + if isinstance(result.output, Flight): + if not ctx.tool_call_approved: + raise ApprovalRequired(metadata={"reason": "protected"}) return result.output -@flight_inquiry_agent.output_validator +@agent.output_validator async def validate_output( - ctx: RunContext[Dependences], output: Flight | FlightNoFound | DeferredToolRequests -) -> Flight | FlightNoFound | DeferredToolRequests: + ctx: RunContext[Deps], output: Flight | FlightNoFound +) -> Flight | FlightNoFound: """ - 输出校验:航班信息 + 输出校验 """ # 不校验未查询到航班 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}") + 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 @@ -166,62 +220,72 @@ source_material = """ """ -def init_work_flow() -> WorkFlow: - return WorkFlow( - type=WorkFlowType.BOOK_FLIGHT, - deps=Deps_( - flight_info=flight_info, - date=datetime.date(2025, 1, 10), - origin="SFO", - destination="ANC", - ), - usage_limits={}, - ) - - async def run_stream_events( - usage: RunUsage, user_prompt: str, + deps: Deps, message_history: list[ModelMessage], + usage: RunUsage, + deferred_tool_results: DeferredToolResults | None = None, ) -> AsyncGenerator: - result = None - while True: - if not task: - return + async with agent.run_stream_events( + user_prompt=user_prompt, + deps=deps, + message_history=message_history, + usage=usage, + deferred_tool_results=deferred_tool_results, + ) as events: + async for event in events: + yield event - 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, - ) 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, 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, - ) - yield event - return + +async def main(): + deps = Deps( + source_material=source_material, + date=datetime.date(2025, 1, 10), + origin="SFO", + destination="ANC", + ) + # message_history 传空列表,不要传 None + msg_history: list[ModelMessage] = [] + prompt = f"Find me a flight from {deps.origin} to {deps.destination} on {deps.date}" + + gen = run_stream_events( + user_prompt=prompt, + deps=deps, + message_history=msg_history, + usage=RunUsage(), + ) + # 消费异步生成器 + async for ev in gen: + print(ev) + + +asyncio.run(main()) + +""" +if not isinstance(event, AgentRunResultEvent): + yield event + else: + result = event.result + # 更新任务使用量 + task.usage = usage_to_dict(result.usage) + 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, + ) + yield event + return # 提取座位偏好 case "seat_preference_extraction": if user_prompt == "buy": @@ -237,7 +301,7 @@ async def run_stream_events( task.node = "flight_search" else: - async with seat_preference_extraction_agent.run_stream_events( +async with seat_preference_extraction_agent.run_stream_events( user_prompt=user_prompt, message_history=message_history, usage=usage_to_object(dict(task.usage)), @@ -328,4 +392,25 @@ async def run_stream_events( dialog.thoughts[index].content = f"已调用 {content}" case _: - continue \ No newline at end of file + continue + + + def init_work_flow() -> WorkFlow: + return WorkFlow( + type=WorkFlowType.BOOK_FLIGHT, + deps=Dependences( + source_material=source_material, + date=datetime.date(2025, 1, 10), + origin="SFO", + destination="ANC", + ), + ) +from application.states.models import ( + WorkFlow, + WorkFlowType, +) +from application.workshop.models import ( + DEEPSEEK_V4_FLASH_MODEL, + MODEL_SETTINGS_DISABLED_THINKING, +) +""" diff --git a/agent/application/workshop/models.py b/agent/application/workshop/models.py index a22a536..9f018c1 100644 --- a/agent/application/workshop/models.py +++ b/agent/application/workshop/models.py @@ -3,7 +3,7 @@ 智能体相关模块 """ from pydantic_ai import ModelSettings -from pydantic_ai.models.openai import OpenAIChatModel, OpenAIChatModelSettings +from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( diff --git a/agent/database.db b/agent/database.db index 061df7322d4068adb173635d850a67aa5d1d2881..74f44ce0efdc6a7dc0b1bd60a148a6830ef21da3 100644 GIT binary patch delta 15472 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 F{}1G1Xwv`y delta 312 zcmZo@;A&XFF+rNul7WFiaiW4fqvghgW%*nu8Th++P5I_=pWG}^u!Eb6p(&GvBQ#XL zxpMNw>J&CZGa~~Xh3ST88HFeN)fh5cn3zs}Q(G)-Y-m)gqo7ojT3DJ|TvDuLrC_|7 zv-Y5l=$dwTV64F4^85qQPswcA_(A_L3;LbCB0v{vS z^lRKqa+?>QNo3{%$}#Y_@!#NIKbh}<8eECS=EZjsnFUz*-vIS9$b*dL|H%Jl