This commit is contained in:
liubiren 2026-09-01 20:25:42 +08:00
parent 38b5b8289a
commit 7df4e51bbb
6 changed files with 166 additions and 251 deletions

View File

@ -9,7 +9,7 @@ from application.states.models import (
ConversationHistoryItem, ConversationHistoryItem,
MessageHistoryItem, MessageHistoryItem,
MessageType, MessageType,
WorkType, WorkflowType,
) )
@ -278,8 +278,9 @@ def use_guidance() -> rx.Component:
rx.hstack( rx.hstack(
rx.box( rx.box(
"预定航班", "预定航班",
on_click=lambda: ConversationState.init_work( # 点击事件:设置工作流为预定航班
WorkType.BOOK_FLIGHT on_click=lambda: ConversationState.set_workflow(
WorkflowType.BOOK_FLIGHT
), ),
padding="10px 16px", padding="10px 16px",
background_color="var(--prismui-background-color-6)", background_color="var(--prismui-background-color-6)",

View File

@ -2,8 +2,8 @@
""" """
会话状态 会话状态
""" """
from typing import AsyncGenerator
from datetime import datetime from datetime import datetime
from typing import AsyncGenerator
from pydantic_ai.messages import ( from pydantic_ai.messages import (
FunctionToolCallEvent, FunctionToolCallEvent,
@ -21,19 +21,20 @@ from pydantic_ai.messages import (
) )
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
import reflex as rx import reflex as rx
from pydantic_ai._uuid import uuid7
from application.states.database import DatabaseState from application.states.database import DatabaseState
from application.states.models import ( from application.states.models import (
Conversation,
Message,
MessageType,
Work,
WorkType,
TaskStatus,
AgentRunWarmUpEvent, AgentRunWarmUpEvent,
Conversation,
ConversationHistoryItem, ConversationHistoryItem,
usage_validate_python, Message,
MessageHistoryItem, MessageHistoryItem,
MessageType,
Workflow,
WorkflowType,
deps_dump_python,
deps_validate_python,
usage_validate_python,
) )
@ -270,7 +271,7 @@ class ConversationState(rx.State):
async def run(self) -> AsyncGenerator[None]: async def run(self) -> AsyncGenerator[None]:
""" """
运行 运行
:return: AsyncGenerator[None] :return: AsyncGenerator
""" """
# 当前会话 # 当前会话
conversation = self.conversations.get(self.actived_conversation_id) conversation = self.conversations.get(self.actived_conversation_id)
@ -306,20 +307,28 @@ class ConversationState(rx.State):
# 初始化片段索引映射为消息实例唯一标识字典 # 初始化片段索引映射为消息实例唯一标识字典
index_map_to_message_id: dict[int, str] = {} index_map_to_message_id: dict[int, str] = {}
# 对话 # 初始化工作流
if not conversation.work: if conversation.workflow:
match conversation.workflow.type:
case WorkflowType.BOOK_FLIGHT:
from application.workshop.book_flight import (
run_stream_events,
)
# 运行并流式输出事件
stream_events = run_stream_events(
deps=deps_validate_python(dict(conversation.workflow.deps)),
user_prompt=user_prompt,
message_history=await db_state.get_message_history(
conversation_id=self.actived_conversation_id
),
usage=usage_validate_python(dict(conversation.usage)),
)
else:
from application.workshop.talk import ( from application.workshop.talk import (
run_stream_events, run_stream_events,
) )
else:
# 匹配工作类型
match conversation.work.type:
# 预定航班
case WorkType.BOOK_FLIGHT:
from application.workshop.book_flight import run_stream_events
# 消息列表
messages: list[Message] = []
# 运行并流式输出事件 # 运行并流式输出事件
stream_events = run_stream_events( stream_events = run_stream_events(
user_prompt=user_prompt, user_prompt=user_prompt,
@ -328,11 +337,14 @@ class ConversationState(rx.State):
), ),
usage=usage_validate_python(dict(conversation.usage)), usage=usage_validate_python(dict(conversation.usage)),
) )
# 消息列表
messages: list[Message] = []
# 获取运行流式输出事件 # 获取运行流式输出事件
async for event in stream_events: async for event in stream_events:
# 将等待流式输出设置为否 # 若等待流式输出则将等待流式输出设置为否
if conversation.awaiting_stream:
conversation.awaiting_stream = False conversation.awaiting_stream = False
message: Message | None = None
match event: match event:
# ========== 开始事件 ========== # ========== 开始事件 ==========
case PartStartEvent( case PartStartEvent(
@ -451,22 +463,27 @@ class ConversationState(rx.State):
) )
@rx.event @rx.event
async def init_work(self, work_type: WorkType) -> None: async def set_workflow(self, type: WorkflowType) -> None:
""" """
初始化工作 设置工作流
""" """
# 当前会话 # 当前会话
conversation = self.conversations.get(self.actived_conversation_id) conversation = self.conversations.get(self.actived_conversation_id)
if not conversation: if not conversation:
return return
match work_type: match type:
# 预定航班工作 case WorkflowType.BOOK_FLIGHT:
case WorkType.BOOK_FLIGHT: from application.workshop.book_flight import deps
from application.workshop.book_flight import init_work
# 初始化预定航班任务 # 初始化预定航班工作流
conversation.work = (work := init_work()) workflow = Workflow(type=type, deps=deps_dump_python(deps))
# 设置用户提示词
self.set_user_prompt(
f"帮我查询并预定在 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班"
)
conversation.workflow = workflow
@rx.event @rx.event
def toggle_message_history_item_shown(self, message_id: str) -> None: def toggle_message_history_item_shown(self, message_id: str) -> None:

View File

@ -15,7 +15,7 @@ from application.states.models import (
Conversation, Conversation,
Message, Message,
MessageType, MessageType,
usage_dump_python, usage_validate_json,
) )
@ -72,8 +72,10 @@ class ConversationRecord(SQLModel, table=True):
) )
user_id: str = Field(..., index=True, description="用户唯一标识") user_id: str = Field(..., index=True, description="用户唯一标识")
description: str = Field(default="新会话", description="会话描述") description: str = Field(default="新会话", description="会话描述")
usage: dict[str, Any] = Field( usage: str = Field(default="", description="会话使用量")
default_factory=dict, sa_type=JSON, description="会话使用量" workflow: str = Field(
default="",
description="会话工作流",
) )
is_deleted: bool = Field( is_deleted: bool = Field(
default=False, default=False,
@ -85,6 +87,20 @@ class ConversationRecord(SQLModel, table=True):
) )
class RunRecord(SQLModel, table=True):
"""
运行记录
"""
id: str = Field(
default_factory=lambda: str(uuid7()),
primary_key=True,
description="运行唯一标识",
)
conversation_id: str = Field(..., index=True, description="会话唯一标识")
new_messages: str = Field(default="", sa_type=JSON, description="新增消息")
class MessageRecord(SQLModel, table=True): class MessageRecord(SQLModel, table=True):
""" """
消息记录 消息记录
@ -101,22 +117,6 @@ class MessageRecord(SQLModel, table=True):
content: str = Field(default="", description="消息内容") content: str = Field(default="", description="消息内容")
class RunRecord(SQLModel, table=True):
"""
运行记录
"""
id: str = Field(
default_factory=lambda: str(uuid7()),
primary_key=True,
description="运行唯一标识",
)
conversation_id: str = Field(..., index=True, description="会话唯一标识")
new_messages: str = Field(
default_factory=list, sa_type=JSON, description="新增消息"
)
class DatabaseState(rx.State): class DatabaseState(rx.State):
""" """
数据库状态 数据库状态
@ -220,9 +220,13 @@ class DatabaseState(rx.State):
Conversation( Conversation(
id=conversation_record.id, id=conversation_record.id,
description=conversation_record.description, description=conversation_record.description,
usage=conversation_record.usage, usage=usage_validate_json(
messages={}, conversation_record.usage
), # 将 conversation_record.usage 由 JSON 字符串反序列化为 RunUsage
created_at=conversation_record.created_at, created_at=conversation_record.created_at,
workflow=_validate_json(
conversation_record.workflow
), # 将 conversation_record.workflow 由 JSON 字符串反序列化为 Workflow,
), ),
) )
if not message_record: if not message_record:
@ -371,7 +375,8 @@ class DatabaseState(rx.State):
conversation_id=conversation_id, conversation_id=conversation_id,
new_messages=ModelMessagesTypeAdapter.dump_json( new_messages=ModelMessagesTypeAdapter.dump_json(
new_messages new_messages
).decode(), # 将 messages 由 List[ModelMessage] 序列化为 JSON 字符串 ).decode(),
# 将 messages 由 List[ModelMessage] 序列化为 JSON 字符串
) )
) )
await session.commit() await session.commit()

