This commit is contained in:
liubiren 2026-08-11 20:35:46 +08:00
parent a7fead671a
commit 9e0c58743a
11 changed files with 322 additions and 346 deletions

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: 0d3f238840fe
Revision ID: d52f1ee0f9c4
Revises:
Create Date: 2026-08-06 13:35:59.116161
Create Date: 2026-08-11 14:59:37.246745
"""
from typing import Sequence, Union
@ -12,7 +12,7 @@ import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '0d3f238840fe'
revision: str = 'd52f1ee0f9c4'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@ -23,11 +23,11 @@ def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('captcharecord',
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('captcha', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('code', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('is_valid', sa.Boolean(), nullable=False),
sa.Column('is_verified', sa.Boolean(), nullable=False),
sa.Column('expired_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('email', 'captcha', 'expired_at')
sa.PrimaryKeyConstraint('email', 'code', 'expired_at')
)
with op.batch_alter_table('captcharecord', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_captcharecord_is_valid'), ['is_valid'], unique=False)
@ -37,6 +37,7 @@ def upgrade() -> None:
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('usage', sa.JSON(), nullable=False),
sa.Column('is_deleted', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id')
@ -45,24 +46,26 @@ def upgrade() -> None:
batch_op.create_index(batch_op.f('ix_conversationrecord_is_deleted'), ['is_deleted'], unique=False)
batch_op.create_index(batch_op.f('ix_conversationrecord_user_id'), ['user_id'], unique=False)
op.create_table('dialogrecord',
op.create_table('messagerecord',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('user_prompt', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('thoughts', sa.JSON(), nullable=False),
sa.Column('result_output', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('usage', sa.JSON(), nullable=False),
sa.Column('type', sa.Enum('USER_PROMPT', 'THINKING', 'TOOL_CALL', 'RESULT_OUTPUT', name='messagetype'), nullable=False),
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('dialogrecord', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_dialogrecord_conversation_id'), ['conversation_id'], unique=False)
with op.batch_alter_table('messagerecord', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_messagerecord_conversation_id'), ['conversation_id'], unique=False)
op.create_table('resultrecord',
op.create_table('runrecord',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('dialog_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('new_messages', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('conversation_id', 'dialog_id')
sa.Column('new_messages', sa.JSON(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('runrecord', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_runrecord_conversation_id'), ['conversation_id'], unique=False)
op.create_table('userrecord',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
@ -81,11 +84,14 @@ def downgrade() -> None:
batch_op.drop_index(batch_op.f('ix_userrecord_email'))
op.drop_table('userrecord')
op.drop_table('resultrecord')
with op.batch_alter_table('dialogrecord', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_dialogrecord_conversation_id'))
with op.batch_alter_table('runrecord', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_runrecord_conversation_id'))
op.drop_table('dialogrecord')
op.drop_table('runrecord')
with op.batch_alter_table('messagerecord', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_messagerecord_conversation_id'))
op.drop_table('messagerecord')
with op.batch_alter_table('conversationrecord', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_conversationrecord_user_id'))
batch_op.drop_index(batch_op.f('ix_conversationrecord_is_deleted'))

View File

@ -3,14 +3,14 @@
会话页面
"""
import reflex as rx
from typing import Tuple
from application.states import AuthState, ConversationState
from application.states.models import (
ConversationHistoryItem,
MessageHistoryItem,
MessageType,
ConversationHistoryItem,
WorkFlowType,
)
from application.states import ConversationState, AuthState
def conversation_history_item(
@ -23,10 +23,9 @@ def conversation_history_item(
"""
# 高亮
is_highlight = item.id in (
ConversationState.is_conversation_history_item_popover_shown,
ConversationState.actived_conversation_id,
)
is_highlight = (
item.id == ConversationState.is_conversation_history_item_popover_shown
) | (item.id == ConversationState.actived_conversation_id)
return rx.list.item(
rx.vstack(
@ -227,13 +226,12 @@ def conversation_history(
)
def guidance() -> rx.Component:
def use_guidance() -> rx.Component:
"""
使用指
:return: Component
"""
return rx.vstack(
rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
@ -242,13 +240,12 @@ def guidance() -> rx.Component:
width="64px",
height="64px",
),
rx.box(
rx.text(
"棱镜球",
font_size="calc(var(--prismui-font-size-3) * 2)",
font_weight="var(--prismui-font-weight-3)",
letter_spacing="1px",
),
display="flex",
align_items="center",
gap="8px",
),
@ -263,41 +260,33 @@ def guidance() -> rx.Component:
width="100%",
text_align="center",
),
width="100%",
line_height="1.5",
gap="4px",
),
display="flex",
flex_direction="column",
width="100%",
align_items="center",
gap="12px",
),
# 演示案例
rx.vstack(
rx.hstack(
rx.text(
"演示案例",
margin_bottom="16px",
line_height="24px",
font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-3)",
),
display="flex",
justify_content="space-between",
align_items="center",
width="100%",
margin_bottom="16px",
),
rx.hstack(
rx.box(
"生成产品需求文档",
on_click=ConversationState.generate_prd,
"预定航班",
on_click=lambda: ConversationState.init_work_flow(
WorkFlowType.BOOK_FLIGHT
),
padding="10px 16px",
background_color="var(--prismui-background-color-6)",
border_radius="var(--prismui-border-radius-9)",
color="var(--prismui-color-10)",
cursor="pointer",
),
display="flex",
flex_wrap="wrap",
align_items="center",
gap="12px",
@ -307,29 +296,16 @@ def guidance() -> rx.Component:
background_color="var(--prismui-background-color-1)",
border_radius="var(--prismui-border-radius-7)",
),
display="flex",
flex_direction="column",
flex="1",
justify_content="center",
align_items="center",
gap="24px",
width="100%",
min_height="0",
margin="auto 0",
color="var(--prismui-color-text)",
),
display="flex",
flex="1",
flex_direction="column",
justify_content="flex-start",
gap="24px",
width="100%",
max_width="1200px",
height="100%",
padding="0 12px",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
margin="auto",
color="var(--prismui-color-text)",
)
@ -415,13 +391,29 @@ def thinking(item: MessageHistoryItem) -> rx.Component:
cursor="pointer",
),
# 思考的内容
rx.box(
rx.box(
rx.cond(
item.is_shown,
rx.text(
item.content,
width="100%",
margin="4px 0",
line_height="1.6",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
),
margin_bottom="8px",
rx.fragment(),
),
min_height="0",
),
display="grid",
grid_template_rows=rx.cond(item.is_shown, "1fr", "0fr"),
width="100%",
opacity=rx.cond(item.is_shown, "1", "0"),
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
overflow="hidden",
),
)
@ -553,29 +545,12 @@ def message_history_item(item: MessageHistoryItem) -> rx.Component:
"""
return rx.vstack(
# 若等待流式输出则显示加载动效,否则根据消息类型渲染组件
rx.cond(
ConversationState.awaiting_stream,
rx.box(
rx.box(
class_name="loadin",
flex_shrink="0",
),
display="flex",
justify_content="center",
align_items="center",
width="32px",
height="18px",
margin_bottom="8px",
overflow="hidden",
),
rx.match(
item.type,
(MessageType.USER_PROMPT, user_prompt(item)),
(MessageType.THINKING, thinking(item)),
(MessageType.RESULT_OUTPUT, result_output(item)),
),
),
padding="0 16px",
width="100%",
)
@ -587,16 +562,37 @@ def message_history() -> rx.Component:
:return: Component
"""
return rx.box(
# 若消息历史为空则显示,否则渲染消息历史项
# 若消息历史为空则显示使用指引,否则渲染消息历史项
rx.cond(
ConversationState.message_history.length() == 0,
guidance(),
use_guidance(),
rx.vstack(
rx.auto_scroll(
rx.vstack(
rx.foreach(
ConversationState.message_history,
message_history_item,
),
# 若等待流式输出则显示加载动效
rx.cond(
ConversationState.awaiting_stream,
rx.box(
rx.box(
class_name="awaiting_stream",
flex_shrink="0",
),
display="flex",
justify_content="flexstart",
align_items="center",
width="100%",
padding="0 16px",
margin_bottom="8px",
overflow="hidden",
),
rx.fragment(),
),
width="100%",
),
width="100%",
padding="0 12px",
padding_bottom="180px",
@ -616,6 +612,11 @@ def message_history() -> rx.Component:
overflow_y="auto",
),
),
display="flex",
flex_direction="column",
width="100%",
height="100%",
min_height="0",
)

View File

@ -136,9 +136,10 @@ class AuthState(rx.State):
发送验证码后台任务
:return: None
"""
db_state = await self.get_db_state()
async with self:
# 获取当前数据库状态
db_state = await self.get_db_state()
# 创建验证记录
captcha_code = await db_state.create_captcha_record(email=self.email)
@ -238,7 +239,7 @@ class AuthState(rx.State):
self.is_logging_in = True
# 获取数据库状态
# 获取当前数据库状态
db_state = await self.get_db_state()
# 核验验证码
@ -259,7 +260,7 @@ class AuthState(rx.State):
self.user_id = user_id
self.email = ""
self.captcha = ""
self.captcha_code = ""
self.is_captcha_code_sent = False
self.resend_captcha_code_countdown = 0
self.is_policies_agreed = True
@ -292,6 +293,6 @@ class AuthState(rx.State):
conversation_state = await self.get_state(ConversationState)
conversation_state.user_id = ""
conversation_state.conversations = {}
conversation_state.conversation_id = ""
conversation_state.actived_conversation_id = ""
conversation_state.is_conversation_history_shown = False
self.user_id = ""

View File

@ -301,7 +301,7 @@ class ConversationState(rx.State):
index_map_to_message_id: dict[int, str] = {}
# 初始化工具调用唯一标识集合
tool_call_ids: set[str] = set()
try:
# 匹配工作流类型
match conversation.work_flow:
# 预定航班
@ -314,15 +314,17 @@ class ConversationState(rx.State):
run_stream_events,
)
# 消息列表
messages: list[Message] = []
# 运行并流式输出事件
stream_events = run_stream_events(
user_prompt=user_prompt,
message_history=await db_state.get_message_history(
conversation_id=self.actived_conversation_id
),
usage=usage_validate_python(conversation.usage),
usage=usage_validate_python(dict(conversation.usage)),
)
try:
# 获取运行流式输出事件
async for event in stream_events:
# 将等待流式输出设置为否
@ -395,7 +397,15 @@ class ConversationState(rx.State):
]
message.is_running = False
message.title = "思考完成"
messages.append(message)
case TextPart(content=content):
# 获取消息实例
message = conversation.messages[
index_map_to_message_id[index]
]
message.content = content
messages.append(message)
# ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result):
@ -409,15 +419,9 @@ class ConversationState(rx.State):
new_messages=result.new_messages(),
)
if isinstance(message, Message):
# 强制更新会话并推送前端
self.conversations[self.actived_conversation_id] = conversation
yield
# 创建对话记录
await db_state.create_message_record(
conversation_id=conversation.id,
message=message,
)
except Exception as e:
...
@ -426,10 +430,15 @@ class ConversationState(rx.State):
conversation.is_running = False
# 将等待流式输出设置为否
conversation.awaiting_stream = False
self.conversations[self.actived_conversation_id] = conversation
yield
# 批量创建消息记录
await db_state.create_message_records(
conversation_id=conversation.id,
messages=messages,
)
@rx.event
async def init_work_flow(self, work_flow_type: WorkFlowType) -> None:
"""
@ -441,13 +450,12 @@ class ConversationState(rx.State):
return
match work_flow_type:
# 预定航班
# 预定航班工作流
case WorkFlowType.BOOK_FLIGHT:
from application.workshop.book_flight import init_work_flow
# 初始化预定航班任务
conversation.work_flow = init_work_flow()
conversation.user_prompt = f"帮我找一班从 {conversation.work_flow.deps.origin}{conversation.work_flow.deps.destination}{conversation.work_flow.deps.date} 的航班"
conversation.work_flow = (work_flow := init_work_flow())
@rx.event
def toggle_message_history_item_shown(self, message_id: str) -> None:

View File

@ -5,21 +5,21 @@
from datetime import datetime, timedelta
from random import choices
from typing import Any
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter, RunUsage
from pydantic_ai._uuid import uuid7
import reflex as rx
from sqlmodel import Field, JSON, SQLModel, select, update
from application.states.models import (
Conversation,
MessageType,
Message,
MessageType,
usage_dump_python,
)
class CaptchaRecord(SQLModel, table=True, table_name="captcha_code"):
class CaptchaRecord(SQLModel, table=True):
"""
验证码记录
"""
@ -47,7 +47,7 @@ class CaptchaRecord(SQLModel, table=True, table_name="captcha_code"):
)
class UserRecord(SQLModel, table=True, table_name="user"):
class UserRecord(SQLModel, table=True):
"""
用户记录
"""
@ -60,7 +60,7 @@ class UserRecord(SQLModel, table=True, table_name="user"):
email: str = Field(..., index=True, description="邮箱")
class ConversationRecord(SQLModel, table=True, table_name="conversation"):
class ConversationRecord(SQLModel, table=True):
"""
会话记录
"""
@ -85,7 +85,7 @@ class ConversationRecord(SQLModel, table=True, table_name="conversation"):
)
class MessageRecord(SQLModel, table=True, table_name="message"):
class MessageRecord(SQLModel, table=True):
"""
消息记录
"""
@ -101,7 +101,7 @@ class MessageRecord(SQLModel, table=True, table_name="message"):
content: str = Field(default="", description="消息内容")
class RunRecord(SQLModel, table=True, table_name="run"):
class RunRecord(SQLModel, table=True):
"""
运行记录
"""
@ -310,6 +310,29 @@ class DatabaseState(rx.State):
await session.refresh(record)
return {message.id: message}
async def create_message_records(
self,
conversation_id: str,
messages: list[Message],
) -> None:
"""
创建消息记录
:param conversation_id: 会话唯一标识
:param message: 消息实例
:return: 消息实例
"""
async with rx.asession() as session:
for message in messages:
record = MessageRecord(
id=message.id,
conversation_id=conversation_id,
type=message.type,
title=message.title,
content=message.content,
)
session.add(record)
await session.commit()
async def get_message_history(self, conversation_id: str) -> list[ModelMessage]:
"""
获取消息历史列表

