This commit is contained in:
liubiren 2026-08-05 20:29:26 +08:00
parent bc14d3aa16
commit 406c3877b1
11 changed files with 456 additions and 302 deletions

View File

@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
from application.agents.chat_agent import chat_agent
__all__ = [
"chat_agent",
]

View File

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
"""
开放式对话智能体
"""
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
instructions: str = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
# 实例化智能体
chat_agent: Agent = Agent(
name="chat_agent",
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=None,
output_type=str,
retries=1,
)

View File

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
"""
生成产品需求文档智能体
"""
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
instructions: str = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
# 实例化智能体
chat_agent: Agent = Agent(
name="chat_agent",
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=None,
output_type=str,
retries=1,
)

View File

@ -3,9 +3,10 @@
领域模型 领域模型
""" """
from datetime import datetime from datetime import datetime
from typing import Dict, List from typing import Dict, List, Optional, Callable, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic_ai import Agent, UsageLimits
from pydantic_ai._uuid import uuid7 from pydantic_ai._uuid import uuid7
from enum import StrEnum from enum import StrEnum
@ -59,4 +60,3 @@ class TaskType(StrEnum):
""" """
CHAT = "chat" CHAT = "chat"
GENERATING_PRD = "generating_prd"

View File

@ -578,7 +578,7 @@ def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
"hr": lambda _: rx.divider( "hr": lambda _: rx.divider(
height="1px", height="1px",
margin="12px 0", margin="12px 0",
background_color="var(--prismui-background-color-6)", background_color="var(--prismui-background-color-2)",
), ),
"table": lambda children: rx.el.table( "table": lambda children: rx.el.table(
children, children,

View File

@ -173,7 +173,8 @@ class AuthState(rx.State):
self.is_captcha_sent = True self.is_captcha_sent = True
# 获取数据库状态 # 获取数据库状态
self._db_state = await self.get_state(DatabaseState) if not self._db_state:
self._db_state = await self.get_state(DatabaseState)
# 将倒计时和发送验证码事件添加至后台任务队列 # 将倒计时和发送验证码事件添加至后台任务队列
yield type(self).countdown() yield type(self).countdown()
@ -230,11 +231,11 @@ class AuthState(rx.State):
self.login_error_message = "请先阅读并同意协议和政策" self.login_error_message = "请先阅读并同意协议和政策"
return return
if not self._db_state:
return
self.is_logging_in = True self.is_logging_in = True
# 获取数据库状态
if not self._db_state:
return
# 核验验证码 # 核验验证码
if not await self._db_state.verify_captcha( if not await self._db_state.verify_captcha(
email=self.email, captcha=self.captcha email=self.email, captcha=self.captcha

View File

@ -2,9 +2,7 @@
""" """
会话状态 会话状态
""" """
from datetime import datetime from typing import AsyncGenerator, Dict, List, Optional, Literal
from typing import Any, AsyncGenerator, Dict, List, Optional
from pydantic_ai import Agent, ThinkingPartDelta, ModelMessage
from pydantic_ai.messages import ( from pydantic_ai.messages import (
FunctionToolCallEvent, FunctionToolCallEvent,
FunctionToolResultEvent, FunctionToolResultEvent,
@ -15,6 +13,7 @@ from pydantic_ai.messages import (
TextPart, TextPart,
TextPartDelta, TextPartDelta,
ThinkingPart, ThinkingPart,
ThinkingPartDelta,
ToolCallPart, ToolCallPart,
ToolSearchCallPart, ToolSearchCallPart,
) )
@ -30,7 +29,7 @@ from application.domain_models import (
Thought, Thought,
TaskType, TaskType,
) )
from application.agents import chat_agent from application.tasks import run_stream_events
class ConversationState(rx.State): class ConversationState(rx.State):
@ -58,6 +57,15 @@ class ConversationState(rx.State):
# 数据库状态(私有变量,不予序列化) # 数据库状态(私有变量,不予序列化)
_db_state: Optional[DatabaseState] = None _db_state: Optional[DatabaseState] = None
async def get_db_state(self) -> DatabaseState:
"""
获取数据库状态
:return: 数据库状态
"""
if not self._db_state:
self._db_state = await self.get_state(DatabaseState)
return self._db_state
async def resume(self, user_id: str) -> None: async def resume(self, user_id: str) -> None:
""" """
恢复当前用户会话状态 恢复当前用户会话状态
@ -68,16 +76,14 @@ class ConversationState(rx.State):
if not self.user_id: if not self.user_id:
return return
self._db_state = await self.get_state(DatabaseState) # 获取数据库状态
db_state = await self.get_db_state()
# 获取会话字典 # 获取会话字典
self.conversations = await self._db_state.get_conversations( self.conversations = await db_state.get_conversations(user_id=self.user_id)
user_id=self.user_id
)
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例 # 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
if not self.conversations: if not self.conversations:
self.conversations.update( self.conversations.update(
await self._db_state.create_conversations_record(user_id=self.user_id) await db_state.create_conversations_record(user_id=self.user_id)
) )
# 将最后一个会话作为当前会话并更新会话唯一标识 # 将最后一个会话作为当前会话并更新会话唯一标识
self.conversation_id = next(reversed(self.conversations.keys())) self.conversation_id = next(reversed(self.conversations.keys()))
@ -107,19 +113,16 @@ class ConversationState(rx.State):
:param conversation_id: 需删除的会话唯一标识 :param conversation_id: 需删除的会话唯一标识
:return: None :return: None
""" """
if not self._db_state: # 获取数据库状态
return db_state = await self.get_db_state()
# 先设置会话记录为已删除再在会话字典中删除会话实例 # 先设置会话记录为已删除再在会话字典中删除会话实例
await self._db_state.set_conversations_record_deleted( await db_state.set_conversations_record_deleted(conversation_id=conversation_id)
conversation_id=conversation_id
)
del self.conversations[conversation_id] del self.conversations[conversation_id]
# 删除后,若会话字典为空则先创建会话记录再添加会话实例 # 删除后,若会话字典为空则先创建会话记录再添加会话实例
if not self.conversations: if not self.conversations:
self.conversations.update( self.conversations.update(
await self._db_state.create_conversations_record(user_id=self.user_id) await db_state.create_conversations_record(user_id=self.user_id)
) )
# 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识 # 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识
@ -141,12 +144,11 @@ class ConversationState(rx.State):
创建会话 创建会话
:return: None :return: None
""" """
# 获取数据库状态
db_state = await self.get_db_state()
# 创建会话记录再添加会话实例 # 创建会话记录再添加会话实例
if not self._db_state:
return
self.conversations.update( self.conversations.update(
await self._db_state.create_conversations_record( await db_state.create_conversations_record(
user_id=self.user_id, description="新会话" user_id=self.user_id, description="新会话"
) )
) )
@ -198,173 +200,6 @@ class ConversationState(rx.State):
return {} return {}
return conversation.dialogs return conversation.dialogs
async def run_stream_events(
self,
conversation: Conversation,
dialog: Dialog,
agent: Agent,
message_history: Optional[List[ModelMessage]] = None,
) -> AsyncGenerator[None]:
"""
运行并处理流式事件
:param agent: 代理
:param conversation: 当前会话
:param dialog: 当前对话
:param message_history: 消息历史列表
:return: AsyncGenerator[None]
"""
if not self._db_state:
return
# 初始化工具调用唯一标识和片段索引映射字典
tool_call_ids: Dict[str, int] = {}
async with agent.run_stream_events(
conversation_id=self.conversation_id,
user_prompt=dialog.user_prompt,
message_history=message_history,
) as events:
async for event in events:
match event:
# ========== 开始事件 ==========
case PartStartEvent(
index=index,
part=part,
previous_part_kind=previous_part_kind,
):
match part:
# 思考分片开始事件
case ThinkingPart(content=content):
# 若上一分片种类为空则将正在思考设置为正在思考
if not previous_part_kind:
dialog.is_thinking = True
dialog.thoughts[index] = Thought(
type="thinking", content=content
)
# 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id):
# 创建工具调用唯一标识与片段索引映射
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="tool_search",
content="正在生成检索关键词",
)
# 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="capability_load",
content="正在生成加载参数",
)
# 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="tool_call",
content="正在生成调用参数",
)
# 文本分片开始事件
case TextPart(content=content):
dialog.result_output = content
# ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta):
match delta:
# 思考分片增量事件
case ThinkingPartDelta(
content_delta=content_delta,
):
dialog.thoughts[index].content += content_delta or ""
# 文本分片增量事件
case TextPartDelta(
content_delta=content_delta,
):
dialog.result_output += content_delta
# ========== 结束事件 ==========
case PartEndEvent(
index=index,
part=part,
next_part_kind=next_part_kind,
):
match part:
# 思考分片结束事件
case ThinkingPart(content=content):
# 若下一分片种类为文本则将思考状态设置为思考完成
if next_part_kind == "text":
dialog.is_thinking = False
# ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
# 获取分片索引
index = tool_call_ids[tool_call_id]
match dialog.thoughts[index].type:
# 工具检索
case "tool_search":
dialog.thoughts[index].content = "正在检索"
# 能力加载
case "capability_load":
dialog.thoughts[index].content = (
f"正在加载能力 {part.tool_name}"
)
# 工具调用
case "tool_call":
dialog.thoughts[index].content = (
f"正在调用工具 {part.tool_name}"
)
# ========== 函数工具结果事件 ==========
case FunctionToolResultEvent(
tool_call_id=tool_call_id,
content=content,
):
index = tool_call_ids[tool_call_id]
match dialog.thoughts[index].type:
# 工具检索
case "tool_search":
dialog.thoughts[index].content = (
content if isinstance(content, str) else ""
) # 暂仅考虑文本内容
# 能力加载
case "capability_load":
dialog.thoughts[index].content = "已加载"
# 工具调用
case "tool_call":
dialog.thoughts[index].content = f"已调用"
# ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result):
# 获取数据库状态
# 补全对话记录
await self._db_state.complete_dialog_record(
id=dialog.id,
thoughts=dialog.thoughts,
result_output=dialog.result_output,
)
# 创建结果记录
await self._db_state.create_result_record(
conversation_id=self.conversation_id,
dialog_id=dialog.id,
new_messages=result.new_messages(),
)
# 将正在运行设置为否
conversation.is_running = False
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
@rx.event @rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]: async def handle_user_prompt(self) -> AsyncGenerator[None]:
""" """
@ -374,16 +209,15 @@ class ConversationState(rx.State):
if not self.user_prompt: if not self.user_prompt:
return return
if not self._db_state: # 获取数据库状态
return db_state = await self.get_db_state()
# 当前会话 # 当前会话
conversation = self.conversations[self.conversation_id] conversation = self.conversations[self.conversation_id]
# 将正在运行设置为是 # 将正在运行设置为是
conversation.is_running = True conversation.is_running = True
# 创建对话记录再添加对话实例 # 创建对话记录再添加对话实例
conversation.dialogs.update( conversation.dialogs.update(
await self._db_state.create_dialog_record( await db_state.create_dialog_record(
conversation_id=self.conversation_id, user_prompt=self.user_prompt conversation_id=self.conversation_id, user_prompt=self.user_prompt
) )
) )
@ -395,36 +229,175 @@ class ConversationState(rx.State):
self.conversations[self.conversation_id] = conversation self.conversations[self.conversation_id] = conversation
yield yield
match self.task_type: # 获取消息历史列表
# 开放式对话 message_history = await db_state.get_message_history(
case TaskType.CHAT: conversation_id=self.conversation_id
async for _ in self.run_stream_events( )
conversation=conversation,
dialog=dialog,
agent=chat_agent,
message_history=await self._db_state.get_message_history(
conversation_id=self.conversation_id
),
):
yield
case TaskType.GENERATING_PRD: # 初始化工具调用唯一标识和片段索引映射字典
... tool_call_ids: Dict[str, int] = {}
# 获取运行流式输出事件
async for event in run_stream_events(
task_type=self.task_type,
user_prompt=dialog.user_prompt,
message_history=message_history,
):
match event:
# ========== 开始事件 ==========
case PartStartEvent(
index=index,
part=part,
previous_part_kind=previous_part_kind,
):
match part:
# 思考分片开始事件
case ThinkingPart(content=content):
# 若上一分片种类为空则将正在思考设置为正在思考
if not previous_part_kind:
dialog.is_thinking = True
dialog.thoughts[index] = Thought(
type="thinking", content=content
)
# 工具检索分片开始事件
case ToolSearchCallPart(tool_call_id=tool_call_id):
# 创建工具调用唯一标识与片段索引映射
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="tool_search",
content="正在生成检索关键词",
)
# 能力加载分片开始事件
case LoadCapabilityCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="capability_load",
content="正在生成加载参数",
)
# 工具调用分片开始事件
case ToolCallPart(tool_call_id=tool_call_id):
tool_call_ids[tool_call_id] = index
dialog.thoughts[index] = Thought(
type="tool_call",
content="正在生成调用参数",
)
# 文本分片开始事件
case TextPart(content=content):
dialog.result_output = content
# ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta):
match delta:
# 思考分片增量事件
case ThinkingPartDelta(
content_delta=content_delta,
):
dialog.thoughts[index].content += content_delta or ""
# 文本分片增量事件
case TextPartDelta(
content_delta=content_delta,
):
dialog.result_output += content_delta
# ========== 结束事件 ==========
case PartEndEvent(
index=index,
part=part,
next_part_kind=next_part_kind,
):
match part:
# 思考分片结束事件
case ThinkingPart(content=content):
# 若下一分片种类为文本则将思考状态设置为思考完成
if next_part_kind == "text":
dialog.is_thinking = False
# ========== 函数工具调用事件 ==========
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
# 获取分片索引
index = tool_call_ids[tool_call_id]
match dialog.thoughts[index].type:
# 工具检索
case "tool_search":
dialog.thoughts[index].content = (
f"正在检索 {part.args_as_json_str()}"
)
# 能力加载
case "capability_load":
dialog.thoughts[index].content = (
f"正在加载能力 {part.tool_name}"
)
# 工具调用
case "tool_call":
dialog.thoughts[index].content = (
f"正在调用工具 {part.tool_name}"
)
# ========== 函数工具结果事件 ==========
case FunctionToolResultEvent(
tool_call_id=tool_call_id,
content=content,
):
index = tool_call_ids[tool_call_id]
match dialog.thoughts[index].type:
# 工具检索
case "tool_search":
dialog.thoughts[index].content = (
content if isinstance(content, str) else ""
) # 暂仅考虑文本内容
# 能力加载
case "capability_load":
dialog.thoughts[index].content = f"已加载 {content}"
# 工具调用
case "tool_call":
dialog.thoughts[index].content = f"已调用 {content}"
# ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result):
# 获取数据库状态
# 补全对话记录
await db_state.complete_dialog_record(
id=dialog.id,
thoughts=dialog.thoughts,
result_output=dialog.result_output,
)
# 创建结果记录
await db_state.create_result_record(
conversation_id=self.conversation_id,
dialog_id=dialog.id,
new_messages=result.new_messages(),
)
# 将正在运行设置为否
conversation.is_running = False
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
@rx.event @rx.event
async def generate_prd(self) -> AsyncGenerator[None]: async def generate_prd(self) -> AsyncGenerator[None]:
""" """
生成产品需求文档 生成产品需求文档
""" """
if not self._db_state:
return
# 当前会话 # 当前会话
conversation = self.conversations[self.conversation_id] conversation = self.conversations[self.conversation_id]
# 获取数据库状态
db_state = await self.get_db_state()
# 创建对话记录再添加对话实例 # 创建对话记录再添加对话实例
conversation.dialogs.update( conversation.dialogs.update(
await self._db_state.create_dialog_record( await db_state.create_dialog_record(
conversation_id=self.conversation_id, result_output="请输入产品需求" conversation_id=self.conversation_id, result_output="请输入产品需求"
) )
) )

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
任务模块
"""
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 application.domain_models import Dialog, TaskType
from models import DEEPSEEK_V4_FLASH_MODEL
instruction = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
async def run_stream_events(
task_type: TaskType,
user_prompt: str,
message_history: List[ModelMessage],
) -> AsyncGenerator:
"""
以流式事件模式运行
"""
match task_type:
case TaskType.CHAT:
agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL,
instructions=instruction,
)
async with agent.run_stream_events(
user_prompt=user_prompt,
message_history=message_history,
) as event_stream:
async for event in event_stream:
yield event
case "flight":
yield "未知任务类型"

View File

@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
"""
生成产品需求文档智能体
"""
from dataclasses import dataclass
import datetime
from enum import Enum
from typing import Any, AsyncGenerator, Optional, Literal
from pydantic import Any, BaseModel, Enum, Field, Optional
from pydantic_ai import (
Agent,
ModelMessage,
ModelRetry,
RunContext,
RunUsage,
UsageLimits,
)
from pydantic_ai.usage import RunUsage
from models import DEEPSEEK_V4_FLASH_MODEL
#from application.tasks.agent import Agent
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
class FlightDetails(BaseModel):
"""Details of the most suitable flight."""
flight_number: str
price: int
origin: str = Field(description='Three-letter airport code')
destination: str = Field(description='Three-letter airport code')
date: datetime.date
class NoFlightFound(BaseModel):
"""When no valid flight is found."""
@dataclass
class Deps:
web_page_text: str
req_origin: str
req_destination: str
req_date: datetime.date
# This agent is responsible for controlling the flow of the conversation.
search_agent= Agent[Deps, FlightDetails | NoFlightFound](
model=DEEPSEEK_V4_FLASH_MODEL,
output_type=FlightDetails | NoFlightFound,
deps_type=Deps,
retries=4,
system_prompt=(
'Your job is to find the cheapest flight for the user on the given date. '
),
)
# This agent is responsible for extracting flight details from web page text.
extraction_agent = Agent(
output_type=list[FlightDetails],
system_prompt='Extract all the flight details from the given text.',
)
@search_agent.tool
async def extract_flights(ctx: RunContext[Deps]) -> list[FlightDetails]:
"""Get details of all flights."""
# we pass the usage to the search agent so requests within this agent are counted
result = await extraction_agent.run(ctx.deps.web_page_text, usage=ctx.usage)
return result.output
@search_agent.output_validator
async def validate_output(
ctx: RunContext[Deps], output: FlightDetails | NoFlightFound
) -> FlightDetails | NoFlightFound:
"""Procedural validation that the flight meets the constraints."""
if isinstance(output, NoFlightFound):
return output
errors: list[str] = []
if output.origin != ctx.deps.req_origin:
errors.append(
f'Flight should have origin {ctx.deps.req_origin}, not {output.origin}'
)
if output.destination != ctx.deps.req_destination:
errors.append(
f'Flight should have destination {ctx.deps.req_destination}, not {output.destination}'
)
if output.date != ctx.deps.req_date:
errors.append(f'Flight should be on {ctx.deps.req_date}, not {output.date}')
if errors:
raise ModelRetry('\n'.join(errors))
else:
return output
class SeatPreference(BaseModel):
row: int = Field(ge=1, le=30)
seat: Literal['A', 'B', 'C', 'D', 'E', 'F']
class Failed(BaseModel):
"""Unable to extract a seat selection."""
# This agent is responsible for extracting the user's seat selection
seat_preference_agent = Agent[object, SeatPreference | Failed](
'openai:gpt-5.2',
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. '
),
)
# in reality this would be downloaded from a booking site,
# potentially using another agent to navigate the site
flights_web_page = """
1. Flight SFO-AK123
- Price: $350
- Origin: San Francisco International Airport (SFO)
- Destination: Ted Stevens Anchorage International Airport (ANC)
- Date: January 10, 2025
2. Flight SFO-AK456
- Price: $370
- Origin: San Francisco International Airport (SFO)
- Destination: Fairbanks International Airport (FAI)
- Date: January 10, 2025
3. Flight SFO-AK789
- Price: $400
- Origin: San Francisco International Airport (SFO)
- Destination: Juneau International Airport (JNU)
- Date: January 20, 2025
4. Flight NYC-LA101
- Price: $250
- Origin: San Francisco International Airport (SFO)
- Destination: Ted Stevens Anchorage International Airport (ANC)
- Date: January 10, 2025
5. Flight CHI-MIA202
- Price: $200
- Origin: Chicago O'Hare International Airport (ORD)
- Destination: Miami International Airport (MIA)
- Date: January 12, 2025
6. Flight BOS-SEA303
- Price: $120
- Origin: Boston Logan International Airport (BOS)
- Destination: Ted Stevens Anchorage International Airport (ANC)
- Date: January 12, 2025
7. Flight DFW-DEN404
- Price: $150
- Origin: Dallas/Fort Worth International Airport (DFW)
- Destination: Denver International Airport (DEN)
- Date: January 10, 2025
8. Flight ATL-HOU505
- Price: $180
- Origin: Hartsfield-Jackson Atlanta International Airport (ATL)
- Destination: George Bush Intercontinental Airport (IAH)
- Date: January 10, 2025
"""
# restrict how many requests this app can make to the LLM
usage_limits = UsageLimits(request_limit=15)
async def flight_booking(
state: FlightFlowState,
user_prompt: Optional[str]
) -> AsyncGenerator:
if state.stage == FlowStage.EXEC:
prompt = f'Find me a flight from {state.deps.req_origin} to {state.deps.req_destination} on {state.deps.req_date}'
run_result = None
# 1. 模型流式查询航班
async with search_agent.run_stream_events(
user_prompt=prompt,
deps=state.deps,
message_history=state.message_history,
usage_limits=state.usage_limits,
) as stream:
async for evt in stream:
yield evt
run_result = stream.result
# 2. 保存本轮模型对话到数据库(核心:上下文持久化,防止断裂)
if run_result is not None:
# 写入对话历史下一轮get_message_history可以读到航班内容
await db_state.create_result_record(
conversation_id=state.conversation_id,
dialog_id=state.dialog_id,
new_messages=run_result.new_messages(),
)
# 用量回填
state.usage = run_result.usage
# 3. 判断业务结果分支
if isinstance(run_result.output, NoFlightFound):
# 无航班场景
state.stage = FlowStage.FINISH
yield AgentStreamEvent.text_event("未找到符合条件的航班,预订流程结束")
return
else:
# ✅ 查询到航班,推送业务选择提示(前端展示按钮/文字提示)
tip_text = "已查询到航班,请回复 buy 购票 / search 重新查询"
yield AgentStreamEvent.text_event(tip_text)
# 【可选】如果需要让AI记住选择指令把提示作为系统消息入库
# await db_state.append_system_message(conversation_id=state.conversation_id, content=tip_text)
# 阶段挂起等待用户输入buy/search不直接结束流程
state.stage = FlowStage.WAIT_USER_INPUT
return

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
智能体相关模块
"""
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
)

Binary file not shown.