309 lines
10 KiB
Python
309 lines
10 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
|
||
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="片段内容")
|
||
task: 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_task(self, index: int):
|
||
"""
|
||
取消指定索引片段的异步任务
|
||
:param index: 片段索引
|
||
"""
|
||
part = self.parts.get(index)
|
||
if part and part.task is not None and not part.task.done():
|
||
part.task.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(),
|
||
)
|