View File

@ -2,9 +2,9 @@
"""
面向 reflex.state 的类
"""
from datetime import datetime
from enum import StrEnum
from typing import Any
from datetime import datetime
from pydantic import BaseModel, Field, TypeAdapter
from pydantic_ai import RunUsage
@ -40,30 +40,6 @@ class Message(BaseModel):
)
class RunStatus(StrEnum):
"""
运行状态枚举
"""
RUNNING = "running"
FINISHED = "finished"
class Run(BaseModel):
"""
运行类
"""
id: str = Field(default_factory=lambda: str(uuid7()), description="运行唯一标识")
messages: dict[str, Message] = Field(default_factory=dict, description="消息字典")
status: RunStatus = Field(default=RunStatus.RUNNING, description="运行状态")
usage: RunUsage = Field(default=RunUsage(), description="运行使用量")
usage_limits: UsageLimits | None = Field(default=None, description="运行使用量限制")
is_thinking: bool = Field(
default=False, description="正在思考True 表示正在思考False 表示未正在思考"
)
class WorkFlowType(StrEnum):
"""
工作流类型枚举
@ -77,7 +53,7 @@ class Deps(BaseModel):
依赖项类
"""
...
pass
class WorkFlow(BaseModel):
@ -86,7 +62,7 @@ class WorkFlow(BaseModel):
"""
type: WorkFlowType = Field(..., description="工作流类型")
deps: Deps | None = Field(default=None, description="工作流依赖项")
deps: Any = Field(default=None, description="工作流依赖项")
usage: RunUsage = Field(default=RunUsage(), description="工作流使用量")
usage_limits: UsageLimits | None = Field(
default=None,
@ -109,7 +85,10 @@ class Conversation(BaseModel):
is_running: bool = Field(
default=False, description="正在运行True 表示正在运行, False 表示运行结束"
)
awaiting_stream: bool = Field(default=False, description="等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成")
awaiting_stream: bool = Field(
default=False,
description="等待流式输出True 表示等待流式输出False 表示已开始流式输出或已完成",
)
class ConversationHistoryItem(BaseModel):
@ -139,28 +118,6 @@ class MessageHistoryItem(BaseModel):
)
# Dpes 适配器
DepsAdapter = TypeAdapter(Deps)
def deps_to_object(deps: dict[str, Any]) -> Deps | None:
"""
Deps 转为对象
"""
if not deps:
return None
return DepsAdapter.validate_python(deps)
def deps_to_dict(deps: Deps | None) -> dict[str, Any]:
"""
Deps 转为字典
"""
if not deps:
return {}
return DepsAdapter.dump_python(deps)
# Usage 适配器
UsageAdapter = TypeAdapter(RunUsage)
@ -185,21 +142,3 @@ def usage_dump_python(usage: RunUsage) -> dict[str, Any]:
# UsageLimits 适配器
UsageLimitsAdapter = TypeAdapter(UsageLimits)
def usage_limits_to_object(usage_limits: dict[str, Any]) -> UsageLimits | None:
"""
UsageLimits 转为对象
"""
if not usage_limits:
return None
return UsageLimitsAdapter.validate_python(usage_limits)
def usage_limits_to_dict(usage_limits: UsageLimits | None) -> dict[str, Any]:
"""
UsageLimits 转为字典
"""
if not usage_limits:
return {}
return UsageLimitsAdapter.dump_python(usage_limits)

View File

@ -1,105 +1,48 @@
# -*- coding: utf-8 -*-
"""
预定航班
预定航班范式
"""
import datetime
from typing import AsyncGenerator, Literal
from pydantic import BaseModel, Field
from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage
from pydantic_ai import Agent, ModelMessage, ModelRetry, RunContext, UsageLimits, RunUsage, DeferredToolRequests
from pydantic_ai.run import AgentRunResultEvent
from application.states.models import (
WorkFlow,
WorkFlowType,
usage_limits_to_object,
usage_to_dict,
usage_to_object,
Deps,
)
from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS
from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL, MODEL_SETTINGS_DISABLED_THINKING
class Deps_(BaseModel):
class Dependences(BaseModel):
"""
依赖
依赖
"""
flight_info: str = Field(..., description="所有航班信息")
date: datetime.date = Field(..., description="航班日期")
origin: str = Field(..., description="出发机场")
destination: str = Field(..., description="到达机场")
origin: str = Field(..., description="航班出发机场")
destination: str = Field(..., description="航班到达机场")
source_material: str = Field(..., description="航班来源资料")
class FlightDetail(BaseModel):
class Flight(BaseModel):
"""
航班详情
航班
"""
number: str = Field(description="航班号")
date: datetime.date = Field(description="航班日期")
origin: str = Field(description="出发机场")
destination: str = Field(description="到达机场")
price: int = Field(description="机票价格")
number: str = Field(..., description="航班号")
date: datetime.date = Field(..., description="航班日期")
origin: str = Field(..., description="航班出发机场")
destination: str = Field(..., description="航班到达机场")
airfare: int = Field(..., description="航班机票价格")
class NoFlightFound(BaseModel):
class FlightNoFound(BaseModel):
"""
未查询到航班类
"""
# 航班查询智能体
flight_search_agent = Agent[Deps, FlightDetail | NoFlightFound](
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
deps_type=Deps,
output_type=FlightDetail | NoFlightFound,
system_prompt="你的工作是在给定日期、出发机场和到达机场为用户找到最便宜的航班",
)
# 所有航班详情提取智能体
flight_details_extraction_agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
output_type=list[FlightDetail],
system_prompt="从给定文本中提取所有航班详细,包括航班号、航班日期、出发机场、到达机场和机票价格",
)
@flight_search_agent.tool
async def extract_flight_details(ctx: RunContext[Deps]) -> list[FlightDetail]:
"""
工具提取所有航班详情
"""
result = await flight_details_extraction_agent.run(
ctx.deps.flight_info, usage=ctx.usage
)
return result.output
@flight_search_agent.output_validator
async def validate_output(
ctx: RunContext[Deps], output: FlightDetail | NoFlightFound
) -> FlightDetail | NoFlightFound:
"""
输出校验航班详情
"""
if isinstance(output, NoFlightFound):
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}")
if errors:
raise ModelRetry("\n".join(errors))
return output
class SeatPreference(BaseModel):
"""
座位偏好类
@ -109,21 +52,77 @@ class SeatPreference(BaseModel):
column: Literal["A", "B", "C", "D", "E", "F"] = Field(description="座位列")
class NoSeatExtracted(BaseModel):
class SeatPreferenceNoExtracted(BaseModel):
"""
未提取到座位偏好类
"""
# 提取座位偏好智能体(无依赖项)
seat_preference_extraction_agent = Agent[object, SeatPreference | NoSeatExtracted](
# 航班查询智能体
flight_inquiry_agent = Agent[Dependences, Flight | FlightNoFound | DeferredToolRequests](
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
output_type=SeatPreference | NoSeatExtracted,
system_prompt="提取用户的座位偏好。座位规则说明A 座、F 座为靠窗座位;第 1 排是前排座位腿部空间更大14 排、20 排同样拥有加宽腿部空间",
model_settings=MODEL_SETTINGS_DISABLED_THINKING,
deps_type=Dependences,
output_type=Flight | FlightNoFound | DeferredToolRequests,
system_prompt="你的任务是根据给定的日期、出发机场和到达机场帮助用户找到最便宜的航班。",
)
flight_info = """
# 航班信息提取智能体
flights_extraction_agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS_DISABLED_THINKING,
output_type=list[Flight],
system_prompt="你的任务是根据给定的航班来源资料提取出所有航班信息,包括航班号、航班日期、航班出发机场、航班到达机场和航班机票价格。禁止编造。")
@flight_inquiry_agent.tool
async def extract_flight_details(ctx: RunContext[Dependences]) -> list[Flight]:
"""
提取所有航班信息
"""
result = await flights_extraction_agent.run(
ctx.deps.source_material, usage=ctx.usage
)
return result.output
@flight_inquiry_agent.output_validator
async def validate_output(
ctx: RunContext[Dependences], output: Flight | FlightNoFound | DeferredToolRequests
) -> Flight | FlightNoFound | DeferredToolRequests:
"""
输出校验航班信息
"""
# 不校验未查询到航班
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}")
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
- Origin: San Francisco International Airport (SFO)
@ -170,7 +169,7 @@ flight_info = """
def init_work_flow() -> WorkFlow:
return WorkFlow(
type=WorkFlowType.BOOK_FLIGHT,
deps=Deps(
deps=Deps_(
flight_info=flight_info,
date=datetime.date(2025, 1, 10),
origin="SFO",
@ -197,8 +196,7 @@ async def run_stream_events(
user_prompt=user_prompt,
deps=task.deps,
message_history=message_history,
usage=usage_to_object(dict(task.usage)),
usage_limits=usage_limits_to_object(dict(task.usage_limits)),
usage=usage,
) as events:
async for event in events:
if not isinstance(event, AgentRunResultEvent):

View File

@ -14,4 +14,4 @@ DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
),
)
MODEL_SETTINGS = ModelSettings(extra_body={"thinking": {"type": "disabled"}})
MODEL_SETTINGS_DISABLED_THINKING = ModelSettings(extra_body={"thinking": {"type": "disabled"}}) # 禁用思考模式

View File

@ -61,13 +61,13 @@ pre, pre code {
font-family: Consolas, "Microsoft YaHei", sans-serif !important;
}
.loadin {
.awaiting_stream {
width: 6px;
aspect-ratio: 1;
border-radius: 50%;
animation: loadin 0.9s infinite ease-in-out;
animation: awaiting_stream 0.9s infinite ease-in-out;
}
@keyframes loadin {
@keyframes awaiting_stream {
0% {
box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9);
background: var(--prismui-color-5);

Binary file not shown.

View File

@ -446,7 +446,7 @@
"@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
"arg": ["arg@5.0.2", "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],