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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ OPENAI_API_KEY=
# For OpenAI-compatible providers (OpenRouter, LiteLLM, vLLM, etc.):
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
# EMBEDDING_MODEL=openai/text-embedding-3-small

# --- MiniMax LLM provider (alternative to OpenAI for classification/summarization) ---
# MINIMAX_API_KEY= # MiniMax API key (get one at https://platform.minimaxi.com)
# LLM_PROVIDER=auto # auto|openai|minimax — auto tries OPENAI_API_KEY, then MINIMAX_API_KEY
# CLASSIFICATION_MODEL=MiniMax-M2.7 # Default model when using MiniMax (204K context)
AUTOMEM_API_TOKEN=
ADMIN_API_TOKEN=
ENABLE_GRAPH_VIEWER=true
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ EMBEDDING_PROVIDER=auto # auto|voyage|openai|ollama|local|placeholder
OPENAI_API_KEY= # For OpenAI or compatible provider (optional)
OPENAI_BASE_URL= # Custom endpoint for OpenAI-compatible APIs used by embeddings and classification/enrichment (optional)

# LLM provider for classification/summarization (separate from embedding)
LLM_PROVIDER=auto # auto|openai|minimax — auto tries OPENAI_API_KEY, then MINIMAX_API_KEY
MINIMAX_API_KEY= # MiniMax API key for classification/summarization (optional)

# Consolidation intervals (seconds)
CONSOLIDATION_DECAY_INTERVAL_SECONDS=86400 # 1 day (default)
CONSOLIDATION_DECAY_IMPORTANCE_THRESHOLD=0.3 # Only skip truly low-importance items
Expand Down
2 changes: 2 additions & 0 deletions INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ Admin operations additionally require `X-Admin-Token: <admin_token>` header.
| `VOYAGE_MODEL` | Voyage model (Voyage provider) | `voyage-4` |
| `OPENAI_API_KEY` | API key (OpenAI or compatible provider) | _unset_ |
| `OPENAI_BASE_URL` | Custom endpoint for OpenAI-compatible providers | _unset_ |
| `LLM_PROVIDER` | LLM provider for classification/summarization (`auto`, `openai`, `minimax`) | `auto` |
| `MINIMAX_API_KEY` | MiniMax API key (alternative to OpenAI for classification/summarization) | _unset_ |