View File

@ -12,28 +12,15 @@ from pydantic_ai._uuid import uuid7
from pydantic_ai.usage import UsageLimits from pydantic_ai.usage import UsageLimits
class TaskStatus(StrEnum): class AgentRunWarmUpEvent(BaseModel):
""" """
任务状态枚举 智能体运行预热事件类
""" """
RUNNING = "running" content: str = Field(
DONE = "done" default="",
PENDING_APPROVAL = "pending_approval" description="预热内容",
ERROR = "error" )
class Task(BaseModel):
"""
任务类
"""
id: str = Field(default_factory=lambda: str(uuid7()), description="任务唯一标识")
tool_name: str = Field(..., description="工具名称")
tool_call_id: str = Field(..., description="工具调用唯一标识")
status: TaskStatus = Field(default=TaskStatus.RUNNING, description="任务状态")
title: str = Field(default="", description="任务标题")
content: str = Field(default="", description="任务内容")
class MessageType(StrEnum): class MessageType(StrEnum):
@ -57,7 +44,6 @@ class Message(BaseModel):
type: MessageType = Field(..., description="消息类型") type: MessageType = Field(..., description="消息类型")
title: str = Field(default="", description="消息标题") title: str = Field(default="", description="消息标题")
content: str = Field(default="", description="消息内容") content: str = Field(default="", description="消息内容")
tasks: list[Task] = Field(default_factory=list, description="任务列表")
is_running: bool = Field( is_running: bool = Field(
default=False, description="正在运行True 表示正在运行, False 表示运行结束" default=False, description="正在运行True 表示正在运行, False 表示运行结束"
) )
@ -66,18 +52,7 @@ class Message(BaseModel):
) )
class AgentRunWarmUpEvent(BaseModel): class WorkflowType(StrEnum):
"""
智能体运行预热事件类
"""
content: str = Field(
default="",
description="预热内容",
)
class WorkType(StrEnum):
""" """
工作类型枚举 工作类型枚举
""" """
@ -85,18 +60,13 @@ class WorkType(StrEnum):
BOOK_FLIGHT = "预定航班" BOOK_FLIGHT = "预定航班"
class Work(BaseModel): class Workflow(BaseModel):
""" """
工作流类 工作流类
""" """
type: WorkType = Field(..., description="工作类型") type: WorkflowType = Field(..., description="工作类型")
deps: Any = Field(default=None, description="工作依赖项") deps: dict[str, Any] = Field(default_factory=dict, description="工作依赖项")
usage: RunUsage = Field(default=RunUsage(), description="工作使用量")
usage_limits: UsageLimits | None = Field(
default=None,
description="工作使用量限制",
)
class Conversation(BaseModel): class Conversation(BaseModel):
@ -106,10 +76,9 @@ class Conversation(BaseModel):
id: str = Field(..., description="会话唯一标识") id: str = Field(..., description="会话唯一标识")
description: str = Field(..., description="会话描述") description: str = Field(..., description="会话描述")
work: Work | None = Field(default=None, description="工作")
user_prompt: str = Field(default="", description="用户提示词") user_prompt: str = Field(default="", description="用户提示词")
usage: dict[str, Any] = Field(..., description="会话使用量") usage: RunUsage = Field(..., description="会话使用量")
messages: dict[str, Message] = Field(..., description="消息字典") messages: dict[str, Message] = Field(default_factory=dict, description="消息字典")
created_at: datetime = Field(..., description="会话创建日期时间") created_at: datetime = Field(..., description="会话创建日期时间")
is_running: bool = Field( is_running: bool = Field(
default=False, description="正在运行True 表示正在运行, False 表示运行结束" default=False, description="正在运行True 表示正在运行, False 表示运行结束"
@ -118,6 +87,7 @@ class Conversation(BaseModel):
default=False, default=False,
description="等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成", description="等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成",
) )
workflow: Workflow | None = Field(default=None, description="工作流")
class ConversationHistoryItem(BaseModel): class ConversationHistoryItem(BaseModel):
@ -136,27 +106,45 @@ class MessageHistoryItem(Message):
""" """
# Usage 适配器 # 依赖项适配器
DepsAdapter = TypeAdapter(Any)
def deps_validate_python(deps: dict[str, Any]) -> Any:
"""
将依赖项反序列化
"""
if not deps:
return None
return DepsAdapter.validate_python(deps)
def deps_dump_python(deps: Any) -> dict[str, Any]:
"""
将依赖项序列化
:param deps: 依赖项
:return: python 字典
"""
return DepsAdapter.dump_python(deps)
# 使用量RunUsage适配器
UsageAdapter = TypeAdapter(RunUsage) UsageAdapter = TypeAdapter(RunUsage)
def usage_validate_python(usage: dict[str, Any]) -> RunUsage: def usage_validate_json(usage: str) -> RunUsage:
""" """
Usage 反序列化 使用量反序列化
""" """
if not usage: if not usage:
return RunUsage() return RunUsage()
return UsageAdapter.validate_python(usage) return UsageAdapter.validate_json(usage)
def usage_dump_python(usage: RunUsage) -> dict[str, Any]: def usage_dump_json(usage: RunUsage) -> dict[str, Any]:
""" """
Usage 序列化 使用量序列化
:param usage: 使用量 :param usage: 使用量
:return: python 字典 :return: python 字典
""" """
return UsageAdapter.dump_python(usage) return UsageAdapter.dump_python(usage)
# UsageLimits 适配器
UsageLimitsAdapter = TypeAdapter(UsageLimits)

