Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions apps/claude-code-plugin/scripts/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -209,26 +209,49 @@ if not base_url and not auth_token:
base_url = base_url.strip()

missing = []
llm_credentials_missing = False
if not provider:
missing.append("POWERMEM_INIT_LLM_PROVIDER")
llm_credentials_missing = True
if not model:
missing.append("POWERMEM_INIT_LLM_MODEL")
if provider not in {"ollama", "vllm"}:
if provider == "anthropic":
if not auth_token and not api_key:
llm_credentials_missing = True
missing.append(
"ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL, "
"ANTHROPIC_API_KEY, or POWERMEM_INIT_LLM_API_KEY"
)
if auth_token and not base_url:
llm_credentials_missing = True
missing.append("ANTHROPIC_BASE_URL or POWERMEM_INIT_LLM_BASE_URL")
elif not api_key:
llm_credentials_missing = True
missing.append("POWERMEM_INIT_LLM_API_KEY or LLM_API_KEY")

if missing:
print("Missing configuration: " + ", ".join(missing), file=sys.stderr)
print("Run init again with these environment variables set.", file=sys.stderr)
sys.exit(2)
if llm_credentials_missing:
print("No complete LLM configuration found: " + ", ".join(missing))
print(
"PowerMem will run in no-LLM mode. Basic memory add/search/update/delete "
"will work, while fact extraction, profile extraction, query rewrite, "
"compression, and graph extraction will be skipped."
)
provider = "noop"
provider_source = "fallback:no complete LLM config"
model = "noop"
model_source = "fallback:no complete LLM config"
api_key = ""
api_key_source = ""
auth_token = ""
auth_token_source = ""
base_url = ""
base_url_source = ""
else:
print("Missing configuration: " + ", ".join(missing), file=sys.stderr)
print("Run init again with these environment variables set.", file=sys.stderr)
sys.exit(2)

embedding_provider = env_first("POWERMEM_INIT_EMBEDDING_PROVIDER", "EMBEDDING_PROVIDER") or "default"
embedding_provider = embedding_provider.lower()
Expand Down Expand Up @@ -441,6 +464,10 @@ if provider == 'anthropic':
)
sys.exit(1)

if provider == 'noop':
print("LLM validation skipped: no-LLM mode is enabled.")
sys.exit(0)

if provider in {'ollama', 'vllm'} or not (api_key or auth_token):
sys.exit(0)

Expand Down
31 changes: 29 additions & 2 deletions src/powermem/agent/components/scope_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __init__(self, config: Dict[str, Any]):

# Extract llm config as dict (handle both ConfigObject and dict)
try:
llm_provider = config.llm.provider if hasattr(config, 'llm') else 'mock'
llm_provider = config.llm.provider if hasattr(config, 'llm') else 'noop'
llm_config_obj = config.llm.config if hasattr(config, 'llm') else {}

# Convert to dict if ConfigObject
Expand All @@ -49,7 +49,7 @@ def __init__(self, config: Dict[str, Any]):
llm_config = dict(llm_config_obj) if llm_config_obj else {}
except Exception as e:
logger.warning(f"Failed to extract LLM config: {e}")
llm_provider = 'mock'
llm_provider = 'noop'
llm_config = {}

self.llm = LLMFactory.create(llm_provider, llm_config)
Expand Down Expand Up @@ -163,6 +163,29 @@ def _initialize_scope_configs(self) -> None:
}
}

def _default_scope(self) -> MemoryScope:
default_scope = self.multi_agent_config.default_scope
if isinstance(default_scope, MemoryScope):
return default_scope

default_scope_str = str(default_scope).upper()
fallback_mapping = {
'PRIVATE': MemoryScope.PRIVATE,
'PUBLIC': MemoryScope.PUBLIC,
'AGENT_GROUP': MemoryScope.AGENT_GROUP,
'AGENT': MemoryScope.AGENT_GROUP,
'USER_GROUP': MemoryScope.USER_GROUP,
'USER': MemoryScope.USER_GROUP,
'RESTRICTED': MemoryScope.RESTRICTED,
}
if default_scope_str in fallback_mapping:
return fallback_mapping[default_scope_str]

try:
return MemoryScope(str(default_scope).lower())
except ValueError:
return MemoryScope.PRIVATE

