This commit is contained in:
parent
6624427528
commit
f289955974
|
|
@ -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)",
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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=(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
Loading…
Reference in New Issue