Python/utils/agent.py

309 lines
10 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 -*-
"""
智能体模块
"""
# 列举导入模块
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
from pydantic import Field
from pydantic import BaseModel
from pydantic_ai.messages import (
ModelMessagesTypeAdapter,
ModelResponse,
PartStartEvent,
PartDeltaEvent,
PartEndEvent,
TextPart,
ToolSearchCallPart,
NativeToolSearchCallPart,
LoadCapabilityCallPart,
ThinkingPart,
ToolCallPart,
NativeToolCallPart,
TextPartDelta,
ThinkingPartDelta,
ModelResponseStreamEvent,
ToolReturnPart,
)
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 Part(BaseModel):
"""
片段模型
"""
type: Literal["thinking", "text"] = Field(..., description="片段类型")
content: str = Field(..., description="片段内容")
timer: Optional[Task] = Field(default=None, description="片段绑定的延时异步任务")
class StreamEventsDebouncer:
"""
流式事件防抖器
"""
def __init__(self, debounce_by: float = 0.2):
"""
初始化
:param debounce_by: 防抖间隔(秒)
"""
# 片段缓存
self.parts: Dict[int, Part] = {}
async def handle_event(self, event):
"""
处理流式事件
:param event: 流式事件
:yield: 处理后流式事件
"""
# 若为片段开始事件且片段类型为思考或纯文本则创建片段缓存,否则直接返回
if isinstance(event, PartStartEvent):
index, part = event.index, event.part
match part:
case ThinkingPart():
part_type = "thinking"
case TextPart():
part_type = "text"
case _:
yield event
return
# 缓存片段
self.parts[index] = Part(
type=part_type,
content=part.content if part.content else "",
)
# 片段首次触发直接推送原始事件,保证首条内容立刻展示
yield event
return
# 若为片段增量事件且增量类型为思考或纯文本则在片段缓存的片段内容追加
if isinstance(event, PartDeltaEvent):
index, delta = event.index, event.delta
part = self.parts.get(index)
if isinstance(delta, (ThinkingPartDelta, TextPartDelta)):
# 增量内容追加至缓冲区
part.content += delta.content_delta
# 重置防抖计时器,实现滑动窗口效果
self._cancel_timer(index)
part.timer = create_task(self._delay_flush(index))
else:
# 非文本类增量事件直接透传
yield event
return
# 命中强制刷新规则:先推送所有缓存聚合内容,再透传当前触发事件
if isinstance(event, FLUSH_TRIGGER_TYPES):
async for flush_event in self.flush_all():
yield flush_event
yield event
return
# 其余未匹配的所有事件原样透传
yield event
def _cancel_timer(self, index: int):
"""
取消指定索引片段的计时器
:param index: 片段索引
"""
part = self.parts.get(index)
if part and part.timer is not None and not part.timer.done():
part.timer.cancel()
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(),
)