Python/产品需求文档AI生成/application/utils/agent.py

266 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Pydantic AI 聊天智能体和相关模块
"""
# 列举导入模块
from enum import StrEnum
from typing import AsyncGenerator, List, Optional, Union
from uuid import uuid4
from pydantic_ai import Agent
from pydantic_ai.capabilities import AgentCapability
from pydantic_ai.messages import (
AgentStreamEvent,
ModelMessage,
PartStartEvent,
ThinkingPart,
ToolSearchCallPart,
ThinkingPartDelta,
ToolCallPart,
LoadCapabilityCallPart,
TextPartDelta,
ToolCallPartDelta,
FunctionToolResultEvent,
PartDeltaEvent,
TextPart,
PartEndEvent,
)
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.output import OutputSpec
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.run import AgentRunResultEvent
from pydantic import BaseModel, Field
DEFAULT_INSTRUCTIONS: str = """
# 角色
专业友好AI助手结构化解答各类问题。
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹;
4. 重点内容标注 **粗体**/*斜体*
5. 思考、工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染。
# 行文要求
语言通俗,逻辑完整简洁,无多余废话。
"""
class Event(BaseModel):
"""
事件类
"""
event_kind: str = Field(..., description="事件种类")
content: Optional[str] = Field(default=None, description="事件内容")
part_index: Optional[int] = Field(default=None, description="分片索引")
part_kind: str = Field(..., description="分片种类")
tool_name: Optional[str] = Field(default=None, description="工具名称")
class AIAgent:
"""
基于 Pydantic AI 封装的智能体
"""
def __init__(
self,
chat_id: str,
instructions: Optional[str] = None,
output_type: OutputSpec = str,
capabilities: Optional[List[AgentCapability]] = None,
retries: int = 1,
):
"""
初始化
:param chat_id: 聊天唯一标识
:param instructions: 指令
:param capabilities: 技能列表,默认为不使用技能
:param output_type: 输出类型
:param retries: 重试次数默认为1次
:return: 智能体实例
"""
# 聊天唯一标识
self.chat_id = chat_id
# 一次聊天chat包含若干论对话dialog每轮对话包含用户提示词user_prompt和输出output。其中输出包含若干分片Part
# 本轮对话工具调用唯一标识与片段索引映射表
self.tool_call_ids: dict[str, int] = {}
# 本轮对话新增消息列表
self.new_messages: List[ModelMessage] = []
# 若指令为空则使用默认指令
if not instructions:
instructions = DEFAULT_INSTRUCTIONS
# 初始化智能体
self.agent = Agent(
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=capabilities,
output_type=output_type,
retries=retries,
)
async def run(
self,
user_prompt: str | List[str],
message_history: Optional[List[ModelMessage]] = None,
) -> AsyncGenerator[Event]:
"""
运行
:param user_prompt: 用户提示词(用户提示词)
:param flush_delay: 刷新延迟时长(单位为秒)
:yield: AsyncGenerator[Event]
"""
async with self.agent.run_stream_events(
user_prompt=user_prompt,
message_history=message_history,
) as events:
async for event in events:
match event:
# ========== 分片开始事件 ==========
case PartStartEvent(event_kind=event_kind, index=index, part=part):
match part:
# 思考分片开始事件
case ThinkingPart(part_kind=part_kind, content=content):
yield Event(
event_kind=event_kind,
content=content,
part_index=index,
part_kind=part_kind,
)
# 检索工具分片开始事件
case ToolSearchCallPart(
part_kind=part_kind, tool_call_id=tool_call_id, tool_name=tool_name
):
# 记录工具调用唯一标识与片段索引映射
self.tool_call_ids[tool_call_id] = index
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 加载能力分片开始事件
case LoadCapabilityCallPart(
part_kind=part_kind, tool_call_id=tool_call_id, tool_name=tool_name
):
# 记录工具调用唯一标识与片段索引映射
self.tool_call_ids[tool_call_id] = index
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 调用工具分片开始事件
case ToolCallPart(
part_kind=part_kind,
tool_call_id=tool_call_id,
tool_name=tool_name,
):
# 记录工具调用唯一标识与片段索引映射
self.tool_call_ids[tool_call_id] = index
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 文本分片开始事件
case TextPart(part_kind=part_kind, content=content):
yield Event(
event_kind=event_kind,
content=content,
part_index=index,
part_kind=part_kind,
)
# ========== 分片增量事件(前端仅就文本片段实现打字机效果) ==========
case PartDeltaEvent(
event_kind=event_kind, index=index, delta=delta
):
match delta:
# 文本分片增量事件
case TextPartDelta(
part_delta_kind=part_delta_kind,
content_delta=content_delta,
):
yield Event(
event_kind=event_kind,
content=content_delta, # 增量
part_index=index,
part_kind=part_delta_kind,
)
# ========== 分片结束事件 ==========
case PartEndEvent(event_kind=event_kind, index=index, part=part):
match part:
# 思考分片结束事件
case ThinkingPart(part_kind=part_kind, content=content):
yield Event(
event_kind=event_kind,
content=content,
part_index=index,
part_kind=part_kind,
)
# 检索工具分片结束事件
case ToolSearchCallPart(part_kind=part_kind, tool_name=tool_name):
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 加载能力分片结束事件
case LoadCapabilityCallPart(part_kind=part_kind, tool_name=tool_name):
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 调用工具分片结束事件
case ToolCallPart(part_kind=part_kind, tool_name=tool_name):
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)
# 文本分片结束事件
case TextPart(part_kind=part_kind):
yield Event(
event_kind=event_kind,
part_index=index,
part_kind=part_kind,
)
# ========== 函数工具结果回调事件 ==========
case FunctionToolResultEvent(
event_kind=event_kind,
content=content,
part=part,
):
yield Event(
event_kind=event_kind,
content=content if isinstance(content, str) else None, #
part_index=index,
part_kind=part_kind,
tool_name=tool_name,
)