2025-08-31 00:34:26 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-07 17:12:29 +08:00
|
|
|
|
import asyncio
|
2025-11-07 12:28:58 +08:00
|
|
|
|
import importlib.util
|
2025-10-12 16:04:46 +08:00
|
|
|
|
import os
|
2025-11-12 11:00:39 +08:00
|
|
|
|
import tomllib as tomli
|
2025-08-31 00:34:26 +08:00
|
|
|
|
from abc import abstractmethod
|
2026-03-07 17:12:29 +08:00
|
|
|
|
from inspect import isawaitable
|
2025-10-24 00:11:52 +08:00
|
|
|
|
from pathlib import Path
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-10-11 21:10:39 +08:00
|
|
|
|
from langgraph.checkpoint.memory import InMemorySaver
|
2025-10-12 16:04:46 +08:00
|
|
|
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
|
2025-10-24 00:11:52 +08:00
|
|
|
|
from langgraph.graph.state import CompiledStateGraph
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
2025-10-12 16:04:46 +08:00
|
|
|
|
from src import config as sys_config
|
2025-08-31 00:34:26 +08:00
|
|
|
|
from src.agents.common.context import BaseContext
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from src.utils import logger
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BaseAgent:
|
|
|
|
|
|
"""
|
|
|
|
|
|
定义一个基础 Agent 供 各类 graph 继承
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
name = "base_agent"
|
|
|
|
|
|
description = "base_agent"
|
2025-11-08 10:51:30 +08:00
|
|
|
|
capabilities: list[str] = [] # 智能体能力列表,如 ["file_upload", "web_search"] 等
|
2025-12-02 16:47:21 +08:00
|
|
|
|
context_schema: type[BaseContext] = BaseContext # 智能体上下文 schema
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
|
self.graph = None # will be covered by get_graph
|
2025-10-12 16:04:46 +08:00
|
|
|
|
self.checkpointer = None
|
2026-03-07 17:12:29 +08:00
|
|
|
|
self._checkpointer_cm = None
|
2026-01-22 02:38:37 +08:00
|
|
|
|
self._async_conn = None
|
2025-10-12 16:04:46 +08:00
|
|
|
|
self.workdir = Path(sys_config.save_dir) / "agents" / self.module_name
|
|
|
|
|
|
self.workdir.mkdir(parents=True, exist_ok=True)
|
2025-11-07 12:28:58 +08:00
|
|
|
|
self._metadata_cache = None # Cache for metadata to avoid repeated file reads
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def module_name(self) -> str:
|
|
|
|
|
|
"""Get the module name of the agent class."""
|
2025-09-01 22:37:03 +08:00
|
|
|
|
return self.__class__.__module__.split(".")[-2]
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def id(self) -> str:
|
|
|
|
|
|
"""Get the agent's class name."""
|
|
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
|
|
2026-02-26 18:31:55 +08:00
|
|
|
|
async def get_info(self, include_configurable_items: bool = True):
|
2025-11-07 12:28:58 +08:00
|
|
|
|
# Load metadata from file
|
|
|
|
|
|
metadata = self.load_metadata()
|
2026-02-26 18:31:55 +08:00
|
|
|
|
configurable_items = {}
|
|
|
|
|
|
if include_configurable_items:
|
|
|
|
|
|
configurable_items = self.context_schema.get_configurable_items()
|
2025-11-07 12:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
# Merge metadata with class attributes, metadata takes precedence
|
2025-08-31 00:34:26 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"id": self.id,
|
2025-11-07 12:28:58 +08:00
|
|
|
|
"name": metadata.get("name", getattr(self, "name", "Unknown")),
|
|
|
|
|
|
"description": metadata.get("description", getattr(self, "description", "Unknown")),
|
|
|
|
|
|
"examples": metadata.get("examples", []),
|
2026-02-26 18:31:55 +08:00
|
|
|
|
"configurable_items": configurable_items,
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"has_checkpointer": await self.check_checkpointer(),
|
2025-11-08 10:51:30 +08:00
|
|
|
|
"capabilities": getattr(self, "capabilities", []), # 智能体能力列表
|
2025-08-31 00:34:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async def get_config(self):
|
|
|
|
|
|
return self.context_schema.from_file(module_name=self.module_name)
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def stream_values(self, messages: list[str], input_context=None, **kwargs):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
graph = await self.get_graph()
|
2026-01-22 05:57:13 +08:00
|
|
|
|
context = self.context_schema()
|
|
|
|
|
|
agent_config = (input_context or {}).get("agent_config")
|
|
|
|
|
|
if isinstance(agent_config, dict):
|
|
|
|
|
|
context.update(agent_config)
|
|
|
|
|
|
context.update(input_context or {})
|
2026-03-01 17:44:53 +08:00
|
|
|
|
for event in graph.astream({"messages": messages}, stream_mode="values", context=context):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
yield event["messages"]
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def stream_messages(self, messages: list[str], input_context=None, **kwargs):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
graph = await self.get_graph()
|
2026-01-22 05:57:13 +08:00
|
|
|
|
context = self.context_schema()
|
|
|
|
|
|
agent_config = (input_context or {}).get("agent_config")
|
|
|
|
|
|
if isinstance(agent_config, dict):
|
|
|
|
|
|
context.update(agent_config)
|
|
|
|
|
|
context.update(input_context or {})
|
2025-09-01 03:38:46 +08:00
|
|
|
|
logger.debug(f"stream_messages: {context}")
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 构建配置:LangGraph 会自动从 checkpointer 恢复 state
|
2026-01-22 05:57:13 +08:00
|
|
|
|
input_config = {
|
|
|
|
|
|
"configurable": {"thread_id": context.thread_id, "user_id": context.user_id},
|
|
|
|
|
|
"recursion_limit": 300,
|
|
|
|
|
|
}
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async for msg, metadata in graph.astream(
|
2026-02-13 22:16:11 +08:00
|
|
|
|
{"messages": messages},
|
2025-11-08 10:51:30 +08:00
|
|
|
|
stream_mode="messages",
|
|
|
|
|
|
context=context,
|
|
|
|
|
|
config=input_config,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
yield msg, metadata
|
|
|
|
|
|
|
2025-10-27 15:32:12 +08:00
|
|
|
|
async def invoke_messages(self, messages: list[str], input_context=None, **kwargs):
|
|
|
|
|
|
graph = await self.get_graph()
|
2026-01-22 05:57:13 +08:00
|
|
|
|
context = self.context_schema()
|
|
|
|
|
|
agent_config = (input_context or {}).get("agent_config")
|
|
|
|
|
|
if isinstance(agent_config, dict):
|
|
|
|
|
|
context.update(agent_config)
|
|
|
|
|
|
context.update(input_context or {})
|
2025-10-27 15:32:12 +08:00
|
|
|
|
logger.debug(f"invoke_messages: {context}")
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 构建配置
|
2026-01-22 05:57:13 +08:00
|
|
|
|
input_config = {
|
|
|
|
|
|
"configurable": {"thread_id": context.thread_id, "user_id": context.user_id},
|
|
|
|
|
|
"recursion_limit": 100,
|
|
|
|
|
|
}
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
msg = await graph.ainvoke(
|
2026-02-13 22:16:11 +08:00
|
|
|
|
{"messages": messages},
|
|
|
|
|
|
context=context,
|
|
|
|
|
|
config=input_config,
|
2025-11-08 10:51:30 +08:00
|
|
|
|
)
|
2025-10-27 15:32:12 +08:00
|
|
|
|
return msg
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
async def check_checkpointer(self):
|
|
|
|
|
|
app = await self.get_graph()
|
|
|
|
|
|
if not hasattr(app, "checkpointer") or app.checkpointer is None:
|
|
|
|
|
|
logger.warning(f"智能体 {self.name} 的 Graph 未配置 checkpointer,无法获取历史记录")
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def get_history(self, user_id, thread_id) -> list[dict]:
|
|
|
|
|
|
"""获取历史消息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
app = await self.get_graph()
|
|
|
|
|
|
|
|
|
|
|
|
if not await self.check_checkpointer():
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
|
|
|
|
|
|
state = await app.aget_state(config)
|
|
|
|
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
|
if state:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
messages = state.values.get("messages", [])
|
2025-08-31 00:34:26 +08:00
|
|
|
|
for msg in messages:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
if hasattr(msg, "model_dump"):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
msg_dict = msg.model_dump() # 转换成字典
|
|
|
|
|
|
else:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
msg_dict = dict(msg) if hasattr(msg, "__dict__") else {"content": str(msg)}
|
2025-08-31 00:34:26 +08:00
|
|
|
|
result.append(msg_dict)
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取智能体 {self.name} 历史消息出错: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2025-12-28 22:55:42 +08:00
|
|
|
|
def reload_graph(self):
|
|
|
|
|
|
"""重置 graph 缓存,强制下次调用 get_graph 时重新构建"""
|
|
|
|
|
|
self.graph = None
|
2026-03-07 17:12:29 +08:00
|
|
|
|
self.checkpointer = None
|
|
|
|
|
|
if self._checkpointer_cm is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
loop = None
|
|
|
|
|
|
|
|
|
|
|
|
if loop is not None:
|
|
|
|
|
|
loop.create_task(self._close_checkpointer_context())
|
2025-12-28 22:55:42 +08:00
|
|
|
|
logger.info(f"{self.name} graph 缓存已清空,将在下次调用时重新构建")
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
@abstractmethod
|
|
|
|
|
|
async def get_graph(self, **kwargs) -> CompiledStateGraph:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取并编译对话图实例。
|
|
|
|
|
|
必须确保在编译时设置 checkpointer,否则将无法获取历史记录。
|
|
|
|
|
|
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
|
|
|
|
|
|
"""
|
|
|
|
|
|
pass
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
|
|
|
|
|
async def _get_checkpointer(self):
|
2026-01-22 02:38:37 +08:00
|
|
|
|
if self.checkpointer is not None:
|
|
|
|
|
|
return self.checkpointer
|
|
|
|
|
|
|
2025-10-12 16:04:46 +08:00
|
|
|
|
checkpointer = None
|
2026-02-25 16:26:09 +08:00
|
|
|
|
backend = os.getenv("LANGGRAPH_CHECKPOINTER_BACKEND", "sqlite").strip().lower()
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
2026-02-25 16:26:09 +08:00
|
|
|
|
if backend == "postgres":
|
|
|
|
|
|
checkpointer = await self._create_postgres_checkpointer()
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
2026-02-25 16:26:09 +08:00
|
|
|
|
if checkpointer is None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
checkpointer = AsyncSqliteSaver(await self.get_async_conn())
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"构建 sqlite checkpointer 失败: {e}, 尝试使用内存存储")
|
|
|
|
|
|
checkpointer = InMemorySaver()
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
2026-01-22 02:38:37 +08:00
|
|
|
|
self.checkpointer = checkpointer
|
|
|
|
|
|
return self.checkpointer
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
2026-02-25 16:26:09 +08:00
|
|
|
|
async def _create_postgres_checkpointer(self):
|
|
|
|
|
|
postgres_url = os.getenv("POSTGRES_URL")
|
|
|
|
|
|
if not postgres_url:
|
|
|
|
|
|
logger.warning("POSTGRES_URL 未配置,无法启用 postgres checkpointer,回退 sqlite")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver # type: ignore
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"langgraph postgres checkpointer 不可用,回退 sqlite: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
conn_str = postgres_url.replace("+asyncpg", "")
|
|
|
|
|
|
try:
|
|
|
|
|
|
saver_factory = getattr(AsyncPostgresSaver, "from_conn_string", None)
|
|
|
|
|
|
if callable(saver_factory):
|
|
|
|
|
|
saver = saver_factory(conn_str)
|
|
|
|
|
|
else:
|
|
|
|
|
|
saver = AsyncPostgresSaver(conn_str) # type: ignore[call-arg]
|
|
|
|
|
|
|
2026-03-07 17:12:29 +08:00
|
|
|
|
if hasattr(saver, "__aenter__") and hasattr(saver, "__aexit__"):
|
|
|
|
|
|
self._checkpointer_cm = saver
|
|
|
|
|
|
saver = await saver.__aenter__()
|
|
|
|
|
|
|
2026-02-25 16:26:09 +08:00
|
|
|
|
setup_fn = getattr(saver, "setup", None)
|
|
|
|
|
|
if callable(setup_fn):
|
|
|
|
|
|
result = setup_fn()
|
2026-03-07 17:12:29 +08:00
|
|
|
|
if isawaitable(result):
|
2026-02-25 16:26:09 +08:00
|
|
|
|
await result
|
|
|
|
|
|
logger.info(f"{self.name} 使用 postgres checkpointer")
|
|
|
|
|
|
return saver
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"初始化 postgres checkpointer 失败,回退 sqlite: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-07 17:12:29 +08:00
|
|
|
|
async def _close_checkpointer_context(self):
|
|
|
|
|
|
if self._checkpointer_cm is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
cm = self._checkpointer_cm
|
|
|
|
|
|
self._checkpointer_cm = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
await cm.__aexit__(None, None, None)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"关闭 postgres checkpointer 失败: {e}")
|
|
|
|
|
|
|
2025-10-12 16:04:46 +08:00
|
|
|
|
async def get_async_conn(self) -> aiosqlite.Connection:
|
|
|
|
|
|
"""获取异步数据库连接"""
|
2026-01-22 02:38:37 +08:00
|
|
|
|
if self._async_conn is not None:
|
|
|
|
|
|
return self._async_conn
|
|
|
|
|
|
|
2025-12-15 10:04:30 +08:00
|
|
|
|
conn = await aiosqlite.connect(os.path.join(self.workdir, "aio_history.db"))
|
|
|
|
|
|
# Patch: langgraph's AsyncSqliteSaver expects is_alive() method which aiosqlite may not have
|
|
|
|
|
|
if not hasattr(conn, "is_alive"):
|
|
|
|
|
|
conn.is_alive = lambda: True
|
2026-01-22 02:38:37 +08:00
|
|
|
|
self._async_conn = conn
|
|
|
|
|
|
return self._async_conn
|
2025-10-12 16:04:46 +08:00
|
|
|
|
|
|
|
|
|
|
async def get_aio_memory(self) -> AsyncSqliteSaver:
|
|
|
|
|
|
"""获取异步存储实例"""
|
|
|
|
|
|
return AsyncSqliteSaver(await self.get_async_conn())
|
2025-11-07 12:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
def load_metadata(self) -> dict:
|
|
|
|
|
|
"""Load metadata from metadata.toml file in the agent's source directory."""
|
|
|
|
|
|
if self._metadata_cache is not None:
|
|
|
|
|
|
return self._metadata_cache
|
|
|
|
|
|
|
|
|
|
|
|
# Try to find metadata.toml in the agent's source directory
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Get the agent's source file directory
|
|
|
|
|
|
agent_module = self.__class__.__module__
|
|
|
|
|
|
|
|
|
|
|
|
# Use importlib to get the module's file path
|
|
|
|
|
|
spec = importlib.util.find_spec(agent_module)
|
|
|
|
|
|
if spec and spec.origin:
|
|
|
|
|
|
agent_file = Path(spec.origin)
|
|
|
|
|
|
agent_dir = agent_file.parent
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Fallback: construct path from module name
|
|
|
|
|
|
module_path = agent_module.replace(".", "/")
|
|
|
|
|
|
agent_file = Path(f"src/{module_path}.py")
|
|
|
|
|
|
agent_dir = agent_file.parent
|
|
|
|
|
|
|
|
|
|
|
|
metadata_file = agent_dir / "metadata.toml"
|
|
|
|
|
|
|
|
|
|
|
|
if metadata_file.exists():
|
|
|
|
|
|
with open(metadata_file, "rb") as f:
|
|
|
|
|
|
metadata = tomli.load(f)
|
|
|
|
|
|
self._metadata_cache = metadata
|
|
|
|
|
|
logger.debug(f"Loaded metadata from {metadata_file}")
|
|
|
|
|
|
return metadata
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.debug(f"No metadata.toml found for {self.module_name} at {metadata_file}")
|
|
|
|
|
|
self._metadata_cache = {}
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error loading metadata for {self.module_name}: {e}")
|
|
|
|
|
|
self._metadata_cache = {}
|
2025-11-08 10:51:30 +08:00
|
|
|
|
return {}
|