# -*- coding: utf-8 -*- """ 智能体模块 """ # 列举导入模块 from pathlib import Path import time from typing import AsyncGenerator, List, Optional, Dict, Literal from uuid import uuid4 from pydantic_ai.messages import ModelMessage from pydantic_ai import Agent as PydanticAIAgent, ModelMessage from pydantic_ai.capabilities import AgentCapability from asyncio import Task, create_task, sleep, Queue, QueueEmpty from pydantic import Field from pydantic import BaseModel from pydantic_ai.messages import ( ModelMessagesTypeAdapter, ModelResponse, ModelMessageEvent, PartStartEvent, PartDeltaEvent, PartEndEvent, TextPart, ToolSearchCallPart, NativeToolSearchCallPart, LoadCapabilityCallPart, ThinkingPart, ToolCallPart, NativeToolCallPart, TextPartDelta, ThinkingPartDelta, ModelResponseStreamEvent, ToolReturnPart, AgentRunResultEvent, NativeToolReturnPart, ) from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.output import OutputSpec from pydantic_ai.providers.openai import OpenAIProvider from .sqlite import SQLite class AgentMemory(SQLite): """ 智能体记忆体,支持: create:新增对话消息 read:查询会话历史消息 """ def __init__(self): """ 初始化 """ # 构建数据库路径 super().__init__(database=Path(__file__).parent.resolve() / "agent_memory.db") try: with self: self.execute( sql=""" CREATE TABLE IF NOT EXISTS new_messages ( --唯一标识 id TEXT PRIMARY KEY, --会话唯一标识 session_id TEXT NOT NULL, --新对话消息 new_messages TEXT NOT NULL, --时间戳(毫秒) timestamp INTEGER NOT NULL ) """ ) except Exception as exception: raise RuntimeError(f"初始化数据库发生异常:{str(exception)}") from exception def create_new_messages( self, session_id: str, new_messages: List[ModelMessage] ) -> bool: """ 新增新对话消息 :param session_id: 会话唯一标识 :param new_messages: 新对话消息 :return: 新增是否成功 """ try: with self: return self.execute( sql=""" INSERT INTO new_messages (id, session_id, new_messages, timestamp) VALUES (?, ?, ?, ?) """, parameters=( uuid4().hex.lower(), session_id, ModelMessagesTypeAdapter.dump_json(new_messages), int(time.time() * 1000), ), ) except Exception as exception: raise RuntimeError(f"新增对话消息发生异常:{str(exception)}") from exception def get_message_history(self, session_id: str) -> List[ModelMessage]: """ 获取指定会话的消息历史 :param session_id: 会话唯一标识 :return: 消息历史 """ try: with self: result = self.query_all( sql=""" SELECT new_messages FROM new_messages WHERE session_id = ? ORDER BY timestamp ASC """, parameters=(session_id,), ) message_history = [] for row in result: message_history.extend( ModelMessagesTypeAdapter.validate_json(row["new_messages"]) ) return message_history except Exception as exception: raise RuntimeError( f"查询会话历史消息发生异常:{str(exception)}" ) from exception class PendingFlushPart(BaseModel): """ 待刷新模型消息片段 """ type_: Literal["thinking", "text"] = Field(..., description="片段类型") content_delta: str = Field(default="", description="片段内容增量") task: Optional[Task] = Field(default=None, description="异步协程任务") class EventsDebouncer: """ 模型消息事件防抖器 使用 Pydantic-AI 中 run_stream_events 方法时需就其返回的模型消息事件处理,延迟刷新片段类型为思考或文本的片段增量事件以实现防抖(打字机效果) 在 Pydantic-AI 中模型流式输出包含若干片段,例如思考片段、文本片段等。每个片段包含若干模型消息事件,例如片段开始事件、片段增量事件、片段结束事件等 """ def __init__(self, flush_delay: float = 0.2): """ 初始化 :param flush_delay: 刷新延迟(单位为秒) """ self.flush_delay = flush_delay # 待刷新模型响应片段 self.pending_flush_parts: Dict[int, PendingFlushPart] = {} # 待刷新列队 self.pending_flush_queue = Queue() async def handle_event( self, event: ModelMessageEvent ) -> AsyncGenerator[ModelMessageEvent, None]: """ 处理模型消息事件 :param event: 模型消息事件 :yield: AsyncGenerator """ # 返回待刷新列队中模型消息事件 while True: try: yield self.pending_flush_queue.get_nowait() except QueueEmpty: break # 若为思考或文本的片段开始事件则先缓存片段再返回,其它事件直接返回 if isinstance(event, PartStartEvent): index, part = event.index, event.part # 片段索引、片段 match part: case ThinkingPart(): type_ = "thinking" case TextPart(): type_ = "text" case _: yield event return # 新增片段缓存 self.pending_flush_parts[index] = PendingFlushPart( type_=type_, ) yield event return # 若为思考或文本的片段增量事件则更新缓存,其它事件直接返回 if isinstance(event, PartDeltaEvent): index, delta = event.index, event.delta pending_flush_part = self.pending_flush_parts.get(index) if pending_flush_part and isinstance( delta, (ThinkingPartDelta, TextPartDelta) ): pending_flush_part.content_delta += delta.content_delta or "" # 原理:若持续输出片段增量事件则拦截并重新创建异步协程任务,至输出间隔超过刷新延迟则返回 # 取消异步协程任务 self._cancel_task(index=index) # 创建异步协程任务 pending_flush_part.task = create_task( coro=self._delay_flush(index=index) ) else: yield event return # 若为其它事件则先刷新所有片段缓存再直接返回 for index in list(self.pending_flush_parts.keys()): flush_event = await self._flush(index=index) if flush_event: yield flush_event # 若为片段结束事件则删除该片段缓存 if isinstance(event, PartEndEvent): self.part_caches.pop(event.index, None) return def _cancel_task(self, index: int) -> None: """ 取消异步协程任务 :param index: 片段索引 :yield: None """ pending_flush_part = self.pending_flush_parts.get(index) if not pending_flush_part or not pending_flush_part.task: return # 若异步协程任务未完成则取消 if not pending_flush_part.task.done(): pending_flush_part.task.cancel() pending_flush_part.task = None async def _delay_flush(self, index: int): """ 延迟刷新 :param index: 片段索引 :return: None """ await sleep(delay=self.flush_delay) flush_event = await self._flush(index=index) if flush_event: await self.pending_flush_queue.put(item=flush_event) async def _flush(self, index: int) -> Optional[PartDeltaEvent]: """ 刷新 :param index: 片段索引 :return: Optional[PartDeltaEvent] """ pending_flush_part = self.pending_flush_parts.get(index) if not pending_flush_part or not pending_flush_part.content_delta: return None match pending_flush_part.type_: case "thinking": delta = ThinkingPartDelta(content_delta=pending_flush_part.content_delta) case "text": delta = TextPartDelta(content_delta=pending_flush_part.content_delta) pending_flush_part.content_delta = "" pending_flush_part.task = None return PartDeltaEvent(index=index, delta=delta) class Agent: """ 智能体,支持: 1 实例智能体 2 异步运行 """ def __init__( self, session_id: str, instructions: str, output_type: OutputSpec = str, capabilities: Optional[List[AgentCapability]] = None, ): """ 初始化智能体 :param session_id: 会话唯一标识 :param instructions: 指令 :param skills: 智能体技能列表,默认为不使用技能 :param output_type: 输出类型 :return: 智能体实例 """ # 会话唯一标识 self.session_id = session_id # 实例智能体的记忆体 self.agent_memory = AgentMemory() # 实例智能体 self.agent = PydanticAIAgent( 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=1, ) self.agent.to_web() async def stream_messages_events( self, user_prompt: str | List[str] ) -> AsyncGenerator[str, None]: """ 流式输出消息事件 :param user_prompt: 用户提示词(用户输入消息) :return: 消息事件 """ """定义:一次会话(session)包含若干论对话(turn),每一轮对话由用户输入消息(message)和智能体输出消息组成""" # 获取指定会话的消息历史 message_history = self.agent_memory.get_message_history( session_id=self.session_id ) async with self.agent.run_stream_events( user_prompt=user_prompt, message_history=message_history, ) as events: async for event in events: # 处理片段开始事件 if isinstance(event, PartStartEvent): part = event.part if isinstance(part, ThinkingPart): # 处理思考片段 if content := part.content: yield f"00:{content}" elif isinstance(part, TextPart): # 处理文本片段 if content := part.content: yield f"01:{content}" elif isinstance( part, ( NativeToolSearchCallPart, NativeToolCallPart, ToolSearchCallPart, ToolCallPart, LoadCapabilityCallPart, ), ): # 处理原生搜索、原生工具、自封搜索、自封工具和加载能力调用片段 yield f"02:技能名称:{part.tool_name}" self.agent_memory.create_new_messages( session_id=self.session_id, new_messages=events.new_messages(), )