This commit is contained in:
liubiren 2026-09-20 20:55:08 +08:00
parent 6624427528
commit f289955974
9 changed files with 212 additions and 31 deletions

View File

@ -294,10 +294,12 @@ def use_guidance() -> rx.Component:
font_weight="var(--prismui-font-weight-3)", font_weight="var(--prismui-font-weight-3)",
), ),
rx.hstack( rx.hstack(
# 预定航班按钮 # 工作流按钮:预定航班
workflow_button(WorkflowType.BOOK_FLIGHT), workflow_button(WorkflowType.BOOK_FLIGHT),
# 银行客服按钮 # 工作流按钮:银行客户服务
workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE), workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE),
# 工作流按钮:研究
workflow_button(WorkflowType.RESEARCH),
flex_wrap="wrap", flex_wrap="wrap",
align_items="center", align_items="center",
gap="12px", gap="12px",
@ -408,7 +410,6 @@ def thinking(item: MessageHistoryItem) -> rx.Component:
rx.markdown( rx.markdown(
item.content, item.content,
width="100%", width="100%",
margin="4px 0",
line_height="1.6", line_height="1.6",
font_size="var(--prismui-font-size-1)", font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)", color="var(--prismui-color-3)",

View File

@ -109,10 +109,11 @@ class ConversationState(rx.State):
# 当前会话 # 当前会话
conversation = self.conversations[self.conversation_id] conversation = self.conversations[self.conversation_id]
# 最后一个消息 # 最后一个消息
message = next(reversed(conversation.messages.values())) message = next(iter(reversed(conversation.messages.values())), None)
# 若最后一个消息包含审批且审批结果为 None 则将会话正在运行设置为是、消息正在运行设置为是 # 若最后一个消息包含审批且审批结果为 None 则将会话正在运行设置为是、消息正在运行设置为是
if ( if (
isinstance(approval := message.approval, Approval) message is not None
and isinstance(approval := message.approval, Approval)
and approval.result is None and approval.result is None
): ):
conversation.is_running = True conversation.is_running = True
@ -396,8 +397,21 @@ class ConversationState(rx.State):
usage=usage, usage=usage,
) )
# 消息列表 # 研究
messages: list[Message] = [] case WorkflowType.RESEARCH:
from application.workshop.research import (
run_stream_events,
)
# 运行并流式输出事件
stream_events = run_stream_events(
user_prompt=user_prompt,
message_history=message_history,
usage=usage,
)
messages: list[Message] = [] # 消息列表
previous_provider_details = {} # 上一个供应商详情
# 获取运行流式输出事件 # 获取运行流式输出事件
async for event in stream_events: async for event in stream_events:
# 若等待流式输出则将等待流式输出设置为否 # 若等待流式输出则将等待流式输出设置为否
@ -459,26 +473,47 @@ class ConversationState(rx.State):
# ========== 增量事件 ========== # ========== 增量事件 ==========
case PartDeltaEvent(index=index, delta=delta): case PartDeltaEvent(index=index, delta=delta):
# 获取消息
match delta: match delta:
# 思考分片增量 # 思考分片增量
case ThinkingPartDelta( case ThinkingPartDelta(
content_delta=content_delta, content_delta=content_delta,
provider_details=provider_details,
): ):
# 若内容增量不为空则增量更新 message = conversation.messages[
index_map_to_message_id[index]
]
# 若内容增量非空则将内容追加至消息内容,否则若供应商详情可调用更新消息内容
if content_delta: if content_delta:
conversation.messages[ message.content += content_delta
index_map_to_message_id[index]
].content += content_delta
yield # 通知前端更新渲染 yield # 通知前端更新渲染
else:
if callable(provider_details):
# 获取原始内容
raw_content = (
previous_provider_details := provider_details(
previous_provider_details
)
).get("raw_content")
if isinstance(raw_content, str):
...
elif isinstance(raw_content, list):
raw_content = "".join(raw_content)
else:
continue
message.content = raw_content
yield # 通知前端更新渲染
# 文本分片增量 # 文本分片增量
case TextPartDelta( case TextPartDelta(
content_delta=content_delta, content_delta=content_delta,
): ):
# 增量更新 message = conversation.messages[
conversation.messages[
index_map_to_message_id[index] index_map_to_message_id[index]
].content += content_delta ]
# 增量更新
message.content += content_delta
yield # 通知前端更新渲染 yield # 通知前端更新渲染
# ========== 结束事件 ========== # ========== 结束事件 ==========
@ -677,6 +712,12 @@ class ConversationState(rx.State):
# 设置用户提示词 # 设置用户提示词
self.set_user_prompt(f"帮我查询账户余额") self.set_user_prompt(f"帮我查询账户余额")
# 研究
case WorkflowType.RESEARCH:
# 初始化研究工作流
workflow = Workflow(type=type)
# 其他
case _: case _:
# 初始化对话工作流 # 初始化对话工作流
workflow = Workflow() workflow = Workflow()

