This commit is contained in:
parent
16c4072c62
commit
38b5b8289a
|
|
@ -30,6 +30,7 @@ from application.states.models import (
|
||||||
Work,
|
Work,
|
||||||
WorkType,
|
WorkType,
|
||||||
TaskStatus,
|
TaskStatus,
|
||||||
|
AgentRunWarmUpEvent,
|
||||||
ConversationHistoryItem,
|
ConversationHistoryItem,
|
||||||
usage_validate_python,
|
usage_validate_python,
|
||||||
MessageHistoryItem,
|
MessageHistoryItem,
|
||||||
|
|
@ -357,8 +358,7 @@ class ConversationState(rx.State):
|
||||||
# 文本分片开始事件
|
# 文本分片开始事件
|
||||||
case TextPart(content=content):
|
case TextPart(content=content):
|
||||||
# 构建消息实例
|
# 构建消息实例
|
||||||
message = Message(type=MessageType.RESULT_OUTPUT)
|
message = Message(type=MessageType.TEXT, content=content)
|
||||||
message.content = content
|
|
||||||
# 添加至消息字典
|
# 添加至消息字典
|
||||||
conversation.messages[message.id] = message
|
conversation.messages[message.id] = message
|
||||||
# 将消息实例唯一标识与片段索引映射
|
# 将消息实例唯一标识与片段索引映射
|
||||||
|
|
@ -413,6 +413,18 @@ class ConversationState(rx.State):
|
||||||
]
|
]
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
|
# ========== 智能体运行预热事件 ==========
|
||||||
|
case AgentRunWarmUpEvent(content=content):
|
||||||
|
# 构建消息实例
|
||||||
|
message = Message(
|
||||||
|
type=MessageType.TEXT,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
# 添加至消息字典
|
||||||
|
conversation.messages[message.id] = message
|
||||||
|
yield # 通知前端更新渲染
|
||||||
|
messages.append(message)
|
||||||
|
|
||||||
# ========== 智能体运行结果事件 ==========
|
# ========== 智能体运行结果事件 ==========
|
||||||
case AgentRunResultEvent(result=result):
|
case AgentRunResultEvent(result=result):
|
||||||
# 更新使用量
|
# 更新使用量
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class MessageType(StrEnum):
|
||||||
THINKING = "thinking"
|
THINKING = "thinking"
|
||||||
WORK = "work"
|
WORK = "work"
|
||||||
RESULT_OUTPUT = "result_output"
|
RESULT_OUTPUT = "result_output"
|
||||||
|
TEXT = "text"
|
||||||
|
|
||||||
|
|
||||||
class Message(BaseModel):
|
class Message(BaseModel):
|
||||||
|
|
@ -65,6 +66,17 @@ class Message(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRunWarmUpEvent(BaseModel):
|
||||||
|
"""
|
||||||
|
智能体运行预热事件类
|
||||||
|
"""
|
||||||
|
|
||||||
|
content: str = Field(
|
||||||
|
default="",
|
||||||
|
description="预热内容",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkType(StrEnum):
|
class WorkType(StrEnum):
|
||||||
"""
|
"""
|
||||||
工作类型枚举
|
工作类型枚举
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from pydantic_ai.run import AgentRunResultEvent, AgentRunResult
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from pydantic_ai._uuid import uuid7
|
from pydantic_ai._uuid import uuid7
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
|
AgentStreamEvent,
|
||||||
FunctionToolCallEvent,
|
FunctionToolCallEvent,
|
||||||
FunctionToolResultEvent,
|
FunctionToolResultEvent,
|
||||||
LoadCapabilityCallPart,
|
LoadCapabilityCallPart,
|
||||||
|
|
@ -73,6 +74,18 @@ class MessageType(StrEnum):
|
||||||
THINKING = "thinking"
|
THINKING = "thinking"
|
||||||
WORK_OUTPUT = "work_output"
|
WORK_OUTPUT = "work_output"
|
||||||
RESULT_OUTPUT = "result_output"
|
RESULT_OUTPUT = "result_output"
|
||||||
|
TEXT = "text"
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRunWarmUpEvent(BaseModel):
|
||||||
|
"""
|
||||||
|
智能体运行预热事件类
|
||||||
|
"""
|
||||||
|
|
||||||
|
content: str = Field(
|
||||||
|
default="",
|
||||||
|
description="预热内容",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Message(BaseModel):
|
class Message(BaseModel):
|
||||||
|
|
@ -132,11 +145,13 @@ class Flight(BaseModel):
|
||||||
return airport_code
|
return airport_code
|
||||||
|
|
||||||
|
|
||||||
class NoSelectedFlight(BaseModel):
|
class NoResult(BaseModel):
|
||||||
"""
|
"""
|
||||||
未选择到航班
|
无结果类
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class Deps(BaseModel):
|
class Deps(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|
@ -149,28 +164,28 @@ class Deps(BaseModel):
|
||||||
searched_flights: list[Flight] | None = Field(
|
searched_flights: list[Flight] | None = Field(
|
||||||
default=None, description="查询到的航班"
|
default=None, description="查询到的航班"
|
||||||
)
|
)
|
||||||
recommended_flight: Flight | NoSelectedFlight | None = Field(
|
extracted_flight_number: str | None = Field(
|
||||||
default=None, description="推荐航班"
|
default=None, description="提取到的航班号"
|
||||||
)
|
)
|
||||||
|
booked_flight: Flight | None = Field(default=None, description="预定到的航班")
|
||||||
|
|
||||||
|
|
||||||
# 主智能体
|
# 主智能体
|
||||||
agent = Agent[Deps, Flight | NoSelectedFlight | DeferredToolRequests](
|
agent = Agent[Deps, Flight | NoResult | DeferredToolRequests](
|
||||||
model=DEEPSEEK_V4_FLASH_MODEL,
|
model=DEEPSEEK_V4_FLASH_MODEL,
|
||||||
model_settings=MODEL_SETTINGS,
|
model_settings=MODEL_SETTINGS,
|
||||||
deps_type=Deps,
|
deps_type=Deps,
|
||||||
output_type=Flight | NoSelectedFlight | DeferredToolRequests,
|
output_type=Flight | NoResult | DeferredToolRequests,
|
||||||
system_prompt=(
|
system_prompt=(
|
||||||
"你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**:",
|
"你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**:",
|
||||||
"1. 使用 match_flight 匹配满足用户需求的航班;",
|
"1. 使用 search_flights 查询满足用户需求的航班;",
|
||||||
"2. 使用 select_flight 选择航班。若未选择到航班则返回 NoSelectedFlight;",
|
"2. 使用 book_flight 预定航班。若未选择到航班则返回 NoSelectedFlight;",
|
||||||
"3. 若选择到航班则使用 book_flight 预定航班。由用户确认后预定。",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# 提取智能体
|
# 提取所有航班信息智能体
|
||||||
extraction_agent = Agent[Deps, list[Flight]](
|
extraction_flights_agent = Agent[Deps, list[Flight]](
|
||||||
model=DEEPSEEK_V4_FLASH_MODEL,
|
model=DEEPSEEK_V4_FLASH_MODEL,
|
||||||
model_settings=MODEL_SETTINGS,
|
model_settings=MODEL_SETTINGS,
|
||||||
deps_type=Deps,
|
deps_type=Deps,
|
||||||
|
|
@ -183,6 +198,20 @@ extraction_agent = Agent[Deps, list[Flight]](
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 提取航班号智能体
|
||||||
|
extraction_flight_number_agent = Agent[Deps, str | None](
|
||||||
|
model=DEEPSEEK_V4_FLASH_MODEL,
|
||||||
|
model_settings=MODEL_SETTINGS,
|
||||||
|
deps_type=Deps,
|
||||||
|
output_type=str | None,
|
||||||
|
system_prompt=(
|
||||||
|
"你的任务是从用户提示词中提取航班号且须在查询到的航班中。",
|
||||||
|
"其中,航班号须由英文、数字和短横线组成、大写,例如 SFO-AK123。",
|
||||||
|
"若用户提示词中没有航班号则返回 None。",
|
||||||
|
"禁止编造。",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
source_material = """
|
source_material = """
|
||||||
1. Flight SFO-AK123
|
1. Flight SFO-AK123
|
||||||
|
|
@ -228,7 +257,7 @@ source_material = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@agent.tool
|
@agent.tool(name="查询航班")
|
||||||
async def search_flights(ctx: RunContext[Deps]) -> list[Flight]:
|
async def search_flights(ctx: RunContext[Deps]) -> list[Flight]:
|
||||||
"""
|
"""
|
||||||
查询航班
|
查询航班
|
||||||
|
|
@ -241,76 +270,76 @@ async def search_flights(ctx: RunContext[Deps]) -> list[Flight]:
|
||||||
**设计工具需先设计业务流程**
|
**设计工具需先设计业务流程**
|
||||||
本示例按照 查询航班 -> 推荐航班 -> 预定航班
|
本示例按照 查询航班 -> 推荐航班 -> 预定航班
|
||||||
"""
|
"""
|
||||||
extraction_agent_result = await extraction_agent.run(
|
# 提取到的所有航班信息
|
||||||
|
extracted_flights = (
|
||||||
|
await extraction_flights_agent.run(
|
||||||
f"来源资料:\n{source_material}",
|
f"来源资料:\n{source_material}",
|
||||||
deps=ctx.deps,
|
deps=ctx.deps,
|
||||||
usage=ctx.usage,
|
usage=ctx.usage,
|
||||||
usage_limits=UsageLimits(request_limit=10),
|
usage_limits=UsageLimits(request_limit=10),
|
||||||
)
|
)
|
||||||
if (
|
).output
|
||||||
extracted_flight_counts := len(
|
if not extracted_flights: # 模拟业务规则约束:提取到的所有航班信息不可能为空
|
||||||
extracted_flights := extraction_agent_result.output
|
raise ModelRetry(f"提取到的所有航班信息不可能为空")
|
||||||
)
|
|
||||||
) != 8: # 模拟业务规则约束
|
|
||||||
raise ModelRetry(
|
|
||||||
f"提取到的所有航班信息数量应为 8 ,不是 {extracted_flight_counts}"
|
|
||||||
)
|
|
||||||
# 查询到的航班
|
# 查询到的航班
|
||||||
ctx.deps.searched_flights = [
|
ctx.deps.searched_flights = sorted(
|
||||||
|
[
|
||||||
flight
|
flight
|
||||||
for flight in extracted_flights
|
for flight in extracted_flights
|
||||||
if flight.date == ctx.deps.date
|
if flight.date == ctx.deps.date
|
||||||
and flight.origin_airport_code == ctx.deps.origin_airport_code
|
and flight.origin_airport_code == ctx.deps.origin_airport_code
|
||||||
and flight.destination_airport_code == ctx.deps.destination_airport_code
|
and flight.destination_airport_code == ctx.deps.destination_airport_code
|
||||||
]
|
],
|
||||||
|
key=lambda flight: flight.airfare,
|
||||||
|
)
|
||||||
return ctx.deps.searched_flights
|
return ctx.deps.searched_flights
|
||||||
|
|
||||||
|
|
||||||
@agent.tool
|
@agent.tool(name="预定航班")
|
||||||
async def recommend_flight(ctx: RunContext[Deps]) -> Flight | NoSelectedFlight:
|
async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult:
|
||||||
"""
|
|
||||||
推荐航班
|
|
||||||
**如何提高提取准确率**
|
|
||||||
1. 系统提示词规则约束
|
|
||||||
2. 降低模型温度
|
|
||||||
3. 输出类型约定和输出模型校验
|
|
||||||
4. 业务规则约束(例如,输出不可为空列表)
|
|
||||||
"""
|
|
||||||
if (searched_flights := ctx.deps.searched_flights) is None:
|
|
||||||
raise ModelRetry("必须先使用 search_flights 查询航班")
|
|
||||||
ctx.deps.recommended_flight = (
|
|
||||||
min(searched_flights, key=lambda flight: flight.airfare)
|
|
||||||
if searched_flights
|
|
||||||
else NoSelectedFlight()
|
|
||||||
)
|
|
||||||
return ctx.deps.recommended_flight
|
|
||||||
|
|
||||||
|
|
||||||
@agent.tool
|
|
||||||
async def book_flight(ctx: RunContext[Deps]) -> Flight | NoSelectedFlight:
|
|
||||||
"""
|
"""
|
||||||
预定航班
|
预定航班
|
||||||
"""
|
"""
|
||||||
if (recommended_flight := ctx.deps.recommended_flight) is None:
|
if (searched_flights := ctx.deps.searched_flights) is None:
|
||||||
raise ModelRetry("必须先使用 recommend_flight 推荐航班")
|
raise ModelRetry("必须先使用 search_flights 查询航班")
|
||||||
|
|
||||||
if isinstance(recommended_flight, NoSelectedFlight):
|
|
||||||
return recommended_flight
|
|
||||||
else:
|
|
||||||
if not ctx.tool_call_approved:
|
if not ctx.tool_call_approved:
|
||||||
raise ApprovalRequired()
|
raise ApprovalRequired()
|
||||||
return recommended_flight
|
|
||||||
|
# 提取到的航班号
|
||||||
|
ctx.deps.extracted_flight_number = (
|
||||||
|
await extraction_flight_number_agent.run(
|
||||||
|
f"用户提示词:\n{ctx.prompt}\n查询到的航班:\n{(flight_numbers:= [flight.number for flight in searched_flights])}",
|
||||||
|
deps=ctx.deps,
|
||||||
|
usage=ctx.usage,
|
||||||
|
usage_limits=UsageLimits(request_limit=10),
|
||||||
|
)
|
||||||
|
).output
|
||||||
|
if not ctx.deps.extracted_flight_number:
|
||||||
|
return NoResult()
|
||||||
|
if ctx.deps.extracted_flight_number not in flight_numbers:
|
||||||
|
raise ModelRetry(
|
||||||
|
f"提取到的航班号 {ctx.deps.extracted_flight_number} 不在查询到的航班中"
|
||||||
|
)
|
||||||
|
# 预定到的航班
|
||||||
|
ctx.deps.booked_flight = next(
|
||||||
|
flight
|
||||||
|
for flight in searched_flights
|
||||||
|
if flight.number == ctx.deps.extracted_flight_number
|
||||||
|
)
|
||||||
|
return ctx.deps.booked_flight
|
||||||
|
|
||||||
|
|
||||||
@agent.output_validator
|
@agent.output_validator
|
||||||
async def validate_output(
|
async def validate_output(
|
||||||
ctx: RunContext[Deps], output: Flight | NoSelectedFlight | DeferredToolRequests
|
ctx: RunContext[Deps],
|
||||||
) -> Flight | NoSelectedFlight | DeferredToolRequests:
|
output: Flight | NoResult | DeferredToolRequests,
|
||||||
|
) -> Flight | NoResult | DeferredToolRequests:
|
||||||
"""
|
"""
|
||||||
输出校验
|
输出校验
|
||||||
"""
|
"""
|
||||||
# 不校验未选择到航班或延迟工具请求
|
if isinstance(output, (NoResult, DeferredToolRequests)):
|
||||||
if isinstance(output, NoSelectedFlight | DeferredToolRequests):
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
@ -335,14 +364,20 @@ async def run_stream_events(
|
||||||
message_history: list[ModelMessage],
|
message_history: list[ModelMessage],
|
||||||
usage: RunUsage,
|
usage: RunUsage,
|
||||||
deferred_tool_results: DeferredToolResults | None = None,
|
deferred_tool_results: DeferredToolResults | None = None,
|
||||||
) -> AsyncGenerator:
|
) -> AsyncGenerator[
|
||||||
# 构建消息
|
AgentStreamEvent
|
||||||
message = Message(
|
| AgentRunResultEvent
|
||||||
type=MessageType.WORK_OUTPUT,
|
| AgentRunWarmUpEvent
|
||||||
title="预定航班",
|
| Flight
|
||||||
is_running=True,
|
| NoResult
|
||||||
|
| DeferredToolRequests,
|
||||||
|
None,
|
||||||
|
]:
|
||||||
|
# 构建智能体运行预热事件
|
||||||
|
yield AgentRunWarmUpEvent(
|
||||||
|
content=f"正在预定 {deps.date.strftime('%Y-%m-%d')} 从 {deps.origin_airport_code} 到 {deps.destination_airport_code} 的航班"
|
||||||
)
|
)
|
||||||
yield message
|
tool_names = set()
|
||||||
async with agent.run_stream_events(
|
async with agent.run_stream_events(
|
||||||
user_prompt=user_prompt,
|
user_prompt=user_prompt,
|
||||||
deps=deps,
|
deps=deps,
|
||||||
|
|
@ -357,73 +392,35 @@ async def run_stream_events(
|
||||||
part=part,
|
part=part,
|
||||||
):
|
):
|
||||||
match part:
|
match part:
|
||||||
case ToolCallPart(
|
case ToolCallPart(tool_name=tool_name):
|
||||||
tool_name=tool_name, tool_call_id=tool_call_id
|
|
||||||
):
|
|
||||||
match tool_name:
|
match tool_name:
|
||||||
case "search_flights":
|
case "查询航班" | "预定航班":
|
||||||
if tool_name not in message.tasks:
|
if tool_name not in tool_names:
|
||||||
message.tasks[tool_name] = Task(
|
tool_names.add(tool_name)
|
||||||
tool_name=tool_name,
|
yield event
|
||||||
tool_call_id=tool_call_id,
|
|
||||||
title="正在查询航班",
|
|
||||||
)
|
|
||||||
yield message
|
|
||||||
|
|
||||||
case "recommend_flight":
|
|
||||||
if tool_name not in message.tasks:
|
|
||||||
message.tasks[tool_name] = Task(
|
|
||||||
tool_name=tool_name,
|
|
||||||
tool_call_id=tool_call_id,
|
|
||||||
title="正在推荐航班",
|
|
||||||
)
|
|
||||||
yield message
|
|
||||||
|
|
||||||
case "book_flight":
|
|
||||||
if tool_name not in message.tasks:
|
|
||||||
message.tasks[tool_name] = Task(
|
|
||||||
tool_name=tool_name,
|
|
||||||
tool_call_id=tool_call_id,
|
|
||||||
title="正在预定航班",
|
|
||||||
)
|
|
||||||
yield message
|
|
||||||
|
|
||||||
case FunctionToolResultEvent(part):
|
case FunctionToolResultEvent(part):
|
||||||
match part:
|
match part:
|
||||||
case ToolReturnPart(
|
case ToolReturnPart(
|
||||||
tool_name=tool_name,
|
tool_name=tool_name,
|
||||||
tool_call_id=tool_call_id,
|
|
||||||
content=content,
|
content=content,
|
||||||
):
|
):
|
||||||
match tool_name:
|
match tool_name:
|
||||||
case "search_flights":
|
case "查询航班":
|
||||||
task = message.tasks[tool_name]
|
# 查询到的航班
|
||||||
searched_flights = cast(list[Flight], content)
|
searched_flights = cast(list[Flight], content)
|
||||||
task.title = (
|
event.part.content = f"已查询到 **{len(searched_flights)} 班航班**:\n"
|
||||||
f"已查询到 {len(searched_flights)} 班航班"
|
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"
|
||||||
task.content = "\n".join(
|
yield event
|
||||||
[
|
|
||||||
f"{searched_flight.number} - {searched_flight.date.strftime('%Y-%m-%d')} 从 {searched_flight.origin_airport_code} 飞往 {searched_flight.destination_airport_code} - {searched_flight.airfare}"
|
|
||||||
for searched_flight in searched_flights
|
|
||||||
]
|
|
||||||
)
|
|
||||||
task.status = TaskStatus.DONE
|
|
||||||
yield message
|
|
||||||
|
|
||||||
case "recommend_flight":
|
case "预定航班":
|
||||||
task = message.tasks[tool_name]
|
if isinstance(content, NoResult):
|
||||||
recommended_flight = cast(
|
event.part.content = "未查询到符合条件的航班"
|
||||||
Flight | NoSelectedFlight, content
|
|
||||||
)
|
|
||||||
if isinstance(recommended_flight, Flight):
|
|
||||||
task.title = "为您推荐"
|
|
||||||
task.content = f"{recommended_flight.number} - {recommended_flight.date.strftime('%Y-%m-%d')} 从 {recommended_flight.origin_airport_code} 飞往 {recommended_flight.destination_airport_code} - {recommended_flight.airfare}"
|
|
||||||
else:
|
|
||||||
task.title = "为您推荐"
|
|
||||||
|
|
||||||
task.status = TaskStatus.DONE
|
if isinstance(content, Flight):
|
||||||
yield message
|
event.part.content = f"已预定航班:\n{content.number} {content.airfare} 于 {content.date.strftime('%Y-%m-%d')} 从 {content.origin_airport_code} 到 {content.destination_airport_code}\n"
|
||||||
|
yield event
|
||||||
|
|
||||||
case AgentRunResultEvent(
|
case AgentRunResultEvent(
|
||||||
result=result,
|
result=result,
|
||||||
|
|
@ -433,14 +430,8 @@ async def run_stream_events(
|
||||||
match output:
|
match output:
|
||||||
case DeferredToolRequests(approvals=approvals):
|
case DeferredToolRequests(approvals=approvals):
|
||||||
for approval in approvals:
|
for approval in approvals:
|
||||||
match approval:
|
output = approvals
|
||||||
case ToolCallPart(tool_name=tool_name):
|
yield event
|
||||||
task = message.tasks[tool_name]
|
|
||||||
task.content = "请确认是否预定 "
|
|
||||||
task.status = (
|
|
||||||
TaskStatus.PENDING_APPROVAL
|
|
||||||
)
|
|
||||||
yield message
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue