Tools (18 total): - calculate: Safe AST-based math expression evaluator - fetch: Async HTTP with SSRF protection - files: Sandboxed read/write/list/delete - shell: Command execution with blocklist - search: Web search (SerpAPI, Google, Bing) - keyvalue: In-memory key-value store - librarian: exist-db XML database integration - convert: XML↔JSON conversion + XPath extraction Infrastructure: - CLI with run/init/check/version commands - Config loader for organism.yaml - Feature detection for optional dependencies - Optional extras in pyproject.toml LLM: - Fixed llm_connection.py to wrap working router WASM: - Documented WASM listener interface - Stub implementation for future work MCP: - Reddit sentiment MCP server example Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
"""
|
|
Optional feature detection for xml-pipeline.
|
|
|
|
This module checks which optional dependencies are installed and provides
|
|
graceful degradation when features are unavailable.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from importlib.util import find_spec
|
|
from typing import Callable
|
|
|
|
|
|
def _check_import(module: str) -> bool:
|
|
"""Check if a module can be imported."""
|
|
return find_spec(module) is not None
|
|
|
|
|
|
# Feature registry: feature_name -> (check_function, description)
|
|
FEATURES: dict[str, tuple[Callable[[], bool], str]] = {
|
|
"anthropic": (lambda: _check_import("anthropic"), "Anthropic Claude SDK"),
|
|
"openai": (lambda: _check_import("openai"), "OpenAI SDK"),
|
|
"redis": (lambda: _check_import("redis"), "Redis for distributed keyvalue"),
|
|
"search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"),
|
|
"auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"),
|
|
"server": (lambda: _check_import("websockets"), "WebSocket server"),
|
|
}
|
|
|
|
|
|
def get_available_features() -> dict[str, bool]:
|
|
"""Return dict of feature_name -> is_available."""
|
|
return {name: check() for name, (check, _) in FEATURES.items()}
|
|
|
|
|
|
def is_feature_available(feature: str) -> bool:
|
|
"""Check if a specific feature is available."""
|
|
if feature not in FEATURES:
|
|
return False
|
|
check, _ = FEATURES[feature]
|
|
return check()
|
|
|
|
|
|
def require_feature(feature: str) -> None:
|
|
"""Raise ImportError if feature is not available."""
|
|
if not is_feature_available(feature):
|
|
_, description = FEATURES.get(feature, (None, feature))
|
|
raise ImportError(
|
|
f"Feature '{feature}' is not installed. "
|
|
f"Install with: pip install xml-pipeline[{feature}]"
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class FeatureCheck:
|
|
"""Result of checking features against a config."""
|
|
|
|
available: dict[str, bool] = field(default_factory=dict)
|
|
missing: dict[str, str] = field(default_factory=dict) # feature -> reason needed
|
|
|
|
|
|
def check_features(config) -> FeatureCheck:
|
|
"""
|
|
Check which optional features are needed for a config.
|
|
|
|
Returns FeatureCheck with available features and missing ones needed by config.
|
|
"""
|
|
result = FeatureCheck(available=get_available_features())
|
|
|
|
# Check LLM backends
|
|
for backend in getattr(config, "llm_backends", []):
|
|
provider = getattr(backend, "provider", "").lower()
|
|
if provider == "anthropic" and not result.available.get("anthropic"):
|
|
result.missing["anthropic"] = f"LLM backend '{backend.name}' uses Anthropic"
|
|
if provider == "openai" and not result.available.get("openai"):
|
|
result.missing["openai"] = f"LLM backend '{backend.name}' uses OpenAI"
|
|
|
|
# Check tools
|
|
for listener in getattr(config, "listeners", []):
|
|
# If listener uses keyvalue tool and redis is configured
|
|
# This would need more sophisticated detection based on tool config
|
|
pass
|
|
|
|
# Check if auth is needed (multi-tenant mode)
|
|
if getattr(config, "auth", None):
|
|
if not result.available.get("auth"):
|
|
result.missing["auth"] = "Config has auth enabled"
|
|
|
|
# Check if websocket server is needed
|
|
if getattr(config, "server", None):
|
|
if not result.available.get("server"):
|
|
result.missing["server"] = "Config has server enabled"
|
|
|
|
return result
|
|
|
|
|
|
# Lazy import helpers for optional dependencies
|
|
def get_redis_client():
|
|
"""Get Redis client, or raise helpful error."""
|
|
require_feature("redis")
|
|
import redis
|
|
return redis
|
|
|
|
|
|
def get_anthropic_client():
|
|
"""Get Anthropic client, or raise helpful error."""
|
|
require_feature("anthropic")
|
|
import anthropic
|
|
return anthropic
|
|
|
|
|
|
def get_openai_client():
|
|
"""Get OpenAI client, or raise helpful error."""
|
|
require_feature("openai")
|
|
import openai
|
|
return openai
|