👉 **New to Qdrant?** See the [Qdrant Setup Guide](docs/QDRANT_SETUP.md) for setup options (self-hosted on Railway or Qdrant Cloud).

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ Run benchmarks: `make bench-eval BENCH=locomo-mini CONFIG=baseline` (quick) or `
- `VOYAGE_API_KEY` / `VOYAGE_MODEL` - Voyage embeddings (if using `voyage`)
- `OPENAI_API_KEY` - OpenAI embeddings (if using `openai`)
- `OPENAI_BASE_URL` - Custom endpoint for OpenAI-compatible providers (OpenRouter, LiteLLM, vLLM, etc.)
- `MINIMAX_API_KEY` - [MiniMax](https://platform.minimaxi.com) API key for classification/summarization LLM
- `LLM_PROVIDER` - Choose `auto`, `openai`, or `minimax` for classification/summarization LLM (`auto` tries OpenAI then MiniMax)
- `OLLAMA_BASE_URL` / `OLLAMA_MODEL` - Ollama embeddings (if using `ollama`)
- `ADMIN_API_TOKEN` - Required for `/admin/reembed` and enrichment controls
- Consolidation tuning: `CONSOLIDATION_*_INTERVAL_SECONDS`
Expand Down
21 changes: 20 additions & 1 deletion automem/classification/memory_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import re
from typing import Any, Callable, Optional

# MiniMax model prefixes that require temperature clamping to (0, 1]
_MINIMAX_PREFIXES = ("MiniMax-", "minimax-", "abab")

# Regex to strip <think>…</think> blocks from model responses (used by MiniMax M2.5+)
_THINK_TAG_RE = re.compile(r"<think>.*?</think>\s*", re.DOTALL)


class MemoryClassifier:
"""Classifies memories into specific types based on content patterns."""
Expand Down Expand Up @@ -139,11 +145,17 @@ def _classify_with_llm(self, content: str) -> Optional[tuple[str, float]]:
uses_max_completion_tokens = self._classification_model.startswith(
self.MAX_COMPLETION_PREFIXES
)
is_minimax = self._classification_model.startswith(_MINIMAX_PREFIXES)

if uses_max_completion_tokens:
extra_params["max_completion_tokens"] = 50
else:
extra_params["max_tokens"] = 50
extra_params["temperature"] = 0.3
temperature = 0.3
# MiniMax requires temperature in (0, 1]
if is_minimax:
temperature = max(0.01, min(temperature, 1.0))
extra_params["temperature"] = temperature

response = client.chat.completions.create(
model=self._classification_model,
Expand All @@ -160,6 +172,13 @@ def _classify_with_llm(self, content: str) -> Optional[tuple[str, float]]:
self._logger.warning("LLM returned empty classification response")
return None

# Strip <think>…</think> blocks (MiniMax M2.5+ reasoning traces)
if is_minimax:
raw_content = _THINK_TAG_RE.sub("", raw_content).strip()
if not raw_content:
self._logger.warning("LLM response was only thinking tags")
return None

try:
result = json.loads(raw_content)
except (json.JSONDecodeError, TypeError):
Expand Down
9 changes: 9 additions & 0 deletions automem/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
CLASSIFICATION_MODEL = os.getenv("CLASSIFICATION_MODEL", "gpt-4o-mini")

# LLM provider for classification/summarization
# "auto" (default): Use OPENAI_API_KEY if set, else MINIMAX_API_KEY
# "openai": Use OpenAI explicitly
# "minimax": Use MiniMax explicitly (OpenAI-compatible API at api.minimax.io)
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "auto").strip().lower()
MINIMAX_API_KEY: str | None = os.getenv("MINIMAX_API_KEY")
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
MINIMAX_DEFAULT_MODEL = "MiniMax-M2.7"

RECALL_RELATION_LIMIT = int(os.getenv("RECALL_RELATION_LIMIT", "5"))
RECALL_EXPANSION_LIMIT = int(os.getenv("RECALL_EXPANSION_LIMIT", "25"))
RECALL_MIN_SCORE = float(os.getenv("RECALL_MIN_SCORE", "0.0"))
Expand Down
92 changes: 79 additions & 13 deletions automem/service_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Any, Callable

from automem.config import MINIMAX_BASE_URL, MINIMAX_DEFAULT_MODEL


def init_openai(
*,
Expand All @@ -10,29 +12,93 @@ def init_openai(
openai_cls: Any,
get_env_fn: Callable[[str], str | None],
) -> None:
"""Initialize OpenAI client for memory type classification (not embeddings)."""
"""Initialize OpenAI-compatible client for memory type classification (not embeddings).

Supports multiple LLM providers via the ``LLM_PROVIDER`` env var:
- ``auto`` (default): Use OPENAI_API_KEY if set, else MINIMAX_API_KEY
- ``openai``: Use OpenAI explicitly
- ``minimax``: Use MiniMax explicitly (OpenAI-compatible API at api.minimax.io)
"""
if state.openai_client is not None:
return

if openai_cls is None:
logger.info("OpenAI package not installed (used for memory type classification)")
return

llm_provider = (get_env_fn("LLM_PROVIDER") or "auto").strip().lower()

if llm_provider == "minimax":
api_key = get_env_fn("MINIMAX_API_KEY")
if not api_key:
logger.info("LLM_PROVIDER=minimax but MINIMAX_API_KEY not set")
return
try:
state.openai_client = openai_cls(api_key=api_key, base_url=MINIMAX_BASE_URL)
state.llm_provider = "minimax"
logger.info(
"MiniMax client initialized for memory type classification "
"(model: %s)",
get_env_fn("CLASSIFICATION_MODEL") or MINIMAX_DEFAULT_MODEL,
)
except Exception:
logger.exception("Failed to initialize MiniMax client")
state.openai_client = None
return

if llm_provider == "openai":
api_key = get_env_fn("OPENAI_API_KEY")
if not api_key:
logger.info("LLM_PROVIDER=openai but OPENAI_API_KEY not set")
return
try:
openai_base_url = (get_env_fn("OPENAI_BASE_URL") or "").strip() or None
client_kwargs: dict[str, Any] = {"api_key": api_key}
if openai_base_url:
client_kwargs["base_url"] = openai_base_url
state.openai_client = openai_cls(**client_kwargs)
state.llm_provider = "openai"
logger.info("OpenAI client initialized for memory type classification")
except Exception:
logger.exception("Failed to initialize OpenAI client")
state.openai_client = None
return

# Auto-detection: try OPENAI_API_KEY first, then MINIMAX_API_KEY
api_key = get_env_fn("OPENAI_API_KEY")
if not api_key:
logger.info("OpenAI API key not provided (used for memory type classification)")
if api_key:
try:
openai_base_url = (get_env_fn("OPENAI_BASE_URL") or "").strip() or None
client_kwargs = {"api_key": api_key}
if openai_base_url:
client_kwargs["base_url"] = openai_base_url
state.openai_client = openai_cls(**client_kwargs)
state.llm_provider = "openai"
logger.info("OpenAI client initialized for memory type classification")
except Exception:
logger.exception("Failed to initialize OpenAI client")
state.openai_client = None
return

minimax_key = get_env_fn("MINIMAX_API_KEY")
if minimax_key:
try:
state.openai_client = openai_cls(api_key=minimax_key, base_url=MINIMAX_BASE_URL)
state.llm_provider = "minimax"
logger.info(
"MiniMax client auto-detected for memory type classification "
"(model: %s)",
get_env_fn("CLASSIFICATION_MODEL") or MINIMAX_DEFAULT_MODEL,
)
except Exception:
logger.exception("Failed to initialize MiniMax client")
state.openai_client = None
return

try:
openai_base_url = (get_env_fn("OPENAI_BASE_URL") or "").strip() or None
client_kwargs = {"api_key": api_key}
if openai_base_url:
client_kwargs["base_url"] = openai_base_url
state.openai_client = openai_cls(**client_kwargs)
logger.info("OpenAI client initialized for memory type classification")
except Exception:
logger.exception("Failed to initialize OpenAI client")
state.openai_client = None
logger.info(
"No LLM API key provided for classification "
"(set OPENAI_API_KEY or MINIMAX_API_KEY)"
)


def get_memory_graph(*, state: Any, init_falkordb_fn: Callable[[], None]) -> Any:
Expand Down
17 changes: 16 additions & 1 deletion automem/utils/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

logger = logging.getLogger(__name__)

# MiniMax model prefixes that require temperature clamping to (0, 1]
_MINIMAX_PREFIXES = ("MiniMax-", "minimax-", "abab")

# Regex to strip <think>…</think> blocks from model responses (used by MiniMax M2.5+)
_THINK_TAG_RE = re.compile(r"<think>.*?</think>\s*", re.DOTALL)

# Common stopwords to exclude from search tokens
SEARCH_STOPWORDS = {
"the",
Expand Down Expand Up @@ -149,11 +155,16 @@ def summarize_content(

# Build model-specific params (o-series and gpt-5 don't support temperature)
extra_params: dict = {}
is_minimax = model.startswith(_MINIMAX_PREFIXES)
if model.startswith(("o", "gpt-5")):
extra_params["max_completion_tokens"] = token_limit
else:
extra_params["max_tokens"] = token_limit
extra_params["temperature"] = 0.3
temperature = 0.3
# MiniMax requires temperature in (0, 1]
if is_minimax:
temperature = max(0.01, min(temperature, 1.0))
extra_params["temperature"] = temperature

response = openai_client.chat.completions.create(
model=model,
Expand All @@ -166,6 +177,10 @@ def summarize_content(

summary = response.choices[0].message.content.strip()

# Strip <think>…</think> blocks (MiniMax M2.5+ reasoning traces)
if is_minimax and summary:
summary = _THINK_TAG_RE.sub("", summary).strip()

# Validate we actually got a shorter result
if summary and len(summary) < len(content):
logger.info(
Expand Down
22 changes: 21 additions & 1 deletion docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ Controls embedding provider and classification model settings.
| `EMBEDDING_MODEL` | OpenAI embedding model (used when provider is `openai`) | `text-embedding-3-small` | `text-embedding-3-small`, `text-embedding-3-large` |
| `VECTOR_SIZE` | Embedding dimension | `1024` | Must match embedding provider (1024=voyage-4, 1536=text-embedding-3-small native; choose ≤1536 when truncating, 3072=text-embedding-3-large native) |
| `VECTOR_SIZE_AUTODETECT` | Adopt existing collection dimension instead of failing on mismatch | `true` | `false` to enforce strict matching |
| `CLASSIFICATION_MODEL` | LLM for memory type classification | `gpt-4o-mini` | `gpt-4o-mini`, `gpt-4.1`, `gpt-5.1` |
| `CLASSIFICATION_MODEL` | LLM for memory type classification | `gpt-4o-mini` | `gpt-4o-mini`, `gpt-4.1`, `gpt-5.1`, `MiniMax-M2.7` |
| `LLM_PROVIDER` | LLM provider for classification/summarization | `auto` | `auto`, `openai`, `minimax` |
| `MINIMAX_API_KEY` | MiniMax API key (for MiniMax LLM provider) | _unset_ | Get one at [platform.minimaxi.com](https://platform.minimaxi.com) |

**Embedding Provider Comparison:**

Expand Down Expand Up @@ -272,6 +274,24 @@ VECTOR_SIZE=768
| `gpt-4o-mini` | $0.15/1M | $0.60/1M | **Default** - Good enough for classification |
| `gpt-4.1` | ~$2/1M | ~$8/1M | Better reasoning |
| `gpt-5.1` | $1.25/1M | $10/1M | Best reasoning, use for benchmarks |
| `MiniMax-M2.7` | ~$0.14/1M | ~$0.56/1M | MiniMax default, 204K context, OpenAI-compatible |
| `MiniMax-M2.7-highspeed` | ~$0.07/1M | ~$0.28/1M | Faster variant, 204K context |

**LLM Provider for Classification/Summarization:**

The `LLM_PROVIDER` env var controls which API backend is used for memory type
classification and auto-summarization (separate from the embedding provider):

- `auto` (default): Use `OPENAI_API_KEY` if set, otherwise fall back to `MINIMAX_API_KEY`
- `openai`: Use OpenAI explicitly (requires `OPENAI_API_KEY`)
- `minimax`: Use [MiniMax](https://platform.minimaxi.com) explicitly (requires `MINIMAX_API_KEY`)

MiniMax example:
```bash
LLM_PROVIDER=minimax
MINIMAX_API_KEY=eyJ...
CLASSIFICATION_MODEL=MiniMax-M2.7
```

**⚠️ Changing embedding models requires re-embedding all memories.** See [Re-embedding Guide](#re-embedding-memories) below.

Expand Down
Loading