This commit is contained in:
parent
2502d2f663
commit
a8dc684c20
|
|
@ -370,14 +370,12 @@ def thinking(item: MessageHistoryItem) -> rx.Component:
|
|||
rx.text(
|
||||
item.title,
|
||||
font_size="var(--prismui-font-size-1)",
|
||||
color="var(--prismui-color-3)",
|
||||
class_name=rx.cond(item.is_running, "is_running", None),
|
||||
),
|
||||
rx.icon(
|
||||
"chevron-right",
|
||||
width="14px",
|
||||
height="14px",
|
||||
color="var(--prismui-color-3)",
|
||||
transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"),
|
||||
transition="transform 0.18s ease-in-out",
|
||||
),
|
||||
|
|
@ -442,57 +440,102 @@ def tool_call(item: MessageHistoryItem) -> rx.Component:
|
|||
margin_bottom="8px",
|
||||
line_height="22px",
|
||||
),
|
||||
# 内容
|
||||
rx.markdown(
|
||||
item.content,
|
||||
width="100%",
|
||||
margin="4px 0",
|
||||
line_height="1.6",
|
||||
font_size="var(--prismui-font-size-1)",
|
||||
),
|
||||
# 审批
|
||||
rx.cond(
|
||||
item.has_approval,
|
||||
rx.hstack(
|
||||
rx.text_area(
|
||||
placeholder="请输入",
|
||||
# 值绑定审批用户提示词
|
||||
value=item.approval_user_prompt,
|
||||
# 值变化事件:设置审批用户提示词
|
||||
on_change=lambda user_prompt: ConversationState.set_approval_user_prompt(
|
||||
item.id, user_prompt
|
||||
),
|
||||
flex=1,
|
||||
height="100%",
|
||||
padding="4px",
|
||||
border_radius="var(--prismui-border-radius-4)",
|
||||
line_height="16px",
|
||||
border="none !important",
|
||||
rx.hstack(
|
||||
# 分割线
|
||||
rx.box(
|
||||
width="1px",
|
||||
margin="12px 7px",
|
||||
background_color="var(--prismui-background-color-2)",
|
||||
),
|
||||
rx.vstack(
|
||||
# 内容
|
||||
rx.markdown(
|
||||
item.content,
|
||||
width="100%",
|
||||
margin="4px 0",
|
||||
line_height="1.6",
|
||||
font_size="var(--prismui-font-size-1)",
|
||||
),
|
||||
rx.button(
|
||||
"发送",
|
||||
# 点击事件:审批
|
||||
on_click=lambda: ConversationState.approve(item.id, True),
|
||||
# 是否禁用绑定验证码发送禁用状态
|
||||
margin="0 8px",
|
||||
background_color="var(--prismui-background-color-1)",
|
||||
line_height="16px",
|
||||
# 若验证码发送禁用状态为是则颜色设置为 color-4,否则设置为 color-5(主题色)
|
||||
color=rx.cond(
|
||||
AuthState.is_captcha_code_sending_button_disabled,
|
||||
"var(--prismui-color-4)",
|
||||
"var(--prismui-color-5)",
|
||||
# 审批
|
||||
rx.cond(
|
||||
item.has_approval,
|
||||
rx.vstack(
|
||||
rx.text_area(
|
||||
# 值绑定审批用户提示词
|
||||
value=item.approval_user_prompt,
|
||||
# 值变化事件:设置审批用户提示词
|
||||
on_change=lambda user_prompt: ConversationState.set_approval_user_prompt(
|
||||
item.id, user_prompt
|
||||
),
|
||||
disabled=item.is_approval_user_prompt_input_disabled,
|
||||
placeholder="请输入",
|
||||
vertical_align="middle",
|
||||
width="100%",
|
||||
height="auto",
|
||||
padding="4px 0",
|
||||
outline="none !important",
|
||||
box_shadow="none !important",
|
||||
style={
|
||||
".rt-TextAreaInput": {
|
||||
"font-size": "var(--prismui-font-size-1) !important",
|
||||
"border": "none !important",
|
||||
"overflow-y": "hidden !important",
|
||||
},
|
||||
".rt-TextAreaInput:disabled": {
|
||||
"background-color": "var(--prismui-background-color-1) !important",
|
||||
"font-size": "var(--prismui-font-size-1) !important",
|
||||
"cursor": "not-allowed !important",
|
||||
},
|
||||
},
|
||||
),
|
||||
rx.hstack(
|
||||
rx.button(
|
||||
rx.cond(
|
||||
item.is_approval_user_prompt_input_disabled,
|
||||
"已提交",
|
||||
"提交",
|
||||
),
|
||||
# 点击事件:审批
|
||||
on_click=lambda: ConversationState.approve(item.id),
|
||||
disabled=item.is_approval_user_prompt_input_disabled,
|
||||
padding="0 12px",
|
||||
# 是否禁用绑定审批用户提示词不可输出状态
|
||||
background_color=rx.cond(
|
||||
item.is_approval_user_prompt_input_disabled,
|
||||
"var(--prismui-background-color-8)",
|
||||
"var(--prismui-background-color-7)",
|
||||
),
|
||||
border_radius="var(--prismui-border-radius-3)",
|
||||
line_height="18px",
|
||||
font_size="var(--prismui-font-size-1)",
|
||||
color="var(--prismui-color-9)",
|
||||
cursor=rx.cond(
|
||||
item.is_approval_user_prompt_input_disabled,
|
||||
"not-allowed",
|
||||
"pointer",
|
||||
),
|
||||
),
|
||||
justify_content="flex-end",
|
||||
width="100%",
|
||||
),
|
||||
width="100%",
|
||||
padding="12px 16px",
|
||||
background_color="var(--prismui-background-color-1)",
|
||||
border="var(--prismui-border-1)",
|
||||
border_radius="var(--prismui-border-radius-4)",
|
||||
),
|
||||
rx.fragment(),
|
||||
),
|
||||
width="100%",
|
||||
height="45px",
|
||||
align_items="center",
|
||||
border="var(--prismui-border-1)",
|
||||
border_radius="var(--prismui-border-radius-4)",
|
||||
gap="4px",
|
||||
),
|
||||
rx.fragment(),
|
||||
align_items="stretch",
|
||||
width="100%",
|
||||
margin_bottom="8px",
|
||||
gap="4px",
|
||||
),
|
||||
width="100%",
|
||||
margin_bottom="8px",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -828,17 +871,16 @@ def user_prompt_sending() -> rx.Component:
|
|||
inline_height="1.5",
|
||||
font_size="var(--prismui-font-size-2)",
|
||||
white_space="nowrap",
|
||||
overflow="hidden",
|
||||
cursor=rx.cond(
|
||||
ConversationState.is_user_prompt_sending_button_disabled,
|
||||
"not-allowed",
|
||||
"pointer",
|
||||
),
|
||||
),
|
||||
width="100%",
|
||||
justify_content="flex-end",
|
||||
align_items="center",
|
||||
height="32px",
|
||||
width="100%",
|
||||
),
|
||||
width="100%",
|
||||
padding="12px 16px",
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@
|
|||
会话状态
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator, cast
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pydantic_ai import (
|
||||
DeferredToolRequests,
|
||||
DeferredToolResults,
|
||||
ToolApproved,
|
||||
ToolDenied,
|
||||
)
|
||||
from pydantic_ai import DeferredToolRequests, DeferredToolResults, ToolApproved
|
||||
from pydantic_ai.messages import (
|
||||
FunctionToolResultEvent,
|
||||
PartDeltaEvent,
|
||||
|
|
@ -110,6 +105,7 @@ class ConversationState(rx.State):
|
|||
)
|
||||
# 将最后一个会话的唯一标识作为激活会话唯一标识
|
||||
self.actived_conversation_id = next(reversed(self.conversations.keys()))
|
||||
print(self.conversations[self.actived_conversation_id])
|
||||
|
||||
@rx.event
|
||||
def toggle_conversation_history_shown(self) -> None:
|
||||
|
|
@ -193,6 +189,11 @@ class ConversationState(rx.State):
|
|||
**message.model_dump(),
|
||||
has_approval=isinstance(approval := message.approval, Approval),
|
||||
approval_user_prompt=getattr(approval, "user_prompt", ""),
|
||||
is_approval_user_prompt_input_disabled=(
|
||||
False
|
||||
if (isinstance(approval, Approval) and approval.result is None)
|
||||
else True
|
||||
),
|
||||
)
|
||||
)
|
||||
return message_history
|
||||
|
|
@ -273,10 +274,6 @@ class ConversationState(rx.State):
|
|||
if not (user_prompt := approval.user_prompt):
|
||||
return
|
||||
|
||||
# 将等待流式输出设置为是
|
||||
conversation.awaiting_stream = True
|
||||
yield # 通知前端更新渲染
|
||||
|
||||
else:
|
||||
# 若用户提示词为空则直接返回
|
||||
if not (user_prompt := conversation.user_prompt):
|
||||
|
|
@ -368,19 +365,23 @@ class ConversationState(rx.State):
|
|||
case ToolCallPart(
|
||||
tool_call_id=tool_call_id, tool_name=tool_name
|
||||
):
|
||||
# 构建消息实例
|
||||
message = Message(
|
||||
type=MessageType.TOOL_CALL,
|
||||
title=f"正在{tool_name}",
|
||||
is_running=True,
|
||||
)
|
||||
# 添加至消息字典
|
||||
conversation.messages[message.id] = message
|
||||
# 将消息实例唯一标识与工具调用唯一标识映射
|
||||
conversation.workflow.tool_call_id_map_to_message_id[
|
||||
if (
|
||||
tool_call_id
|
||||
] = message.id
|
||||
yield # 通知前端更新渲染
|
||||
not in conversation.workflow.tool_call_id_map_to_message_id
|
||||
):
|
||||
# 构建消息实例
|
||||
message = Message(
|
||||
type=MessageType.TOOL_CALL,
|
||||
title=f"正在{tool_name}",
|
||||
is_running=True,
|
||||
)
|
||||
# 添加至消息字典
|
||||
conversation.messages[message.id] = message
|
||||
# 将消息实例唯一标识与工具调用唯一标识映射
|
||||
conversation.workflow.tool_call_id_map_to_message_id[
|
||||
tool_call_id
|
||||
] = message.id
|
||||
yield # 通知前端更新渲染
|
||||
|
||||
# 文本分片
|
||||
case TextPart(content=content):
|
||||
|
|
@ -458,8 +459,7 @@ class ConversationState(rx.State):
|
|||
]
|
||||
message.is_running = False
|
||||
message.title = f"{tool_name}已完成"
|
||||
# 若内容非空字符串则更新
|
||||
if content := message.content:
|
||||
if isinstance(content, str) and content:
|
||||
message.content = content
|
||||
yield # 通知前端更新渲染
|
||||
messages.append(message)
|
||||
|
|
@ -510,6 +510,7 @@ class ConversationState(rx.State):
|
|||
conversation_id=conversation.id,
|
||||
new_messages=result.new_messages(),
|
||||
)
|
||||
|
||||
# 批量创建消息记录
|
||||
await db_state.create_message_records(
|
||||
conversation_id=conversation.id,
|
||||
|
|
@ -569,11 +570,10 @@ class ConversationState(rx.State):
|
|||
message.approval.user_prompt = user_prompt
|
||||
|
||||
@rx.event
|
||||
async def approve(self, item_id: str, result: bool) -> None:
|
||||
async def approve(self, item_id: str) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
审批消息历史项
|
||||
:param item_id: 消息历史项唯一标识
|
||||
:param result: 审批结果(True 表示审批通过,False 表示审批拒绝)
|
||||
"""
|
||||
# 当前会话
|
||||
conversation = self.conversations.get(self.actived_conversation_id)
|
||||
|
|
@ -585,16 +585,19 @@ class ConversationState(rx.State):
|
|||
):
|
||||
return
|
||||
|
||||
# 更新审批结果
|
||||
approval.result = result
|
||||
# 若会话未正在运行则将正在运行设置为是
|
||||
if not conversation.is_running:
|
||||
conversation.is_running = True
|
||||
# 将等待流式输出设置为是
|
||||
conversation.awaiting_stream = True
|
||||
# 更新审批结果为审批通过
|
||||
approval.result = True
|
||||
yield # 通知前端更新渲染
|
||||
|
||||
# 运行
|
||||
async for _ in self.run(
|
||||
approval,
|
||||
DeferredToolResults(
|
||||
approvals={
|
||||
approval.tool_call_id: ToolApproved() if result else ToolDenied()
|
||||
}
|
||||
),
|
||||
DeferredToolResults(approvals={approval.tool_call_id: ToolApproved()}),
|
||||
):
|
||||
...
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ class MessageHistoryItem(Message):
|
|||
|
||||
has_approval: bool = Field(default=False, description="包含审批实例")
|
||||
approval_user_prompt: str = Field(default="", description="审批用户提示词")
|
||||
is_approval_user_prompt_input_disabled: bool = Field(
|
||||
default=False, description="审批用户提示词不可输出状态"
|
||||
)
|
||||
|
||||
|
||||
# 工作流适配器
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import Any, AsyncGenerator, cast
|
|||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_ai import (
|
||||
Agent,
|
||||
AgentRunResult,
|
||||
ApprovalRequired,
|
||||
DeferredToolRequests,
|
||||
DeferredToolResults,
|
||||
|
|
@ -16,11 +17,8 @@ from pydantic_ai import (
|
|||
RunContext,
|
||||
RunUsage,
|
||||
UsageLimits,
|
||||
AgentRunResult,
|
||||
)
|
||||
from pydantic_ai.messages import (
|
||||
PartDeltaEvent,
|
||||
TextPart,
|
||||
AgentStreamEvent,
|
||||
FunctionToolResultEvent,
|
||||
PartStartEvent,
|
||||
|
|
@ -29,12 +27,11 @@ from pydantic_ai.messages import (
|
|||
)
|
||||
from pydantic_ai.run import AgentRunResultEvent
|
||||
|
||||
from application.states.models import NoResult
|
||||
from application.workshop.events import AgentPromptEvent
|
||||
from application.workshop.models import (
|
||||
DEEPSEEK_V4_FLASH_MODEL,
|
||||
DEEPSEEK_V4_FLASH_MODEL_SETTINGS,
|
||||
)
|
||||
from application.workshop.events import AgentPromptEvent
|
||||
|
||||
|
||||
class Flight(BaseModel):
|
||||
|
|
@ -76,15 +73,15 @@ class Deps(BaseModel):
|
|||
|
||||
|
||||
# 主智能体
|
||||
agent = Agent[Deps, Flight | NoResult | DeferredToolRequests](
|
||||
agent = Agent[Deps, Flight | DeferredToolRequests](
|
||||
model=DEEPSEEK_V4_FLASH_MODEL,
|
||||
model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS,
|
||||
deps_type=Deps,
|
||||
output_type=Flight | NoResult | DeferredToolRequests,
|
||||
output_type=Flight | DeferredToolRequests,
|
||||
system_prompt=(
|
||||
"你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**:",
|
||||
"1. 使用 search_flights 查询满足用户需求的航班;",
|
||||
"2. 使用 book_flight 预定航班。若未选择到航班则返回 NoSelectedFlight;",
|
||||
"2. 使用 book_flight 预定航班。当返回航班信息(Flight)时表示预定航班完成。",
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -209,11 +206,11 @@ async def search_flights(ctx: RunContext[Deps]) -> list[Flight]:
|
|||
|
||||
|
||||
@agent.tool(name="预定航班")
|
||||
async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult:
|
||||
async def book_flight(ctx: RunContext[Deps]) -> Flight:
|
||||
"""
|
||||
预定航班
|
||||
"""
|
||||
if ctx.deps.booked_flight:
|
||||
if ctx.deps.booked_flight is not None:
|
||||
return ctx.deps.booked_flight
|
||||
|
||||
if (searched_flights := ctx.deps.searched_flights) is None:
|
||||
|
|
@ -232,12 +229,16 @@ async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult:
|
|||
)
|
||||
).output
|
||||
if not extracted_flight_number:
|
||||
return NoResult()
|
||||
raise ApprovalRequired(
|
||||
metadata={
|
||||
"content": f"未提取到航班号。请确认后重新输入。",
|
||||
}
|
||||
)
|
||||
|
||||
if extracted_flight_number not in flight_numbers:
|
||||
raise ApprovalRequired(
|
||||
metadata={
|
||||
"content": f"提取到的航班号 {extracted_flight_number} 不在查询到的航班中。请确认后重新输入。"
|
||||
"content": f"提取到的航班号 {extracted_flight_number} 不在查询到的航班中。请确认后重新输入。",
|
||||
}
|
||||
)
|
||||
# 预定到的航班
|
||||
|
|
@ -252,12 +253,12 @@ async def book_flight(ctx: RunContext[Deps]) -> Flight | NoResult:
|
|||
@agent.output_validator
|
||||
async def validate_output(
|
||||
ctx: RunContext[Deps],
|
||||
output: Flight | NoResult | DeferredToolRequests,
|
||||
) -> Flight | NoResult | DeferredToolRequests:
|
||||
output: Flight | DeferredToolRequests,
|
||||
) -> Flight | DeferredToolRequests:
|
||||
"""
|
||||
输出校验
|
||||
"""
|
||||
if isinstance(output, (NoResult, DeferredToolRequests)):
|
||||
if isinstance(output, DeferredToolRequests):
|
||||
return output
|
||||
|
||||
errors = []
|
||||
|
|
@ -291,13 +292,14 @@ async def run_stream_events(
|
|||
usage_limits: UsageLimits | None = None,
|
||||
deferred_tool_results: DeferredToolResults | None = None,
|
||||
) -> AsyncGenerator[
|
||||
AgentStreamEvent
|
||||
| AgentRunResultEvent[Flight | NoResult | DeferredToolRequests]
|
||||
| None,
|
||||
AgentStreamEvent | AgentRunResultEvent[Flight | DeferredToolRequests] | None,
|
||||
]:
|
||||
"""
|
||||
运行并流式输出事件
|
||||
"""
|
||||
if not isinstance(deps, Deps):
|
||||
deps = Deps.model_validate(deps)
|
||||
|
||||
async with agent.run_stream_events(
|
||||
user_prompt=user_prompt,
|
||||
deps=deps,
|
||||
|
|
@ -330,12 +332,14 @@ async def run_stream_events(
|
|||
searched_flights = cast(list[Flight], content)
|
||||
event.part.content = f"已查询到 **{len(searched_flights)}** 班航班:\n\n"
|
||||
for searched_flight in searched_flights:
|
||||
event.part.content += f"{searched_flight.number} {searched_flight.airfare}$ 于 {searched_flight.date.strftime('%Y-%m-%d')} 从 {searched_flight.origin_airport_code} 到 {searched_flight.destination_airport_code}\n\n"
|
||||
event.part.content += f"**{searched_flight.number}** | 💰 {searched_flight.airfare} | {searched_flight.date.strftime('%Y-%m-%d')} | {searched_flight.origin_airport_code} ✈️ {searched_flight.destination_airport_code}\n\n"
|
||||
yield event
|
||||
|
||||
case "预定航班":
|
||||
if isinstance(deps.booked_flight, Flight):
|
||||
event.part.content = ""
|
||||
event.part.content = (
|
||||
"请输入航班号:\n\n" # 成功预定内容维持提示
|
||||
)
|
||||
else:
|
||||
event.part.content = (
|
||||
"未提取到航班号,请检查后重试\n\n"
|
||||
|
|
@ -348,10 +352,6 @@ async def run_stream_events(
|
|||
match output:
|
||||
case Flight():
|
||||
yield AgentPromptEvent(
|
||||
content=f"已成功预定:\n\n{output.number} {output.airfare}$ 于 {output.date.strftime('%Y-%m-%d')} 从 {output.origin_airport_code} 到 {output.destination_airport_code}\n\n本次会话结束。"
|
||||
)
|
||||
case NoResult():
|
||||
yield AgentPromptEvent(
|
||||
content="预定失败\n\n本次会话结束。"
|
||||
content=f"---\n\n✅ 预定机票成功:\n\n{output.number} | 💰 {output.airfare} | {output.date.strftime('%Y-%m-%d')} | {output.origin_airport_code} ✈️ {output.destination_airport_code}\n\n本次会话结束。"
|
||||
)
|
||||
yield event
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
--prismui-background-color-5: #ffffff33;
|
||||
--prismui-background-color-6: #f2f2f3;
|
||||
--prismui-background-color-7: #5e7ce0;
|
||||
--prismui-background-color-8: #beccfa;
|
||||
--prismui-background-1: linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%);
|
||||
--prismui-background-2: linear-gradient(180deg, #fffffff2, #f8fafff2 99%);
|
||||
--prismui-background-3: linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd);
|
||||
|
|
@ -90,11 +91,11 @@ pre, pre code {
|
|||
/* 使用主题变量:普通文字色 --prismui-color-3,高光使用主题蓝色 --prismui-color-5 */
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--prismui-color-3) 0%,
|
||||
var(--prismui-color-3) 35%,
|
||||
var(--prismui-color-7) 50%,
|
||||
var(--prismui-color-3) 65%,
|
||||
var(--prismui-color-3) 100%
|
||||
var(--prismui-color-1) 0%,
|
||||
var(--prismui-color-1) 35%,
|
||||
var(--prismui-color-3) 50%,
|
||||
var(--prismui-color-1) 65%,
|
||||
var(--prismui-color-1) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue