fleet-memory/hindsight-api-slim/hindsight_api/engine/providers/none_llm.py
Nicolò Boschi 9e5a066d26
feat: add 'none' LLM provider for chunk-only storage mode (#691)
Adds a proper 'none' provider option so users can run Hindsight as a
chunk store with semantic search but without any LLM dependency, replacing
the hacky workaround of setting provider to 'mock'.

When HINDSIGHT_API_LLM_PROVIDER=none:
- Retain automatically uses chunks mode (no fact extraction)
- Recall works normally (semantic search, BM25, graph retrieval)
- Reflect returns HTTP 400 with clear error message
- Consolidation/observations are disabled
- Mental model refresh returns HTTP 400
- No API key required
2026-03-25 18:01:20 +01:00

78 lines
2.7 KiB
Python

"""
No-op LLM provider for chunk-only storage mode.
When the LLM provider is set to "none", the system operates without any LLM dependency.
Retain uses chunks mode (no fact extraction), and reflect/consolidation are disabled.
This provider acts as a safety net — if any code path unexpectedly tries to call the LLM,
it raises a clear error instead of a confusing connection failure.
"""
import logging
from typing import Any
from ..llm_interface import LLMInterface
from ..response_models import LLMToolCallResult
logger = logging.getLogger(__name__)
class LLMNotAvailableError(Exception):
"""Raised when an operation requires an LLM but the provider is set to 'none'."""
pass
class NoneLLM(LLMInterface):
"""
No-op LLM provider that rejects all LLM calls.
Used when HINDSIGHT_API_LLM_PROVIDER=none to run Hindsight as a chunk store
with semantic search but without LLM-based features (fact extraction, reflect,
consolidation).
"""
async def verify_connection(self) -> None:
"""No-op — no LLM connection to verify."""
logger.debug("NoneLLM: no LLM connection to verify (provider=none)")
async def call(
self,
messages: list[dict[str, str]],
response_format: Any | None = None,
max_completion_tokens: int | None = None,
temperature: float | None = None,
scope: str = "memory",
max_retries: int = 10,
initial_backoff: float = 1.0,
max_backoff: float = 60.0,
skip_validation: bool = False,
strict_schema: bool = False,
return_usage: bool = False,
) -> Any:
"""Raise LLMNotAvailableError — no LLM is configured."""
raise LLMNotAvailableError(
"LLM provider is set to 'none'. This operation requires an LLM. "
"Set HINDSIGHT_API_LLM_PROVIDER to a real provider (e.g., openai, anthropic, gemini)."
)
async def call_with_tools(
self,
messages: list[dict[str, Any]],
tools: list[dict[str, Any]],
max_completion_tokens: int | None = None,
temperature: float | None = None,
scope: str = "tools",
max_retries: int = 5,
initial_backoff: float = 1.0,
max_backoff: float = 30.0,
tool_choice: str | dict[str, Any] = "auto",
) -> LLMToolCallResult:
"""Raise LLMNotAvailableError — no LLM is configured."""
raise LLMNotAvailableError(
"LLM provider is set to 'none'. This operation requires an LLM. "
"Set HINDSIGHT_API_LLM_PROVIDER to a real provider (e.g., openai, anthropic, gemini)."
)
async def cleanup(self) -> None:
"""No-op — nothing to clean up."""
pass