This commit is contained in:
parent
9e0c58743a
commit
5b5f41be49
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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,
|
||||
)
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue