ForcePilot/src/agents/registry.py

370 lines
13 KiB
Python
Raw Normal View History

2025-03-28 11:40:46 +08:00
from __future__ import annotations
import os
import yaml
import uuid
from pathlib import Path
from typing import Annotated, TypedDict, Optional, Any
2025-03-24 19:07:51 +08:00
from abc import abstractmethod
2025-03-28 11:40:46 +08:00
from dataclasses import dataclass, fields, field
2025-03-24 19:07:51 +08:00
2025-07-22 17:29:38 +08:00
from pydantic import BaseModel, Field
2025-03-28 11:40:46 +08:00
from langchain_core.runnables import RunnableConfig
2025-03-25 05:40:07 +08:00
from langchain_core.messages import BaseMessage
2025-03-24 19:07:51 +08:00
from langgraph.graph.state import CompiledStateGraph
2025-03-25 05:40:07 +08:00
from langgraph.graph.message import add_messages
2025-03-24 19:07:51 +08:00
2025-04-02 13:00:25 +08:00
from src.utils import logger
2025-03-24 19:07:51 +08:00
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
@dataclass(kw_only=True)
2025-04-02 13:00:25 +08:00
class Configuration(dict):
2025-03-24 19:07:51 +08:00
"""
定义一个基础 Configuration 各类 graph 继承
配置优先级:
1. 运行时配置(RunnableConfig)最高优先级直接从函数参数传入
2. 文件配置(config.private.yaml)中等优先级从文件加载
3. 类默认配置最低优先级类中定义的默认值
2025-03-24 19:07:51 +08:00
"""
2025-03-28 11:40:46 +08:00
thread_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "线程ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
user_id: str = field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"name": "用户ID",
"configurable": False,
"description": "用来描述智能体的角色和行为"
},
)
2025-03-28 11:40:46 +08:00
@classmethod
def from_runnable_config(
cls, config: RunnableConfig | None = None, module_name: str | None = None
2025-03-28 11:40:46 +08:00
) -> Configuration:
"""Create a Configuration instance from a RunnableConfig object.
Args:
config: RunnableConfig object with highest priority
module_name: Name of the agent to load config file for
Returns:
Configuration instance with merged config values
"""
# 获取类默认配置:创建一个实例获取所有默认值
instance = cls()
2025-03-28 11:40:46 +08:00
_fields = {f.name for f in fields(cls) if f.init}
# 尝试加载文件配置(中等优先级)
file_config = {}
if module_name:
file_config = cls.from_file(module_name)
# 获取运行时配置(最高优先级)
configurable = (config.get("configurable") or {}) if config else {}
# 合并三级配置,注意优先级
merged_config = {}
2025-05-24 11:29:45 +08:00
for config_field in _fields:
# 1. 默认使用类默认值
2025-05-24 11:29:45 +08:00
if hasattr(instance, config_field):
merged_config[config_field] = getattr(instance, config_field)
# 2. 如果文件配置中有此字段,则覆盖
2025-05-24 11:29:45 +08:00
if config_field in file_config:
merged_config[config_field] = file_config[config_field]
# 3. 如果运行时配置中有此字段,则覆盖
2025-05-24 11:29:45 +08:00
if config_field in configurable:
merged_config[config_field] = configurable[config_field]
# 创建并返回配置实例
# logger.debug(f"合并配置: {merged_config}")
return cls(**merged_config)
@classmethod
def from_file(cls, module_name: str) -> Configuration:
"""从文件加载配置"""
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
file_config = {}
if os.path.exists(config_file_path):
try:
2025-05-24 11:29:45 +08:00
with open(config_file_path, encoding='utf-8') as f:
file_config = yaml.safe_load(f) or {}
# logger.info(f"从文件加载智能体 {module_name} 配置: {file_config}")
except Exception as e:
logger.error(f"加载智能体配置文件出错: {e}")
return file_config
@classmethod
def save_to_file(cls, config: dict, module_name: str) -> bool:
"""Save configuration to a YAML file
Args:
config: Configuration dictionary to save
module_name: Name of the agent to save config for
Returns:
True if saving was successful, False otherwise
"""
try:
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
# 确保目录存在
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
with open(config_file_path, 'w', encoding='utf-8') as f:
yaml.dump(config, f, indent=2, allow_unicode=True)
# logger.info(f"智能体 {module_name} 配置已保存到 {config_file_path}")
return True
except Exception as e:
logger.error(f"保存智能体配置文件出错: {e}")
return False
2025-03-28 11:40:46 +08:00
@classmethod
def to_dict(cls):
2025-04-02 00:00:04 +08:00
# 创建一个实例来处理 default_factory
instance = cls()
2025-04-02 13:00:25 +08:00
confs = {}
configurable_items = {}
for f in fields(cls):
if f.init and not f.metadata.get("hide", False):
value = getattr(instance, f.name)
if callable(value) and hasattr(value, "__call__"):
confs[f.name] = value()
else:
confs[f.name] = value
if f.metadata.get("configurable", True):
2025-04-02 13:00:25 +08:00
configurable_items[f.name] = {
"type": f.type.__name__,
"name": f.metadata.get("name", f.name),
"options": f.metadata.get("options", []),
"default": f.default,
"description": f.metadata.get("description", ""),
}
confs["configurable_items"] = configurable_items
return confs
2025-03-25 05:40:07 +08:00
2025-03-24 19:07:51 +08:00
2025-07-22 17:29:38 +08:00
class BaseModelConfiguration(BaseModel):
thread_id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"title": "线程ID",
"description": "用来描述智能体的角色和行为",
"configurable": False,
},
)
user_id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
metadata={
"title": "用户ID",
"description": "用来描述智能体的角色和行为",
"configurable": False,
},
)
@classmethod
def from_runnable_config(
cls,
config: RunnableConfig | None = None,
module_name: str | None = None,
) -> BaseModelConfiguration:
2025-07-22 17:29:38 +08:00
"""
RunnableConfig YAML 文件中构建 Configuration 实例
"""
# 默认配置
default_instance = cls()
default_values = default_instance.dict()
# 文件配置
file_config = cls.from_file(module_name) if module_name else {}
2025-07-22 17:29:38 +08:00
# 运行时配置(最高优先级)
runtime_config = config.get("configurable") if config else {}
merged_config = {
**default_values,
**file_config,
**runtime_config,
}
return cls(**merged_config)
@classmethod
def from_file(cls, module_name: str) -> dict[str, Any]:
2025-07-22 17:29:38 +08:00
"""
YAML 文件加载配置
"""
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
2025-07-22 17:29:38 +08:00
if os.path.exists(config_file_path):
try:
with open(config_file_path, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception as e:
logger.error(f"加载配置文件失败: {e}")
return {}
@classmethod
def save_to_file(cls, config: dict, module_name: str) -> bool:
2025-07-22 17:29:38 +08:00
"""
保存配置到 YAML 文件
"""
try:
config_file_path = Path(f"src/agents/{module_name}/config.private.yaml")
2025-07-22 17:29:38 +08:00
os.makedirs(config_file_path.parent, exist_ok=True)
with open(config_file_path, "w", encoding="utf-8") as f:
yaml.dump(config, f, indent=2, allow_unicode=True)
return True
except Exception as e:
logger.error(f"保存配置文件失败: {e}")
return False
@classmethod
def to_dict(cls) -> dict[str, Any]:
"""
提取类字段的默认值与字段元数据主要用于前端动态生成配置项
"""
# 创建实例以获得 default_factory 值
instance = cls()
confs: dict[str, Any] = {}
configurable_items: dict[str, Any] = {}
for name, _field in cls.model_fields.items():
2025-07-22 17:29:38 +08:00
value = getattr(instance, name)
confs[name] = value
# 安全地处理 Pydantic 字段元数据
field_metadata = {}
if hasattr(_field, 'json_schema_extra') and _field.json_schema_extra:
if isinstance(_field.json_schema_extra, dict):
field_metadata = _field.json_schema_extra['metadata']
elif isinstance(_field.json_schema_extra, list | tuple):
2025-07-22 17:29:38 +08:00
# 在 Pydantic v2 中metadata 可能是列表,合并所有字典项
for item in _field.json_schema_extra:
2025-07-22 17:29:38 +08:00
if isinstance(item, dict):
field_metadata.update(item['metadata'])
# 检查字段是否应该可配置 - 支持不同的元数据格式
if field_metadata.get("configurable", True):
default_type = _field.annotation.__name__ if hasattr(_field.annotation, '__name__') else 'str'
2025-07-22 17:29:38 +08:00
configurable_items[name] = {
"type": field_metadata.get("type", default_type),
2025-07-22 17:29:38 +08:00
"name": field_metadata.get("name") or name,
"options": field_metadata.get("options") or [],
"default": _field.default if _field.default is not None else None,
2025-07-22 17:29:38 +08:00
"description": field_metadata.get("description") or "",
"x_oap_ui_config": field_metadata.get("x_oap_ui_config", {}),
}
confs["configurable_items"] = configurable_items
return confs
2025-05-24 11:29:45 +08:00
class BaseAgent:
2025-03-24 19:07:51 +08:00
"""
定义一个基础 Agent 各类 graph 继承
"""
name = "base_agent"
description = "base_agent"
config_schema: Configuration = Configuration
2025-03-28 11:40:46 +08:00
def __init__(self, **kwargs):
pass
@property
def module_name(self) -> str:
"""Get the module name of the agent class."""
return self.__class__.__module__.split('.')[-2]
@property
def id(self) -> str:
"""Get the agent's class name."""
return self.__class__.__name__
async def get_info(self):
2025-04-01 22:16:07 +08:00
return {
"id": self.id,
"name": self.name if hasattr(self, "name") else "Unknown",
"description": self.description if hasattr(self, "description") else "Unknown",
"config_schema": self.config_schema.to_dict(),
"all_tools": self.all_tools if hasattr(self, "all_tools") else [],
"has_checkpointer": await self.check_checkpointer(),
2025-04-01 22:16:07 +08:00
}
2025-05-16 23:46:25 +08:00
async def stream_values(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs):
graph = await self.get_graph()
logger.debug(f"stream_values: {config_schema}")
2025-05-16 23:46:25 +08:00
for event in graph.astream({"messages": messages}, stream_mode="values", config=config_schema):
yield event["messages"]
2025-05-16 23:46:25 +08:00
async def stream_messages(self, messages: list[str], config_schema: RunnableConfig = None, **kwargs):
graph = await self.get_graph()
logger.debug(f"stream_messages: {config_schema}")
2025-05-16 23:46:25 +08:00
async for msg, metadata in graph.astream({"messages": messages}, stream_mode="messages", config=config_schema):
2025-04-02 00:00:04 +08:00
yield msg, metadata
2025-03-24 19:07:51 +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
2025-05-16 23:46:25 +08:00
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:
messages = state.values.get('messages', [])
for msg in messages:
if hasattr(msg, 'model_dump'):
msg_dict = msg.model_dump() # 转换成字典
else:
msg_dict = dict(msg) if hasattr(msg, '__dict__') else {"content": str(msg)}
result.append(msg_dict)
return result
except Exception as e:
logger.error(f"获取智能体 {self.name} 历史消息出错: {e}")
return []
2025-03-24 19:07:51 +08:00
@abstractmethod
async def get_graph(self, **kwargs) -> CompiledStateGraph:
"""
获取并编译对话图实例
必须确保在编译时设置 checkpointer否则将无法获取历史记录
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
"""
2025-05-24 11:29:45 +08:00
pass