View File

@ -2,11 +2,10 @@
""" """
预定航班工作流范式 预定航班工作流范式
""" """
import asyncio
import datetime import datetime
from typing import AsyncGenerator, cast from typing import AsyncGenerator, cast
from pydantic import BaseModel, Field, field_validator, TypeAdapter from pydantic import BaseModel, Field, field_validator
from pydantic_ai import ( from pydantic_ai import (
Agent, Agent,
ApprovalRequired, ApprovalRequired,
@ -14,112 +13,25 @@ from pydantic_ai import (
DeferredToolResults, DeferredToolResults,
ModelMessage, ModelMessage,
ModelRetry, ModelRetry,
ModelSettings,
RunContext, RunContext,
RunUsage, RunUsage,
UsageLimits, UsageLimits,
) )
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.run import AgentRunResultEvent, AgentRunResult
from enum import StrEnum
from pydantic_ai._uuid import uuid7
from pydantic_ai.messages import ( from pydantic_ai.messages import (
AgentStreamEvent, AgentStreamEvent,
FunctionToolCallEvent,
FunctionToolResultEvent, FunctionToolResultEvent,
LoadCapabilityCallPart,
PartDeltaEvent,
PartEndEvent,
PartStartEvent, PartStartEvent,
TextPart,
TextPartDelta,
ThinkingPart,
ThinkingPartDelta,
ToolCallPart, ToolCallPart,
ToolSearchCallPart,
ToolReturnPart, ToolReturnPart,
) )
from pydantic_ai.run import AgentRunResult, AgentRunResultEvent
from application.states.models import AgentRunWarmUpEvent
class TaskStatus(StrEnum): from application.workshop.models import (
""" DEEPSEEK_V4_FLASH_MODEL,
任务状态枚举 DEEPSEEK_V4_FLASH_MODEL_DISABLED_THINKING,
"""
RUNNING = "running"
DONE = "done"
ERROR = "error"
PENDING_APPROVAL = "pending_approval"
class Task(BaseModel):
"""
任务类
"""
tool_name: str = Field(..., description="工具名称")
tool_call_id: str = Field(..., description="工具调用唯一标识")
status: TaskStatus = Field(default=TaskStatus.RUNNING, description="任务状态")
title: str = Field(default="", description="任务标题")
content: str = Field(default="", description="任务内容")
class MessageType(StrEnum):
"""
消息类型枚举
"""
USER_PROMPT = "user_prompt"
THINKING = "thinking"
WORK_OUTPUT = "work_output"
RESULT_OUTPUT = "result_output"
TEXT = "text"
class AgentRunWarmUpEvent(BaseModel):
"""
智能体运行预热事件类
"""
content: str = Field(
default="",
description="预热内容",
)
class Message(BaseModel):
"""
消息类
"""
id: str = Field(default_factory=lambda: str(uuid7()), description="消息唯一标识")
type: MessageType = Field(..., description="消息类型")
title: str = Field(default="", description="消息标题")
content: str = Field(default="", description="消息内容")
tasks: dict[str, Task] = Field(
default_factory=dict, description="任务字典,键为工具调用唯一标识"
)
is_running: bool = Field(
default=False, description="正在运行True 表示正在运行False 表示运行完成"
)
is_shown: bool = Field(
default=False, description="展示组件True 表示展示False 表示隐藏"
)
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 Flight(BaseModel): class Flight(BaseModel):
""" """
@ -173,7 +85,7 @@ class Deps(BaseModel):
# 主智能体 # 主智能体
agent = Agent[Deps, Flight | NoResult | DeferredToolRequests]( agent = Agent[Deps, Flight | NoResult | DeferredToolRequests](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_MODEL_DISABLED_THINKING,
deps_type=Deps, deps_type=Deps,
output_type=Flight | NoResult | DeferredToolRequests, output_type=Flight | NoResult | DeferredToolRequests,
system_prompt=( system_prompt=(
@ -187,7 +99,7 @@ agent = Agent[Deps, Flight | NoResult | DeferredToolRequests](
# 提取所有航班信息智能体 # 提取所有航班信息智能体
extraction_flights_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=DEEPSEEK_V4_FLASH_MODEL_DISABLED_THINKING,
deps_type=Deps, deps_type=Deps,
output_type=list[Flight], output_type=list[Flight],
system_prompt=( system_prompt=(
@ -201,7 +113,7 @@ extraction_flights_agent = Agent[Deps, list[Flight]](
# 提取航班号智能体 # 提取航班号智能体
extraction_flight_number_agent = Agent[Deps, str | None]( extraction_flight_number_agent = Agent[Deps, str | None](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_MODEL_DISABLED_THINKING,
deps_type=Deps, deps_type=Deps,
output_type=str | None, output_type=str | None,
system_prompt=( system_prompt=(
@ -358,21 +270,29 @@ async def validate_output(
return output return output
deps = Deps(
date=datetime.date(2025, 1, 10),
origin_airport_code="SFO",
destination_airport_code="ANC",
)
async def run_stream_events( async def run_stream_events(
deps: Deps, deps: Deps,
user_prompt: str, user_prompt: str,
message_history: list[ModelMessage], message_history: list[ModelMessage],
usage: RunUsage, usage: RunUsage,
usage_limits: UsageLimits | None = None,
deferred_tool_results: DeferredToolResults | None = None, deferred_tool_results: DeferredToolResults | None = None,
) -> AsyncGenerator[ ) -> AsyncGenerator[
AgentStreamEvent AgentStreamEvent
| AgentRunResultEvent | AgentRunResultEvent[str | DeferredToolRequests]
| AgentRunWarmUpEvent | AgentRunWarmUpEvent
| Flight | None,
| NoResult
| DeferredToolRequests,
None,
]: ]:
"""
运行并流式输出事件
"""
# 构建智能体运行预热事件 # 构建智能体运行预热事件
yield AgentRunWarmUpEvent( yield AgentRunWarmUpEvent(
content=f"正在预定 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班" content=f"正在预定 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班"
@ -383,10 +303,10 @@ async def run_stream_events(
deps=deps, deps=deps,
message_history=message_history, message_history=message_history,
usage=usage, usage=usage,
usage_limits=usage_limits,
deferred_tool_results=deferred_tool_results, deferred_tool_results=deferred_tool_results,
) as events: ) as events:
async for event in events: async for event in events:
print(event)
match event: match event:
case PartStartEvent( case PartStartEvent(
part=part, part=part,
@ -416,7 +336,9 @@ async def run_stream_events(
case "预定航班": case "预定航班":
if isinstance(content, NoResult): if isinstance(content, NoResult):
event.part.content = "未查询到符合条件的航班" event.part.content = (
"未提取到航班号,请检查后重试"
)
if isinstance(content, Flight): if isinstance(content, Flight):
event.part.content = f"已预定航班:\n{content.number} {content.airfare}{content.date.strftime('%Y-%m-%d')}{content.origin_airport_code}{content.destination_airport_code}\n" event.part.content = f"已预定航班:\n{content.number} {content.airfare}{content.date.strftime('%Y-%m-%d')}{content.origin_airport_code}{content.destination_airport_code}\n"
@ -428,35 +350,15 @@ async def run_stream_events(
match result: match result:
case AgentRunResult(output=output): case AgentRunResult(output=output):
match output: match output:
case Flight():
yield AgentRunResultEvent(
result=AgentRunResult(output="预定成功")
)
case DeferredToolRequests(approvals=approvals): case DeferredToolRequests(approvals=approvals):
for approval in approvals: yield AgentRunResultEvent(
output = approvals result=AgentRunResult(
yield event output=DeferredToolRequests(
approvals=approvals
)
async def main():
# 实例化依赖
deps = Deps(
date=datetime.date(2025, 1, 10),
origin_airport_code="SFO",
destination_airport_code="ANC",
) )
user_prompt = f"帮我查询并预定在 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班"
message_history: list[ModelMessage] = []
events = run_stream_events(
deps=deps,
user_prompt=user_prompt,
message_history=message_history,
usage=RunUsage(),
) )
# 消费异步生成器
async for event in events:
print()
if isinstance(event, AgentRunResultEvent):
# 保存本次运行新增消息
message_history.extend(event.result.new_messages())
if isinstance(event, Message):
print(event)
asyncio.run(main())

View File

@ -14,4 +14,6 @@ DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
), ),
) )
MODEL_SETTINGS_DISABLED_THINKING = ModelSettings(extra_body={"thinking": {"type": "disabled"}}) # 禁用思考模式 DEEPSEEK_V4_FLASH_MODEL_DISABLED_THINKING = ModelSettings(
temperature=0, extra_body={"thinking": {"type": "disabled"}} # 温度控制
) # 不启用思考模式