* feat: Add Claude Code integration plugin Complete port of hindsight-openclaw (v0.4.19) adapted to Claude Code's hook-based plugin architecture. Pure Python stdlib, no external dependencies. - Auto-recall via UserPromptSubmit hook (additionalContext injection) - Auto-retain via async Stop hook (chunked retention with sliding window) - Daemon management (auto-start/stop hindsight-embed via uvx) - Dynamic bank IDs with per-agent/project/channel/user granularity - All 34 configuration options with env var overrides - File-based state persistence with fcntl locking - Graceful degradation on all error paths Works with Claude Code Channels (Telegram, Discord, Slack) and interactive sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Set correct chunked retention defaults (10/2, not 1/0) retainEveryNTurns=10 and retainOverlapTurns=2 are the production-tested values — every 10 turns, retain a 12-turn sliding window. The previous defaults (1/0) would retain every single turn with no overlap, defeating the chunked retention design that prevents API bombardment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Align recallBudget and daemonIdleTimeout with Openclaw defaults recallBudget: "low" → "mid" (Openclaw default) daemonIdleTimeout: 300 → 0 (Openclaw default, never auto-stop) As an official Hindsight integration, defaults should match Openclaw. Users can optimize locally via settings.json or env vars. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
145 lines
5.8 KiB
Python
145 lines
5.8 KiB
Python
"""LLM provider detection for Hindsight's fact extraction.
|
|
|
|
Port of: detectLLMConfig() in index.js
|
|
|
|
When running hindsight-embed locally (daemon mode), it needs an LLM to
|
|
extract facts from retained conversations. This module detects the LLM
|
|
config using the same priority chain as Openclaw:
|
|
|
|
1. HINDSIGHT_API_LLM_* environment variables (highest priority)
|
|
2. Plugin config (llmProvider, llmModel, llmApiKeyEnv)
|
|
3. Auto-detect from standard provider env vars
|
|
4. External API mode (server-side LLM, no local config needed)
|
|
"""
|
|
|
|
import os
|
|
|
|
# Provider detection table — same order as Openclaw
|
|
PROVIDER_DETECTION = [
|
|
{"name": "openai", "key_env": "OPENAI_API_KEY", "default_model": "gpt-4o-mini"},
|
|
{"name": "anthropic", "key_env": "ANTHROPIC_API_KEY", "default_model": "claude-3-5-haiku-20241022"},
|
|
{"name": "gemini", "key_env": "GEMINI_API_KEY", "default_model": "gemini-2.5-flash"},
|
|
{"name": "groq", "key_env": "GROQ_API_KEY", "default_model": "openai/gpt-oss-20b"},
|
|
{"name": "ollama", "key_env": "", "default_model": "llama3.2"},
|
|
{"name": "openai-codex", "key_env": "", "default_model": "gpt-5.2-codex"},
|
|
{"name": "claude-code", "key_env": "", "default_model": "claude-sonnet-4-5-20250929"},
|
|
]
|
|
|
|
# Providers that don't require an API key
|
|
NO_KEY_REQUIRED = {"ollama", "openai-codex", "claude-code"}
|
|
|
|
|
|
def _find_provider(name):
|
|
"""Find a provider entry by name."""
|
|
for p in PROVIDER_DETECTION:
|
|
if p["name"] == name:
|
|
return p
|
|
return None
|
|
|
|
|
|
def detect_llm_config(config: dict) -> dict:
|
|
"""Detect LLM configuration.
|
|
|
|
Returns dict with: provider, api_key, model, base_url, source.
|
|
Returns None values for external API mode (server handles LLM).
|
|
Raises RuntimeError if no configuration found and not in external API mode.
|
|
"""
|
|
override_provider = os.environ.get("HINDSIGHT_API_LLM_PROVIDER")
|
|
override_model = os.environ.get("HINDSIGHT_API_LLM_MODEL")
|
|
override_key = os.environ.get("HINDSIGHT_API_LLM_API_KEY")
|
|
override_base_url = os.environ.get("HINDSIGHT_API_LLM_BASE_URL")
|
|
|
|
# Priority 1: HINDSIGHT_API_LLM_PROVIDER env var
|
|
if override_provider:
|
|
if not override_key and override_provider not in NO_KEY_REQUIRED:
|
|
raise RuntimeError(
|
|
f'HINDSIGHT_API_LLM_PROVIDER is set to "{override_provider}" but HINDSIGHT_API_LLM_API_KEY is not set.'
|
|
)
|
|
pinfo = _find_provider(override_provider)
|
|
return {
|
|
"provider": override_provider,
|
|
"api_key": override_key or "",
|
|
"model": override_model or (pinfo["default_model"] if pinfo else None),
|
|
"base_url": override_base_url,
|
|
"source": "HINDSIGHT_API_LLM_PROVIDER override",
|
|
}
|
|
|
|
# Priority 2: Plugin config llmProvider/llmModel
|
|
cfg_provider = config.get("llmProvider")
|
|
if cfg_provider:
|
|
pinfo = _find_provider(cfg_provider)
|
|
api_key = ""
|
|
key_env_name = config.get("llmApiKeyEnv")
|
|
if key_env_name:
|
|
api_key = os.environ.get(key_env_name, "")
|
|
elif pinfo and pinfo["key_env"]:
|
|
api_key = os.environ.get(pinfo["key_env"], "")
|
|
|
|
if not api_key and cfg_provider not in NO_KEY_REQUIRED:
|
|
key_source = key_env_name or (pinfo["key_env"] if pinfo else "unknown")
|
|
raise RuntimeError(
|
|
f'Plugin config llmProvider is "{cfg_provider}" but no API key found. Expected env var: {key_source}'
|
|
)
|
|
return {
|
|
"provider": cfg_provider,
|
|
"api_key": api_key,
|
|
"model": config.get("llmModel") or override_model or (pinfo["default_model"] if pinfo else None),
|
|
"base_url": override_base_url,
|
|
"source": "plugin config",
|
|
}
|
|
|
|
# Priority 3: Auto-detect from standard provider env vars
|
|
for pinfo in PROVIDER_DETECTION:
|
|
if pinfo["name"] in NO_KEY_REQUIRED:
|
|
continue # Must be explicitly requested
|
|
if not pinfo["key_env"]:
|
|
continue
|
|
api_key = os.environ.get(pinfo["key_env"], "")
|
|
if api_key:
|
|
return {
|
|
"provider": pinfo["name"],
|
|
"api_key": api_key,
|
|
"model": override_model or pinfo["default_model"],
|
|
"base_url": override_base_url,
|
|
"source": f"auto-detected from {pinfo['key_env']}",
|
|
}
|
|
|
|
# Priority 4: External API mode — server handles LLM
|
|
if config.get("hindsightApiUrl"):
|
|
return {
|
|
"provider": None,
|
|
"api_key": None,
|
|
"model": None,
|
|
"base_url": None,
|
|
"source": "external-api-mode-no-llm",
|
|
}
|
|
|
|
raise RuntimeError(
|
|
"No LLM configuration found for Hindsight.\n\n"
|
|
"Option 1: Set a standard provider API key (auto-detect):\n"
|
|
" export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n"
|
|
" export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n\n"
|
|
"Option 2: Override with Hindsight-specific env vars:\n"
|
|
" export HINDSIGHT_API_LLM_PROVIDER=openai\n"
|
|
" export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n\n"
|
|
"Option 3: Use an external Hindsight API (server-side LLM):\n"
|
|
" Set hindsightApiUrl in settings.json or HINDSIGHT_API_URL env var"
|
|
)
|
|
|
|
|
|
def get_llm_env_vars(llm_config: dict) -> dict:
|
|
"""Build environment variables for hindsight-embed daemon from LLM config.
|
|
|
|
These are passed to the daemon subprocess so it knows which LLM to use
|
|
for fact extraction.
|
|
"""
|
|
env = {}
|
|
if llm_config.get("provider"):
|
|
env["HINDSIGHT_API_LLM_PROVIDER"] = llm_config["provider"]
|
|
if llm_config.get("api_key"):
|
|
env["HINDSIGHT_API_LLM_API_KEY"] = llm_config["api_key"]
|
|
if llm_config.get("model"):
|
|
env["HINDSIGHT_API_LLM_MODEL"] = llm_config["model"]
|
|
if llm_config.get("base_url"):
|
|
env["HINDSIGHT_API_LLM_BASE_URL"] = llm_config["base_url"]
|
|
return env
|