This commit is contained in:
parent
bc14d3aa16
commit
406c3877b1
|
|
@ -1,7 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from application.agents.chat_agent import chat_agent
|
||||
|
||||
|
||||
__all__ = [
|
||||
"chat_agent",
|
||||
]
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -3,9 +3,10 @@
|
|||
领域模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_ai import Agent, UsageLimits
|
||||
from pydantic_ai._uuid import uuid7
|
||||
from enum import StrEnum
|
||||
|
||||
|
|
@ -59,4 +60,3 @@ class TaskType(StrEnum):
|
|||
"""
|
||||
|
||||
CHAT = "chat"
|
||||
GENERATING_PRD = "generating_prd"
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
|
|||
"hr": lambda _: rx.divider(
|
||||
height="1px",
|
||||
margin="12px 0",
|
||||
background_color="var(--prismui-background-color-6)",
|
||||
background_color="var(--prismui-background-color-2)",
|
||||
),
|
||||
"table": lambda children: rx.el.table(
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ class AuthState(rx.State):
|
|||
self.is_captcha_sent = True
|
||||
|
||||
# 获取数据库状态
|
||||
if not self._db_state:
|
||||
self._db_state = await self.get_state(DatabaseState)
|
||||
|
||||
# 将倒计时和发送验证码事件添加至后台任务队列
|
||||
|
|
@ -230,11 +231,11 @@ class AuthState(rx.State):
|
|||
self.login_error_message = "请先阅读并同意协议和政策"
|
||||
return
|
||||
|
||||
if not self._db_state:
|
||||
return
|
||||
|
||||
self.is_logging_in = True
|
||||
|
||||
# 获取数据库状态
|
||||
if not self._db_state:
|
||||
return
|
||||
# 核验验证码
|
||||
if not await self._db_state.verify_captcha(
|
||||
email=self.email, captcha=self.captcha
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
"""
|
||||
会话状态
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
from pydantic_ai import Agent, ThinkingPartDelta, ModelMessage
|
||||
from typing import AsyncGenerator, Dict, List, Optional, Literal
|
||||
from pydantic_ai.messages import (
|
||||
FunctionToolCallEvent,
|
||||
FunctionToolResultEvent,
|
||||
|
|
@ -15,6 +13,7 @@ from pydantic_ai.messages import (
|
|||
TextPart,
|
||||
TextPartDelta,
|
||||
ThinkingPart,
|
||||
ThinkingPartDelta,
|
||||
ToolCallPart,
|
||||
ToolSearchCallPart,
|
||||
)
|
||||
|
|
@ -30,7 +29,7 @@ from application.domain_models import (
|
|||
Thought,
|
||||
TaskType,
|
||||
)
|
||||
from application.agents import chat_agent
|
||||
from application.tasks import run_stream_events
|
||||
|
||||
|
||||
class ConversationState(rx.State):
|
||||
|
|
@ -58,6 +57,15 @@ class ConversationState(rx.State):
|
|||
# 数据库状态(私有变量,不予序列化)
|
||||
_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:
|
||||
"""
|
||||
恢复当前用户会话状态
|
||||
|
|
@ -68,16 +76,14 @@ class ConversationState(rx.State):
|
|||
if not self.user_id:
|
||||
return
|
||||
|
||||
self._db_state = await self.get_state(DatabaseState)
|
||||
|
||||
# 获取数据库状态
|
||||
db_state = await self.get_db_state()
|
||||
# 获取会话字典
|
||||
self.conversations = await self._db_state.get_conversations(
|
||||
user_id=self.user_id
|
||||
)
|
||||
self.conversations = await db_state.get_conversations(user_id=self.user_id)
|
||||
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
|
||||
if not self.conversations:
|
||||
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()))
|
||||
|
|
@ -107,19 +113,16 @@ class ConversationState(rx.State):
|
|||
:param conversation_id: 需删除的会话唯一标识
|
||||
:return: None
|
||||
"""
|
||||
if not self._db_state:
|
||||
return
|
||||
|
||||
# 获取数据库状态
|
||||
db_state = await self.get_db_state()
|
||||
# 先设置会话记录为已删除再在会话字典中删除会话实例
|
||||
await self._db_state.set_conversations_record_deleted(
|
||||
conversation_id=conversation_id
|
||||
)
|
||||
await db_state.set_conversations_record_deleted(conversation_id=conversation_id)
|
||||
del self.conversations[conversation_id]
|
||||
|
||||
# 删除后,若会话字典为空则先创建会话记录再添加会话实例
|
||||
if not self.conversations:
|
||||
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
|
||||
"""
|
||||
# 获取数据库状态
|
||||
db_state = await self.get_db_state()
|
||||
# 创建会话记录再添加会话实例
|
||||
if not self._db_state:
|
||||
return
|
||||
|
||||
self.conversations.update(
|
||||
await self._db_state.create_conversations_record(
|
||||
await db_state.create_conversations_record(
|
||||
user_id=self.user_id, description="新会话"
|
||||
)
|
||||
)
|
||||
|
|
@ -198,32 +200,48 @@ class ConversationState(rx.State):
|
|||
return {}
|
||||
return conversation.dialogs
|
||||
|
||||
async def run_stream_events(
|
||||
self,
|
||||
conversation: Conversation,
|
||||
dialog: Dialog,
|
||||
agent: Agent,
|
||||
message_history: Optional[List[ModelMessage]] = None,
|
||||
) -> AsyncGenerator[None]:
|
||||
@rx.event
|
||||
async def handle_user_prompt(self) -> AsyncGenerator[None]:
|
||||
"""
|
||||
运行并处理流式事件
|
||||
:param agent: 代理
|
||||
:param conversation: 当前会话
|
||||
:param dialog: 当前对话
|
||||
:param message_history: 消息历史列表
|
||||
处理用户提示词
|
||||
:return: AsyncGenerator[None]
|
||||
"""
|
||||
if not self._db_state:
|
||||
if not self.user_prompt:
|
||||
return
|
||||
|
||||
# 获取数据库状态
|
||||
db_state = await self.get_db_state()
|
||||
# 当前会话
|
||||
conversation = self.conversations[self.conversation_id]
|
||||
# 将正在运行设置为是
|
||||
conversation.is_running = True
|
||||
# 创建对话记录再添加对话实例
|
||||
conversation.dialogs.update(
|
||||
await db_state.create_dialog_record(
|
||||
conversation_id=self.conversation_id, user_prompt=self.user_prompt
|
||||
)
|
||||
)
|
||||
# 将最后一个对话作为当前对话
|
||||
dialog = next(reversed(conversation.dialogs.values()))
|
||||
# 清空前端用户提示词
|
||||
self.user_prompt = ""
|
||||
# 强制更新会话并推送前端
|
||||
self.conversations[self.conversation_id] = conversation
|
||||
yield
|
||||
|
||||
# 获取消息历史列表
|
||||
message_history = await db_state.get_message_history(
|
||||
conversation_id=self.conversation_id
|
||||
)
|
||||
|
||||
# 初始化工具调用唯一标识和片段索引映射字典
|
||||
tool_call_ids: Dict[str, int] = {}
|
||||
async with agent.run_stream_events(
|
||||
conversation_id=self.conversation_id,
|
||||
# 获取运行流式输出事件
|
||||
async for event in run_stream_events(
|
||||
task_type=self.task_type,
|
||||
user_prompt=dialog.user_prompt,
|
||||
message_history=message_history,
|
||||
) as events:
|
||||
async for event in events:
|
||||
):
|
||||
match event:
|
||||
# ========== 开始事件 ==========
|
||||
case PartStartEvent(
|
||||
|
|
@ -308,7 +326,9 @@ class ConversationState(rx.State):
|
|||
match dialog.thoughts[index].type:
|
||||
# 工具检索
|
||||
case "tool_search":
|
||||
dialog.thoughts[index].content = "正在检索"
|
||||
dialog.thoughts[index].content = (
|
||||
f"正在检索 {part.args_as_json_str()}"
|
||||
)
|
||||
|
||||
# 能力加载
|
||||
case "capability_load":
|
||||
|
|
@ -337,23 +357,23 @@ class ConversationState(rx.State):
|
|||
|
||||
# 能力加载
|
||||
case "capability_load":
|
||||
dialog.thoughts[index].content = "已加载"
|
||||
dialog.thoughts[index].content = f"已加载 {content}"
|
||||
|
||||
# 工具调用
|
||||
case "tool_call":
|
||||
dialog.thoughts[index].content = f"已调用"
|
||||
dialog.thoughts[index].content = f"已调用 {content}"
|
||||
|
||||
# ========== 智能体运行结果事件 ==========
|
||||
case AgentRunResultEvent(result=result):
|
||||
# 获取数据库状态
|
||||
# 补全对话记录
|
||||
await self._db_state.complete_dialog_record(
|
||||
await db_state.complete_dialog_record(
|
||||
id=dialog.id,
|
||||
thoughts=dialog.thoughts,
|
||||
result_output=dialog.result_output,
|
||||
)
|
||||
# 创建结果记录
|
||||
await self._db_state.create_result_record(
|
||||
await db_state.create_result_record(
|
||||
conversation_id=self.conversation_id,
|
||||
dialog_id=dialog.id,
|
||||
new_messages=result.new_messages(),
|
||||
|
|
@ -365,66 +385,19 @@ class ConversationState(rx.State):
|
|||
self.conversations[self.conversation_id] = conversation
|
||||
yield
|
||||
|
||||
@rx.event
|
||||
async def handle_user_prompt(self) -> AsyncGenerator[None]:
|
||||
"""
|
||||
处理用户提示词
|
||||
:return: AsyncGenerator[None]
|
||||
"""
|
||||
if not self.user_prompt:
|
||||
return
|
||||
|
||||
if not self._db_state:
|
||||
return
|
||||
|
||||
# 当前会话
|
||||
conversation = self.conversations[self.conversation_id]
|
||||
# 将正在运行设置为是
|
||||
conversation.is_running = True
|
||||
# 创建对话记录再添加对话实例
|
||||
conversation.dialogs.update(
|
||||
await self._db_state.create_dialog_record(
|
||||
conversation_id=self.conversation_id, user_prompt=self.user_prompt
|
||||
)
|
||||
)
|
||||
# 将最后一个对话作为当前对话
|
||||
dialog = next(reversed(conversation.dialogs.values()))
|
||||
# 清空前端用户提示词
|
||||
self.user_prompt = ""
|
||||
# 强制更新会话并推送前端
|
||||
self.conversations[self.conversation_id] = conversation
|
||||
yield
|
||||
|
||||
match self.task_type:
|
||||
# 开放式对话
|
||||
case TaskType.CHAT:
|
||||
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:
|
||||
...
|
||||
|
||||
@rx.event
|
||||
async def generate_prd(self) -> AsyncGenerator[None]:
|
||||
"""
|
||||
生成产品需求文档
|
||||
"""
|
||||
if not self._db_state:
|
||||
return
|
||||
|
||||
# 当前会话
|
||||
conversation = self.conversations[self.conversation_id]
|
||||
|
||||
# 获取数据库状态
|
||||
db_state = await self.get_db_state()
|
||||
# 创建对话记录再添加对话实例
|
||||
conversation.dialogs.update(
|
||||
await self._db_state.create_dialog_record(
|
||||
await db_state.create_dialog_record(
|
||||
conversation_id=self.conversation_id, result_output="请输入产品需求"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 "未知任务类型"
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
Loading…
Reference in New Issue