Ctrl K
ComunidadGuías…

scout/plugins/spi.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""插件 SPI(服务提供接口)— 对标 DeepSeek Harness「一切皆插件」.

此前 scout 的插件机制只能「附加」(事件钩子),无法「替换」核心组件。
本模块提供轻量 SPI 注册表:插件可声明 provides 并注册 LLM / 存储 / 缓存 /
会话 / 记忆等核心组件的替代实现;应用侧通过 get_provider() 获取,插件实现优先,
默认实现兜底。

使用方式(插件侧):
    class MyLLMPlugin(Plugin):
        provides = [SPI_KIND_LLM]

        async def provide(self, kind: str):
            if kind == SPI_KIND_LLM:
                return MyLLMClient(**self.config)

应用侧接入(2026-08-27 全类型落地):
    # LLMClientFactory.create(provider="spi")        → 插件注册的 LLM 实现
    # get_storage_backend(backend="spi")             → 插件注册的存储实现
    # get_cache_backend(backend="spi")               → 插件注册的缓存实现(SCOUT_CACHE_BACKEND=spi)
    # get_session_store(backend="spi")               → 插件注册的会话实现(SCOUT_SESSION_STORE=spi)
    # get_memory_store(backend="spi")                → 插件注册的记忆实现(SCOUT_MEMORY_STORE=spi)
    以上 backend="spi" 在插件未注册时都会明确报错(不静默回退)。
"""

from __future__ import annotations

import logging
from typing import Any

logger = logging.getLogger("scout.plugins.spi")

# ── SPI 类型常量 ─────────────────────────────────────────
SPI_KIND_LLM = "llm"            # LLM 客户端(实现 scout.llm.base.LLMClient 接口)
SPI_KIND_STORAGE = "storage"    # 关系存储(实现 scout.storage.base.StorageBackend)
SPI_KIND_CACHE = "cache"        # KV 缓存(实现 scout.storage.base.CacheBackend)
SPI_KIND_SESSION = "session"    # 会话存取
SPI_KIND_MEMORY = "memory"      # 长期记忆

ALL_KINDS = (SPI_KIND_LLM, SPI_KIND_STORAGE, SPI_KIND_CACHE, SPI_KIND_SESSION, SPI_KIND_MEMORY)


class SPIRegistry:
    """进程内 SPI 注册表(全局单例)."""

    _providers: dict[str, Any] = {}
    _source: dict[str, str] = {}  # kind → 提供者插件名(可观测性)

    @classmethod
    def register(cls, kind: str, impl: Any, source: str = "plugin") -> None:
        if kind not in ALL_KINDS:
            logger.warning("未知 SPI 类型 %s,仍将注册(类型需与应用侧约定一致)", kind)
        cls._providers[kind] = impl
        cls._source[kind] = source
        logger.info("SPI 已注册: %s <- %s", kind, source)

    @classmethod
    def get(cls, kind: str) -> Any | None:
        return cls._providers.get(kind)

    @classmethod
    def unregister(cls, kind: str) -> None:
        cls._providers.pop(kind, None)
        cls._source.pop(kind, None)

    @classmethod
    def source(cls, kind: str) -> str:
        return cls._source.get(kind, "")

    @classmethod
    def clear(cls) -> None:
        cls._providers.clear()
        cls._source.clear()

    @classmethod
    def all(cls) -> dict[str, Any]:
        return dict(cls._providers)


def register_provider(kind: str, impl: Any, source: str = "plugin") -> None:
    """注册 SPI 实现(应用侧或插件侧通用入口)."""
    SPIRegistry.register(kind, impl, source)


def unregister_provider(kind: str) -> None:
    """注销 SPI 实现."""
    SPIRegistry.unregister(kind)


def get_provider(kind: str) -> Any | None:
    """获取 SPI 实现;未注册返回 None(调用方回退默认实现)."""
    return SPIRegistry.get(kind)


def has_provider(kind: str) -> bool:
    return kind in SPIRegistry._providers