xml-pipeline/xml_pipeline/config/features.py
dullfig e653d63bc1 Rename agentserver to xml_pipeline, add console example
OSS restructuring for open-core model:
- Rename package from agentserver/ to xml_pipeline/
- Update all imports (44 Python files, 31 docs/configs)
- Update pyproject.toml for OSS distribution (v0.3.0)
- Move prompt_toolkit from core to optional [console] extra
- Remove auth/server/lsp from core optional deps (-> Nextra)

New console example in examples/console/:
- Self-contained demo with handlers and config
- Uses prompt_toolkit (optional, falls back to input())
- No password auth, no TUI, no LSP — just the basics
- Shows how to use xml-pipeline as a library

Import changes:
- from agentserver.* -> from xml_pipeline.*
- CLI entry points updated: xml_pipeline.cli:main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 21:41:19 -08:00

115 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"),
"lsp": (lambda: _check_import("lsp_client"), "LSP client for config editor"),
}
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