371 lines
12 KiB
Python
371 lines
12 KiB
Python
# -*- 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
|
||
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 PartCache(BaseModel):
|
||
"""
|
||
片段缓存模型
|
||
"""
|
||
|
||
type_: Literal["thinking", "text"] = Field(..., description="片段类型")
|
||
content: str = Field(..., description="片段内容")
|
||
content_delta: str = Field(default="", description="片段内容增量")
|
||
task: Optional[Task] = Field(default=None, description="片段绑定的异步协程任务")
|
||
|
||
|
||
class EventsDebouncer:
|
||
"""
|
||
模型消息事件防抖器
|
||
"""
|
||
|
||
def __init__(self, delay: float = 0.2, content_delta_limit: int = 50):
|
||
"""
|
||
初始化
|
||
:param delay: 异步协程任务延迟时间(秒)
|
||
:param content_delta_limit: 增量片段内容的字数上限
|
||
"""
|
||
self.delay = delay
|
||
self.content_delta_limit = content_delta_limit
|
||
|
||
# 所有片段缓存
|
||
self.part_caches: Dict[int, PartCache] = {}
|
||
# 待刷新列队
|
||
self.pending_flush_queue = Queue()
|
||
|
||
async def handle_event(
|
||
self, event: ModelMessageEvent
|
||
) -> AsyncGenerator[ModelMessageEvent, None]:
|
||
"""
|
||
处理模型消息事件:合并片段增量事件
|
||
:param event: 模型消息事件
|
||
:yield: AsyncGenerator
|
||
"""
|
||
# 从待刷新列队中返回刷新事件
|
||
async for flush_event in self._yield_pending_flush_events():
|
||
yield flush_event
|
||
|
||
# 若为片段开始事件且片段类型为思考或纯文本则创建片段缓存,否则直接返回
|
||
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.part_caches[index] = PartCache(
|
||
type_=type_,
|
||
content=part.content or "",
|
||
)
|
||
yield event
|
||
return
|
||
|
||
# 若为片段增量事件且增量类型为思考或纯文本则更新片段内容和增量片段内容并刷新片段缓存,否则直接返回
|
||
if isinstance(event, PartDeltaEvent):
|
||
index, delta = event.index, event.delta
|
||
part_cache = self.part_caches.get(index)
|
||
if part_cache and isinstance(delta, (ThinkingPartDelta, TextPartDelta)):
|
||
part_cache.content += delta.content_delta or ""
|
||
part_cache.content_delta += delta.content_delta or ""
|
||
# 取消异步协程任务
|
||
self._cancel_task(index=index)
|
||
# 创建异步协程任务
|
||
part_cache.task = create_task(coro=self._delay_flush(index=index))
|
||
else:
|
||
yield event
|
||
return
|
||
|
||
# 若为其它事件则先刷新所有片段缓存再直接返回
|
||
async for flush_event in self._batch_flush():
|
||
yield flush_event
|
||
yield event
|
||
|
||
# 若为片段结束事件则删除该片段缓存
|
||
if isinstance(event, PartEndEvent):
|
||
self.part_caches.pop(event.index, None)
|
||
return
|
||
|
||
async def _yield_pending_flush_events(
|
||
self,
|
||
) -> AsyncGenerator[ModelMessageEvent, None]:
|
||
"""
|
||
从待刷新列队中返回刷新事件
|
||
:return: AsyncGenerator
|
||
"""
|
||
while not self.pending_flush_queue.empty():
|
||
flush_event = await self.pending_flush_queue.get_nowait()
|
||
yield flush_event
|
||
|
||
def _cancel_task(self, index: int) -> None:
|
||
"""
|
||
取消异步协程任务
|
||
:param index: 片段索引
|
||
:yield: None
|
||
"""
|
||
part_cache = self.part_caches.get(index)
|
||
if part_cache and part_cache.task and not part_cache.task.done():
|
||
part_cache.task.cancel()
|
||
part_cache.task = None
|
||
|
||
async def _flush(self, index: int) -> Optional[PartDeltaEvent]:
|
||
"""刷新片段缓存"""
|
||
part_cache = self.part_caches.get(index)
|
||
if not part_cache or not part_cache.content_delta:
|
||
return None
|
||
|
||
match part_cache.type_:
|
||
case "thinking":
|
||
delta = ThinkingPartDelta(content_delta=part_cache.content_delta)
|
||
case "text":
|
||
delta = TextPartDelta(content_delta=part_cache.content_delta)
|
||
|
||
part_cache.content_delta = ""
|
||
part_cache.task = None
|
||
return PartDeltaEvent(index=index, delta=delta)
|
||
|
||
async def _delay_flush(self, index: int):
|
||
"""
|
||
延迟刷新片段缓存
|
||
:param index: 片段索引
|
||
:return: None
|
||
"""
|
||
await sleep(delay=self.delay)
|
||
flush_event = await self._flush(index=index)
|
||
if flush_event:
|
||
await self.pending_flush_queue.put(item=flush_event)
|
||
|
||
async def _batch_flush(self):
|
||
"""
|
||
批量刷新片段缓存
|
||
"""
|
||
for index in list(self.part_caches.keys()):
|
||
# 刷新片段缓存
|
||
flush_event = await self._flush(index)
|
||
if flush_event:
|
||
yield flush_event
|
||
|
||
|
||
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(),
|
||
)
|