View File

@ -61,6 +61,7 @@ class WorkflowType(StrEnum):
TALK = "对话" TALK = "对话"
BOOK_FLIGHT = "预定航班" BOOK_FLIGHT = "预定航班"
BANK_CUSTOMER_SERVICE = "银行客户服务" BANK_CUSTOMER_SERVICE = "银行客户服务"
RESEARCH = "研究"
class Workflow(BaseModel): class Workflow(BaseModel):

View File

@ -19,8 +19,8 @@ from pydantic_ai.messages import AgentStreamEvent
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
from application.workshop.models import ( from application.workshop.models import (
DEEPSEEK_V4_FLASH_MODEL, DEEPSEEK_V4_FLASH_CHAT_MODEL,
DEEPSEEK_V4_FLASH_MODEL_SETTINGS, DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
) )
@ -75,8 +75,8 @@ async def query_account_balance(ctx: RunContext[Deps]) -> str:
# 智能体 # 智能体
agent = Agent[Deps, str]( agent = Agent[Deps, str](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_CHAT_MODEL,
model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
deps_type=Deps, deps_type=Deps,
output_type=str, # 若输出类型为 str,则智能体输出结果时不进行输出类型校验。若定义输出类型,需考虑处理相应事件 output_type=str, # 若输出类型为 str,则智能体输出结果时不进行输出类型校验。若定义输出类型,需考虑处理相应事件
instructions="你是我方银行的客服智能体,为客户提供客服应答。", # 优先使用 instructions instructions="你是我方银行的客服智能体,为客户提供客服应答。", # 优先使用 instructions

View File

@ -28,8 +28,8 @@ from pydantic_ai.run import AgentRunResultEvent, AgentRunResult
from application.workshop.events import AgentRuntimeEvent from application.workshop.events import AgentRuntimeEvent
from application.workshop.models import ( from application.workshop.models import (
DEEPSEEK_V4_FLASH_MODEL, DEEPSEEK_V4_FLASH_CHAT_MODEL,
DEEPSEEK_V4_FLASH_MODEL_SETTINGS, DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
) )
@ -73,8 +73,8 @@ class Deps(BaseModel):
# 主智能体 # 主智能体
agent = Agent[Deps, Flight | DeferredToolRequests]( agent = Agent[Deps, Flight | DeferredToolRequests](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_CHAT_MODEL,
model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
deps_type=Deps, deps_type=Deps,
output_type=Flight | DeferredToolRequests, output_type=Flight | DeferredToolRequests,
system_prompt=( system_prompt=(
@ -87,8 +87,8 @@ agent = Agent[Deps, Flight | DeferredToolRequests](
# 提取所有航班信息智能体 # 提取所有航班信息智能体
extraction_flights_agent = Agent[Deps, list[Flight]]( extraction_flights_agent = Agent[Deps, list[Flight]](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_CHAT_MODEL,
model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
deps_type=Deps, deps_type=Deps,
output_type=list[Flight], output_type=list[Flight],
system_prompt=( system_prompt=(
@ -101,8 +101,8 @@ 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_CHAT_MODEL,
model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS,
deps_type=Deps, deps_type=Deps,
output_type=str | None, output_type=str | None,
system_prompt=( system_prompt=(

View File

@ -3,10 +3,10 @@
智能体模型模块 智能体模型模块
""" """
from pydantic_ai import ModelSettings from pydantic_ai import ModelSettings
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel, OpenAIResponsesModel
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( DEEPSEEK_V4_FLASH_CHAT_MODEL = OpenAIChatModel(
model_name="deepseek-v4-flash", model_name="deepseek-v4-flash",
provider=OpenAIProvider( provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1", base_url="https://tokenhub.tencentmaas.com/v1",
@ -14,6 +14,16 @@ DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
), ),
) )
DEEPSEEK_V4_FLASH_MODEL_SETTINGS = ModelSettings( DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS = ModelSettings(
temperature=0, extra_body={"thinking": {"type": "disabled"}} # 温度控制 temperature=0, extra_body={"thinking": {"type": "disabled"}} # 温度控制
) # 不启用思考模式 ) # 不启用思考模式
DEEPSEEK_V4_FLASH_RESPONSE_MODEL = OpenAIResponsesModel(
model_name="deepseek/deepseek-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
)
"sk-4a79ec5c0af74680997ef56758604850"

View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""
研究
"""
from typing import AsyncGenerator
import httpx
from pydantic import BaseModel, Field
from pydantic_ai import Agent, ModelMessage, RunContext, RunUsage
from pydantic_ai.messages import AgentStreamEvent
from pydantic_ai.run import AgentRunResultEvent
from pydantic_ai.toolsets import FunctionToolset
from application.workshop.models import DEEPSEEK_V4_FLASH_RESPONSE_MODEL
class Args(BaseModel):
query: str = Field(description="查询语句")
# 实例化工具集
bocha_web_search = FunctionToolset()
@bocha_web_search.tool_plain() # 普通工具:不使用运行上下文
async def web_search(args: Args) -> str:
"""
联网搜索工具:
用于查询互联网实时信息、新闻、政策、行业报告、企业公告、最新数据。
适合查询模型静态知识库不存在的外部网页信息。
回答时必须标注引用来源URL,禁止编造链接。
不要用于简单常识、数学计算、代码编写等无需联网的任务。
"""
try:
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
"https://api.bocha.cn/v1/web-search",
headers={
"Authorization": f"Bearer sk-a9ed73d8b26745799cf970f84d70f512",
"Content-Type": "application/json",
},
json={
"query": args.query,
"summary": True, # 显示文本摘要
},
)
response.raise_for_status()
# 解析响应载荷
response_payload = response.json()
except httpx.HTTPStatusError as exception:
return f"请求错误 {exception.response.status_code}: {exception.response.text}"
except Exception as exception:
return f"请求异常: {str(exception)}"
if (code := response_payload.get("code")) != 200:
return f"业务错误 {code}: {response_payload.get("msg", "未知业务错误")}"
web_pages = response_payload.get("data", {}).get("webPages", {}).get("value", [])
if not web_pages:
return "未搜索到相关网页"
result = [] # 搜索结果
for idx, web_page in enumerate(web_pages, 1):
# 解析网页的标题
name = web_page.get("name", "")
# 解析网页的URL
url = web_page.get("url", "")
# 解析网页的网站名称
site_name = web_page.get("siteName", "")
# 解析网页的发布时间
date_published = web_page.get("datePublished") or web_page.get(
"dateLastCrawled", ""
)
# 优先解析网页内容的文本摘要,若文本摘要为空则解析简短描述
summary = web_page.get("summary") or web_page.get("snippet", "")
result.append(
f"### 搜索结果 {idx}\n"
f"- 标题:{name}\n"
f"- URL:{url}\n"
f"- 来源网站:{site_name}\n"
f"- 发布时间:{date_published}\n"
f"- 摘要:{summary}"
)
return "\n\n".join(result)
instruction = """
你是研究助手,可调用联网搜索工具。
1. 事实、时效、外部资料类问题必须搜索;静态常识可直接回答,支持多次检索、改写query。
2. 优先采用一手资料、官方权威信源,慎用自媒体与转载内容。
3. 所有事实描述必须标注来源链接,引用格式:`[参考{序号}] {URL}`,序号匹配搜索结果编号。
4. 严格区分:【事实】来自网页原文;【推论】基于事实推导;资料不足时标注【推测】,不可当作事实。
5. 严禁编造链接和事实;证据不足直接说明,不要强行下结论。
6. 组织回答:先陈列证据,再总结结论,结论不能超出资料支撑范围。
"""
# 智能体
agent = Agent(
model=DEEPSEEK_V4_FLASH_RESPONSE_MODEL,
output_type=str, # 若输出类型为 str,则智能体输出结果时不进行输出类型校验。若定义输出类型,需考虑处理相应事件
instructions=instruction,
toolsets=[bocha_web_search],
)
async def run_stream_events(
user_prompt: str | None,
message_history: list[ModelMessage],
usage: RunUsage,
) -> AsyncGenerator[AgentStreamEvent | AgentRunResultEvent, None]:
"""
运行并流式输出事件
:param user_prompt: 用户提示词
:param message_history: 消息历史
:param usage: 使用量
:return: AsyncGenerator
"""
async with agent.run_stream_events(
user_prompt=user_prompt,
message_history=message_history,
usage=usage,
) as events:
async for event in events:
print(event)
yield event

View File

@ -8,7 +8,7 @@ from pydantic_ai import Agent, ModelMessage, RunUsage
from pydantic_ai.messages import AgentStreamEvent from pydantic_ai.messages import AgentStreamEvent
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
from application.workshop.models import DEEPSEEK_V4_FLASH_MODEL from application.workshop.models import DEEPSEEK_V4_FLASH_CHAT_MODEL
instruction = """ instruction = """
@ -28,7 +28,7 @@ instruction = """
""" """
agent = Agent( agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_CHAT_MODEL,
instructions=instruction, instructions=instruction,
) )

Binary file not shown.