存档
This commit is contained in:
@@ -12,14 +12,14 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Type
|
||||
from ulid import ULID
|
||||
|
||||
from kilostar.plugin_runtime.event import OrgEvent, TaskState
|
||||
from kilostar.plugin_runtime.manifest import OrgManifest
|
||||
from kilostar.plugin_runtime.agents_config import AgentsConfig, AgentDef
|
||||
from kilostar.utils.logger import get_logger
|
||||
from kilostar.utils.settings import get_artifact_dir
|
||||
from kilostar.utils.settings import get_artifact_dir, get_plugin_data_dir
|
||||
|
||||
|
||||
class BaseOrganization:
|
||||
@@ -59,6 +59,10 @@ class BaseOrganization:
|
||||
self._tools_by_name: Dict[str, Callable] = {}
|
||||
self._agents: Dict[str, Any] = {} # name -> pydantic-ai Agent
|
||||
|
||||
# 插件本地 SQLite 引擎(按需启用,调 init_local_db)
|
||||
self._engine: Any = None
|
||||
self._session_maker: Any = None
|
||||
|
||||
# ─── 生命周期 ──────────────────────────────────────────────
|
||||
|
||||
async def setup(self) -> None:
|
||||
@@ -74,6 +78,48 @@ class BaseOrganization:
|
||||
self._stopped = True
|
||||
if self._worker_task is not None:
|
||||
self._worker_task.cancel()
|
||||
if self._engine is not None:
|
||||
try:
|
||||
await self._engine.dispose()
|
||||
except Exception:
|
||||
self.logger.debug("engine dispose failed; ignored")
|
||||
|
||||
async def on_first_install(self) -> None:
|
||||
"""安装期一次性钩子:插件首次落地时被调用一次。
|
||||
|
||||
典型用途:建数据表、写默认配置、提示用户去前端做后续配置。失败会抛错并让
|
||||
plugin_manager 回滚(不写 marker,下次启动会重试)。子类按需覆盖;默认空实现。
|
||||
"""
|
||||
return None
|
||||
|
||||
async def init_local_db(self, base_classes: List[Type[Any]]) -> None:
|
||||
"""建立插件私有 SQLite 引擎并按 ``base_classes`` 的元数据建表。
|
||||
|
||||
``base_classes`` 是插件自己定义的 ``DeclarativeBase`` 子类(每个插件用独立的 Base,
|
||||
避免跟核心 PG 模型的元数据空间串场)。每次 setup 调用都安全:
|
||||
``create_all`` 是幂等的,已存在的表不会被改动。
|
||||
|
||||
建立后 ``self._session_maker`` 可用于工具/API 内部按需 ``async with sm() as s``。
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
|
||||
db_path = get_plugin_data_dir(self.name) / f"{self.name}.db"
|
||||
url = f"sqlite+aiosqlite:///{db_path}"
|
||||
self._engine = create_async_engine(url, future=True)
|
||||
self._session_maker = async_sessionmaker(
|
||||
self._engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
async with self._engine.begin() as conn:
|
||||
for base in base_classes:
|
||||
metadata = getattr(base, "metadata", None)
|
||||
if metadata is None:
|
||||
continue
|
||||
await conn.run_sync(metadata.create_all)
|
||||
self.logger.info(f"local sqlite ready: {db_path}")
|
||||
|
||||
# ─── 对外通道 ──────────────────────────────────────────────
|
||||
|
||||
@@ -326,29 +372,62 @@ class BaseOrganization:
|
||||
全局工具白名单(``python_executor`` 等)也合并进来,给 agent 兜底。
|
||||
"""
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
|
||||
toolset_dir = Path(self.plugin_dir) / "toolset"
|
||||
if toolset_dir.exists() and (toolset_dir / "manifest.json").exists():
|
||||
with open(toolset_dir / "manifest.json", "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# 跟 loader._import_entry_class 共用一条虚拟 package 链:
|
||||
# ``_kilostar_plugin_<name>`` → ``.toolset``,让 ``from ._s3_common import ...``
|
||||
# 这种相对导入能正常解析。
|
||||
root_pkg = f"_kilostar_plugin_{self.name}"
|
||||
tool_pkg = f"{root_pkg}.toolset"
|
||||
if root_pkg not in sys.modules:
|
||||
root_mod = types.ModuleType(root_pkg)
|
||||
root_mod.__path__ = [str(Path(self.plugin_dir))]
|
||||
sys.modules[root_pkg] = root_mod
|
||||
if tool_pkg not in sys.modules:
|
||||
pkg = types.ModuleType(tool_pkg)
|
||||
pkg.__path__ = [str(toolset_dir)]
|
||||
sys.modules[tool_pkg] = pkg
|
||||
|
||||
# 第一遍:把 toolset 目录下所有 .py 都按文件名注册成子模块,
|
||||
# 让共享辅助模块(如 ``_s3_common``)先就位。
|
||||
for py_path in sorted(toolset_dir.glob("*.py")):
|
||||
if py_path.name == "__init__.py":
|
||||
continue
|
||||
sub_name = f"{tool_pkg}.{py_path.stem}"
|
||||
if sub_name in sys.modules:
|
||||
continue
|
||||
spec = importlib.util.spec_from_file_location(sub_name, str(py_path))
|
||||
if spec is None or spec.loader is None:
|
||||
continue
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
mod.__package__ = tool_pkg
|
||||
sys.modules[sub_name] = mod
|
||||
try:
|
||||
spec.loader.exec_module(mod)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"failed to load tool module {py_path.name}: {e}")
|
||||
sys.modules.pop(sub_name, None)
|
||||
|
||||
# 第二遍:按 manifest 列表挑出工具函数
|
||||
for tool_def in manifest.get("tools", []):
|
||||
tname = tool_def.get("name")
|
||||
tfile = tool_def.get("file", f"{tname}.py")
|
||||
if not tname:
|
||||
continue
|
||||
fpath = toolset_dir / tfile
|
||||
if not fpath.exists():
|
||||
self.logger.warning(f"tool file not found: {fpath}")
|
||||
stem = Path(tfile).stem
|
||||
sub_name = f"{tool_pkg}.{stem}"
|
||||
mod = sys.modules.get(sub_name)
|
||||
if mod is None:
|
||||
self.logger.warning(f"tool module not loaded: {tfile}")
|
||||
continue
|
||||
module_name = f"data.plugin.{self.name}.toolset.{tname}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(fpath))
|
||||
if spec is None or spec.loader is None:
|
||||
continue
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
func = getattr(mod, tname, None)
|
||||
if callable(func):
|
||||
self._tools_by_name[tname] = func
|
||||
@@ -373,6 +452,11 @@ class BaseOrganization:
|
||||
每个 agent 注入:
|
||||
- 自己声明的 tools(从 ``_tools_by_name`` 取)
|
||||
- 一个特殊 ``consult`` 工具(如果 peers 非空),用于跨 agent 协作
|
||||
|
||||
provider+model 的来源:
|
||||
1. agents.json 里若已写死 ``model`` → 直接用(兼容老插件)
|
||||
2. 否则按 ``(plugin_name, slot_name)`` 查 DB,拿用户在 Agent 设置页配置的
|
||||
provider+model;查不到则跳过该 slot(日志 warning,让用户先去配置)
|
||||
"""
|
||||
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
|
||||
@@ -381,10 +465,16 @@ class BaseOrganization:
|
||||
factory = AgentFactory()
|
||||
|
||||
for adef in self.agents_config.agents:
|
||||
provider = snapshot.providers.get(adef.model.provider_title)
|
||||
provider_title, model_id = await self._resolve_slot_model(adef)
|
||||
if not provider_title or not model_id:
|
||||
self.logger.warning(
|
||||
f"agent slot {adef.name!r}: provider/model 未配置(请在 Agent 设置页装配)"
|
||||
)
|
||||
continue
|
||||
provider = snapshot.providers.get(provider_title)
|
||||
if provider is None:
|
||||
self.logger.warning(
|
||||
f"provider {adef.model.provider_title!r} not found; agent {adef.name} skipped"
|
||||
f"provider {provider_title!r} not found; agent {adef.name} skipped"
|
||||
)
|
||||
continue
|
||||
tools = [
|
||||
@@ -399,7 +489,7 @@ class BaseOrganization:
|
||||
try:
|
||||
agent = factory.create_agent(
|
||||
provider=provider,
|
||||
model_id=adef.model.model_id,
|
||||
model_id=model_id,
|
||||
output_type=str,
|
||||
system_prompt=adef.system_prompt or f"You are {adef.role}.",
|
||||
deps_type=type(None),
|
||||
@@ -411,6 +501,26 @@ class BaseOrganization:
|
||||
except Exception as e:
|
||||
self.logger.warning(f"build agent {adef.name} failed: {e}")
|
||||
|
||||
async def _resolve_slot_model(self, adef: AgentDef) -> tuple[str, str]:
|
||||
"""决定 slot 用哪个 provider+model。
|
||||
|
||||
优先静态绑定(向后兼容老插件),否则查 DB 中用户为该 slot 配置的值。
|
||||
DB 不可用时返回空——构建侧据此跳过该 slot。
|
||||
"""
|
||||
if adef.model and adef.model.provider_title and adef.model.model_id:
|
||||
return adef.model.provider_title, adef.model.model_id
|
||||
try:
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
pg = ray_actor_hook("postgres_database").postgres_database
|
||||
row = await pg.find_plugin_slot.remote(self.name, adef.name)
|
||||
if row is None:
|
||||
return "", ""
|
||||
return getattr(row, "provider_title", "") or "", getattr(row, "model_id", "") or ""
|
||||
except Exception as e:
|
||||
self.logger.debug(f"slot model lookup failed (DB?): {e}")
|
||||
return "", ""
|
||||
|
||||
def _make_consult_tool(self, adef: AgentDef):
|
||||
"""为 agent 生成一个 ``consult(peer, question)`` 工具。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user