def determine_scope(
self,
agent_id: str,
Expand All @@ -181,6 +204,10 @@ def determine_scope(
Determined memory scope
"""
try:
if getattr(self.llm, "is_noop", False) is True:
logger.debug("LLM is disabled; using default memory scope.")
return self._default_scope()

# Extract content from metadata if available
content = metadata.get('content', '') if metadata else ''

Expand Down
22 changes: 15 additions & 7 deletions src/powermem/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def _validate_loaded_config(config: Dict[str, Any], strict: bool) -> Dict[str, A
if llm_config:
provider = llm_config.get("provider", "")
inner_config = llm_config.get("config", {})
if provider not in ["mock", "ollama"]:
if provider not in ["mock", "noop", "ollama"]:
api_key = inner_config.get("api_key")
if not api_key:
(errors if strict else warnings).append(
Expand Down Expand Up @@ -356,7 +356,7 @@ def _run_connectivity_checks(config: Dict[str, Any]) -> List[str]:
return errors

llm_provider = (config.get("llm") or {}).get("provider", "")
if llm_provider and llm_provider not in ("mock", "ollama"):
if llm_provider and llm_provider not in ("mock", "noop", "ollama"):
try:
if hasattr(memory, "llm") and memory.llm:
messages = [{"role": "user", "content": "Say 'test' and nothing else."}]
Expand Down Expand Up @@ -507,7 +507,11 @@ def test_cmd(ctx: CLIContext, component, json_output):
print_info("Testing LLM connection...")
# Access the LLM through memory
memory = ctx.memory
if hasattr(memory, 'llm') and memory.llm:
if (
hasattr(memory, 'llm')
and memory.llm
and getattr(memory.llm, "is_noop", False) is not True
):
# Try a simple generation
messages = [{"role": "user", "content": "Say 'test' and nothing else."}]
llm = memory.llm
Expand All @@ -525,9 +529,9 @@ def test_cmd(ctx: CLIContext, component, json_output):
else:
results["llm"] = {
"status": "skipped",
"message": "LLM not configured or using mock provider"
"message": "LLM not configured or disabled"
}
print_warning("LLM: Skipped (not configured)")
print_warning("LLM: Skipped (not configured or disabled)")
except Exception as e:
results["llm"] = {
"status": "failed",
Expand Down Expand Up @@ -1353,7 +1357,11 @@ def init_cmd(ctx: CLIContext, env_file: Optional[str], dry_run: bool, test: bool
print_info("Testing LLM connection...")
try:
memory = test_ctx.memory
if hasattr(memory, "llm") and memory.llm:
if (
hasattr(memory, "llm")
and memory.llm
and getattr(memory.llm, "is_noop", False) is not True
):
messages = [{"role": "user", "content": "Say 'test' and nothing else."}]
llm = memory.llm
if hasattr(llm, "generate_response"):
Expand All @@ -1362,7 +1370,7 @@ def init_cmd(ctx: CLIContext, env_file: Optional[str], dry_run: bool, test: bool
llm.generate(messages=messages)
print_success("LLM: Connected")
else:
print_warning("LLM: Skipped (not configured)")
print_warning("LLM: Skipped (not configured or disabled)")
except Exception as e:
print_error(f"LLM: Failed - {e}")

Expand Down
8 changes: 7 additions & 1 deletion src/powermem/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from powermem.integrations.embeddings.config.providers import CustomEmbeddingConfig
from powermem.integrations.embeddings.config.sparse_base import BaseSparseEmbedderConfig
from powermem.integrations.llm.config.base import BaseLLMConfig
from powermem.integrations.llm.config.noop import NoopConfig # noqa: F401 - register noop provider
from powermem.settings import _DEFAULT_ENV_FILE, settings_config
from powermem.utils.utils import detect_system_timezone

Expand Down Expand Up @@ -219,7 +220,12 @@ def to_config(self) -> Dict[str, Any]:
# Determine model name
llm_model = self.model
if llm_model is None:
llm_model = "qwen-plus" if llm_provider == "qwen" else "gpt-4o-mini"
if llm_provider == "qwen":
llm_model = "qwen-plus"
elif llm_provider == "noop":
llm_model = "noop"
else:
llm_model = "gpt-4o-mini"

# 1. Get provider config class from registry
config_cls = (
Expand Down
1 change: 1 addition & 0 deletions src/powermem/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from powermem.integrations.embeddings.config.sparse_base import BaseSparseEmbedderConfig
import powermem.integrations.embeddings.config.sparse_providers # noqa: F401 — ensures sparse provider registry is populated
from powermem.integrations.llm.config.base import BaseLLMConfig
from powermem.integrations.llm.config.noop import NoopConfig # noqa: F401 - keeps noop provider registered
from powermem.integrations.llm.config.qwen import QwenConfig
from powermem.storage.config.base import BaseVectorStoreConfig, BaseGraphStoreConfig
from powermem.storage.config.sqlite import SQLiteConfig # noqa: F401 — keeps SQLite provider registered
Expand Down
23 changes: 21 additions & 2 deletions src/powermem/core/async_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ def _get_provider(self, component: str, default: str) -> str:
else:
return self.config.get(component, {}).get('provider', default)

def _is_llm_disabled(self) -> bool:
"""Return True when PowerMem is running without LLM-backed features."""
return self.llm_provider == "noop" or getattr(self.llm, "is_noop", False) is True

def _get_component_config(self, component: str) -> Dict[str, Any]:
"""
Helper method to get component configuration uniformly.
Expand Down Expand Up @@ -313,6 +317,10 @@ async def _extract_facts(self, messages: Any) -> List[str]:
Returns:
List of extracted facts
"""
if self._is_llm_disabled():
logger.info("LLM is disabled; skipping fact extraction.")
return []

try:
# Parse messages into conversation format
conversation = parse_messages_for_facts(messages)
Expand Down Expand Up @@ -382,6 +390,10 @@ async def _decide_memory_actions(
Returns:
List of memory action dictionaries
"""
if self._is_llm_disabled():
logger.info("LLM is disabled; skipping memory action planning.")
return []

try:
if not new_facts:
logger.debug("No new facts to process")
Expand Down Expand Up @@ -496,7 +508,10 @@ async def add(
agent_id = agent_id or self.agent_id

# Check if intelligent memory should be used
use_infer = infer and isinstance(messages, list) and len(messages) > 0
llm_disabled = self._is_llm_disabled()
use_infer = infer and isinstance(messages, list) and len(messages) > 0 and not llm_disabled
if infer and llm_disabled:
logger.info("LLM is disabled; falling back to simple add mode.")

# If not using intelligent memory, fall back to simple mode
if not use_infer:
Expand Down Expand Up @@ -877,6 +892,10 @@ async def _add_to_graph_async(
"""
if not self.enable_graph:
return None

if self._is_llm_disabled():
logger.info("LLM is disabled; skipping graph extraction.")
return None

# Extract content from messages for graph processing
if isinstance(messages, str):
Expand Down Expand Up @@ -1146,7 +1165,7 @@ async def search(
})

# Search in graph store
if self.enable_graph:
if self.enable_graph and not self._is_llm_disabled():
filters = {**(filters or {}), "user_id": user_id, "agent_id": agent_id, "run_id": run_id}
graph_results = await asyncio.to_thread(self.graph_store.search, query, filters, limit)
return {"results": transformed_results, "relations": graph_results}
Expand Down
23 changes: 21 additions & 2 deletions src/powermem/core/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,10 @@ def _get_provider(self, component: str, default: str) -> str:
provider = self.config.get(component, {}).get('provider')
return provider if provider is not None else default

def _is_llm_disabled(self) -> bool:
"""Return True when PowerMem is running without LLM-backed features."""
return self.llm_provider == "noop" or getattr(self.llm, "is_noop", False) is True

def _get_component_config(self, component: str) -> Dict[str, Any]:
"""
Helper method to get component configuration uniformly.
Expand Down Expand Up @@ -897,6 +901,10 @@ def _extract_facts(self, messages: Any) -> List[str]:
Returns:
List of extracted facts
"""
if self._is_llm_disabled():
logger.info("LLM is disabled; skipping fact extraction.")
return []

try:
# Parse messages into conversation format
conversation = parse_messages_for_facts(messages)
Expand Down Expand Up @@ -965,6 +973,10 @@ def _decide_memory_actions(
Returns:
List of memory action dictionaries
"""
if self._is_llm_disabled():
logger.info("LLM is disabled; skipping memory action planning.")
return []

try:
if not new_facts:
logger.debug("No new facts to process")
Expand Down Expand Up @@ -1110,7 +1122,10 @@ def add(
)

# Check if intelligent memory should be used
use_infer = infer and isinstance(messages, list) and len(messages) > 0
llm_disabled = self._is_llm_disabled()
use_infer = infer and isinstance(messages, list) and len(messages) > 0 and not llm_disabled
if infer and llm_disabled:
logger.info("LLM is disabled; falling back to simple add mode.")

# If not using intelligent memory, fall back to simple mode
if not use_infer:
Expand Down Expand Up @@ -1509,6 +1524,10 @@ def _add_to_graph(
"""
if not self.enable_graph:
return None

if self._is_llm_disabled():
logger.info("LLM is disabled; skipping graph extraction.")
return None

# Extract content from messages for graph processing
if isinstance(messages, str):
Expand Down Expand Up @@ -1844,7 +1863,7 @@ def search(
})

# Search in graph store
if self.enable_graph:
if self.enable_graph and not self._is_llm_disabled():
filters = {**(filters or {}), "user_id": user_id, "agent_id": agent_id, "run_id": run_id}
graph_results = self.graph_store.search(query, filters, limit)
return {"results": transformed_results, "relations": graph_results}
Expand Down
20 changes: 20 additions & 0 deletions src/powermem/integrations/llm/config/noop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Dict, Optional, Union

from pydantic import Field

from powermem.integrations.llm.config.base import BaseLLMConfig
from powermem.settings import settings_config


class NoopConfig(BaseLLMConfig):
"""Configuration for the disabled LLM provider."""

_provider_name = "noop"
_class_path = "powermem.integrations.llm.noop.NoopLLM"

model_config = settings_config("LLM_", extra="allow", env_file=None)

model: Optional[Union[str, Dict]] = Field(
default="noop",
description="Placeholder model name used when LLM features are disabled.",
)
1 change: 1 addition & 0 deletions src/powermem/integrations/llm/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from powermem.integrations.llm.config.deepseek import DeepSeekConfig
from powermem.integrations.llm.config.gemini import GeminiConfig
from powermem.integrations.llm.config.langchain import LangchainConfig
from powermem.integrations.llm.config.noop import NoopConfig
from powermem.integrations.llm.config.ollama import OllamaConfig
from powermem.integrations.llm.config.openai import OpenAIConfig
from powermem.integrations.llm.config.openai_structured import OpenAIStructuredConfig
Expand Down
Loading
Loading