diff --git a/agent/application/pages/conversation.py b/agent/application/pages/conversation.py index 3803022..6c02bf8 100644 --- a/agent/application/pages/conversation.py +++ b/agent/application/pages/conversation.py @@ -294,10 +294,12 @@ def use_guidance() -> rx.Component: font_weight="var(--prismui-font-weight-3)", ), rx.hstack( - # 预定航班按钮 + # 工作流按钮:预定航班 workflow_button(WorkflowType.BOOK_FLIGHT), - # 银行客服按钮 + # 工作流按钮:银行客户服务 workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE), + # 工作流按钮:研究 + workflow_button(WorkflowType.RESEARCH), flex_wrap="wrap", align_items="center", gap="12px", @@ -408,7 +410,6 @@ def thinking(item: MessageHistoryItem) -> rx.Component: rx.markdown( item.content, width="100%", - margin="4px 0", line_height="1.6", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-3)", diff --git a/agent/application/states/conversation.py b/agent/application/states/conversation.py index 03699d0..19fa38f 100644 --- a/agent/application/states/conversation.py +++ b/agent/application/states/conversation.py @@ -109,10 +109,11 @@ class ConversationState(rx.State): # 当前会话 conversation = self.conversations[self.conversation_id] # 最后一个消息 - message = next(reversed(conversation.messages.values())) + message = next(iter(reversed(conversation.messages.values())), None) # 若最后一个消息包含审批且审批结果为 None 则将会话正在运行设置为是、消息正在运行设置为是 if ( - isinstance(approval := message.approval, Approval) + message is not None + and isinstance(approval := message.approval, Approval) and approval.result is None ): conversation.is_running = True @@ -396,8 +397,21 @@ class ConversationState(rx.State): 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: # 若等待流式输出则将等待流式输出设置为否 @@ -459,26 +473,47 @@ class ConversationState(rx.State): # ========== 增量事件 ========== case PartDeltaEvent(index=index, delta=delta): + # 获取消息 match delta: # 思考分片增量 case ThinkingPartDelta( content_delta=content_delta, + provider_details=provider_details, ): - # 若内容增量不为空则增量更新 + message = conversation.messages[ + index_map_to_message_id[index] + ] + # 若内容增量非空则将内容追加至消息内容,否则若供应商详情可调用更新消息内容 if content_delta: - conversation.messages[ - index_map_to_message_id[index] - ].content += content_delta + message.content += content_delta 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( content_delta=content_delta, ): - # 增量更新 - conversation.messages[ + message = conversation.messages[ index_map_to_message_id[index] - ].content += content_delta + ] + # 增量更新 + message.content += content_delta yield # 通知前端更新渲染 # ========== 结束事件 ========== @@ -677,6 +712,12 @@ class ConversationState(rx.State): # 设置用户提示词 self.set_user_prompt(f"帮我查询账户余额") + # 研究 + case WorkflowType.RESEARCH: + # 初始化研究工作流 + workflow = Workflow(type=type) + + # 其他 case _: # 初始化对话工作流 workflow = Workflow() diff --git a/agent/application/states/models.py b/agent/application/states/models.py index 0a04d1f..23952a8 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -61,6 +61,7 @@ class WorkflowType(StrEnum): TALK = "对话" BOOK_FLIGHT = "预定航班" BANK_CUSTOMER_SERVICE = "银行客户服务" + RESEARCH = "研究" class Workflow(BaseModel): diff --git a/agent/application/workshop/bank_customer_service.py b/agent/application/workshop/bank_customer_service.py index 6ff2c04..f055df1 100644 --- a/agent/application/workshop/bank_customer_service.py +++ b/agent/application/workshop/bank_customer_service.py @@ -19,8 +19,8 @@ from pydantic_ai.messages import AgentStreamEvent from pydantic_ai.run import AgentRunResultEvent from application.workshop.models import ( - DEEPSEEK_V4_FLASH_MODEL, - DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + DEEPSEEK_V4_FLASH_CHAT_MODEL, + DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, ) @@ -75,8 +75,8 @@ async def query_account_balance(ctx: RunContext[Deps]) -> str: # 智能体 agent = Agent[Deps, str]( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + model=DEEPSEEK_V4_FLASH_CHAT_MODEL, + model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, deps_type=Deps, output_type=str, # 若输出类型为 str,则智能体输出结果时不进行输出类型校验。若定义输出类型,需考虑处理相应事件 instructions="你是我方银行的客服智能体,为客户提供客服应答。", # 优先使用 instructions diff --git a/agent/application/workshop/book_flight.py b/agent/application/workshop/book_flight.py index d13e4a2..4b97939 100644 --- a/agent/application/workshop/book_flight.py +++ b/agent/application/workshop/book_flight.py @@ -28,8 +28,8 @@ from pydantic_ai.run import AgentRunResultEvent, AgentRunResult from application.workshop.events import AgentRuntimeEvent from application.workshop.models import ( - DEEPSEEK_V4_FLASH_MODEL, - DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + DEEPSEEK_V4_FLASH_CHAT_MODEL, + DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, ) @@ -73,8 +73,8 @@ class Deps(BaseModel): # 主智能体 agent = Agent[Deps, Flight | DeferredToolRequests]( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + model=DEEPSEEK_V4_FLASH_CHAT_MODEL, + model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, deps_type=Deps, output_type=Flight | DeferredToolRequests, system_prompt=( @@ -87,8 +87,8 @@ agent = Agent[Deps, Flight | DeferredToolRequests]( # 提取所有航班信息智能体 extraction_flights_agent = Agent[Deps, list[Flight]]( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + model=DEEPSEEK_V4_FLASH_CHAT_MODEL, + model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, deps_type=Deps, output_type=list[Flight], system_prompt=( @@ -101,8 +101,8 @@ extraction_flights_agent = Agent[Deps, list[Flight]]( # 提取航班号智能体 extraction_flight_number_agent = Agent[Deps, str | None]( - model=DEEPSEEK_V4_FLASH_MODEL, - model_settings=DEEPSEEK_V4_FLASH_MODEL_SETTINGS, + model=DEEPSEEK_V4_FLASH_CHAT_MODEL, + model_settings=DEEPSEEK_V4_FLASH_CHAT_MODEL_SETTINGS, deps_type=Deps, output_type=str | None, system_prompt=( diff --git a/agent/application/workshop/models.py b/agent/application/workshop/models.py index bc138e6..1c20db5 100644 --- a/agent/application/workshop/models.py +++ b/agent/application/workshop/models.py @@ -3,10 +3,10 @@ 智能体模型模块 """ 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 -DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( +DEEPSEEK_V4_FLASH_CHAT_MODEL = OpenAIChatModel( model_name="deepseek-v4-flash", provider=OpenAIProvider( 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"}} # 温度控制 ) # 不启用思考模式 + +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" \ No newline at end of file diff --git a/agent/application/workshop/research.py b/agent/application/workshop/research.py new file mode 100644 index 0000000..aa486b8 --- /dev/null +++ b/agent/application/workshop/research.py @@ -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 diff --git a/agent/application/workshop/talk.py b/agent/application/workshop/talk.py index 0637ac6..28ff9da 100644 --- a/agent/application/workshop/talk.py +++ b/agent/application/workshop/talk.py @@ -8,7 +8,7 @@ from pydantic_ai import Agent, ModelMessage, RunUsage from pydantic_ai.messages import AgentStreamEvent 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 = """ @@ -28,7 +28,7 @@ instruction = """ """ agent = Agent( - model=DEEPSEEK_V4_FLASH_MODEL, + model=DEEPSEEK_V4_FLASH_CHAT_MODEL, instructions=instruction, ) diff --git a/agent/database.db b/agent/database.db index 4df905a..1a212cc 100644 Binary files a/agent/database.db and b/agent/database.db differ