refactor(memory/mem0): table-driven tool dispatch, shared _oss_hint/_truthy/_add helpers, packed provider tables

This commit is contained in:
Teknium
2026-09-02 21:05:29 -07:00
parent 113f04616b
commit 968cbef157
4 changed files with 106 additions and 221 deletions
+86 -159
View File
@@ -5,8 +5,7 @@ Platform API (cloud), a self-hosted Mem0 server (MEM0_HOST, HTTP), or OSS Memory
Secrets live in $HERMES_HOME/.env (MEM0_API_KEY, MEM0_HOST); behavioral settings
in $HERMES_HOME/mem0.json via `hermes memory setup`: mode ("platform"|"oss"), host,
user_id (canonical id across every gateway so one human gets one merged store;
unset → gateway-native id), agent_id. MEM0_MODE/MEM0_USER_ID/MEM0_AGENT_ID env
vars remain a backward-compatible fallback.
unset → gateway-native id), agent_id. MEM0_* env vars remain a fallback.
"""
from __future__ import annotations
@@ -41,6 +40,13 @@ def _is_client_error(exc: Exception) -> bool:
return type(exc).__name__ in _CLIENT_ERROR_TYPES or any(s in err_str for s in ("404", "not found", "valid uuid"))
def _truthy(value: Any, falsy_strings: tuple[str, ...] | None = None) -> bool:
"""Coerce a flag; strings match ``falsy_strings`` (deny-list) when given, else a truthy allow-list."""
if not isinstance(value, str):
return bool(value)
return value.lower() not in falsy_strings if falsy_strings else value.lower() in ("true", "1", "yes")
def _read_mem0_json(config_path: Path) -> dict:
"""Best-effort read of mem0.json; missing/corrupt file -> {}."""
if config_path.exists():
@@ -56,15 +62,8 @@ def _load_config() -> dict:
Layering avoids a silent failure when the JSON file exists but lacks fields
like ``api_key`` that the user set in ``.env``."""
from hermes_constants import get_hermes_home
config = {
"mode": os.environ.get("MEM0_MODE", "platform"),
"api_key": get_secret("MEM0_API_KEY", ""),
"host": os.environ.get("MEM0_HOST", ""),
"agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"),
"oss": {},
}
# Only carry user_id when explicitly configured so initialize() can fall
# back to the gateway-native id.
config = {"mode": os.environ.get("MEM0_MODE", "platform"), "api_key": get_secret("MEM0_API_KEY", ""), "host": os.environ.get("MEM0_HOST", ""), "agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"), "oss": {}}
# Only carry user_id when explicitly configured so initialize() can fall back to the gateway-native id.
if os.environ.get("MEM0_USER_ID"):
config["user_id"] = os.environ["MEM0_USER_ID"]
file_cfg = _read_mem0_json(get_hermes_home() / "mem0.json")
@@ -76,59 +75,42 @@ def _load_config() -> dict:
# Tool schemas
# ---------------------------------------------------------------------------
def _schema(name: str, description: str, properties: dict, required: list[str]) -> dict:
return {"name": name, "description": description, "parameters": {"type": "object", "properties": properties, "required": required}}
def _param(type_: str, description: str) -> dict:
return {"type": type_, "description": description}
def _schema(name: str, description: str, properties: dict[str, tuple[str, str]], required: list[str]) -> dict:
props = {k: {"type": t, "description": d} for k, (t, d) in properties.items()}
return {"name": name, "description": description, "parameters": {"type": "object", "properties": props, "required": required}}
SEARCH_SCHEMA = _schema(
"mem0_search",
"Search the user's memories by meaning; returns facts ranked by "
"relevance. Use this before answering any question that may depend on "
"what you know about the user (preferences, facts, history, people, "
"projects, past decisions). For multi-part or multi-hop questions, "
"call it several times — vary the wording and run follow-up searches "
"on what earlier results reveal; one search is rarely enough.",
{
"query": _param("string", "What to search for."),
"top_k": _param("integer", "Max results (default: 10, max: 50)."),
"rerank": _param("boolean", "Rerank results for relevance (default: false, platform mode only)."),
},
"Search the user's memories by meaning; returns facts ranked by relevance. Use this before answering any question that may depend on what you know about the user (preferences, facts, history, people, projects, past decisions). For multi-part or multi-hop questions, call it several times — vary the wording and run follow-up searches on what earlier results reveal; one search is rarely enough.",
{"query": ("string", "What to search for."), "top_k": ("integer", "Max results (default: 10, max: 50)."), "rerank": ("boolean", "Rerank results for relevance (default: false, platform mode only).")},
["query"],
)
ADD_SCHEMA = _schema(
"mem0_add",
"Store a durable fact about the user, verbatim (no LLM extraction). "
"Call this the moment the user states a lasting preference, correction, "
"decision, or personal detail worth recalling on future turns — don't "
"wait to be asked to remember. Skip transient chit-chat and facts you've "
"already stored.",
{"content": _param("string", "The fact to store.")},
"Store a durable fact about the user, verbatim (no LLM extraction). Call this the moment the user states a lasting preference, correction, decision, or personal detail worth recalling on future turns — don't wait to be asked to remember. Skip transient chit-chat and facts you've already stored.",
{"content": ("string", "The fact to store.")},
["content"],
)
UPDATE_SCHEMA = _schema(
"mem0_update",
"Replace the text of an existing memory by its ID (take the ID from a "
"mem0_search result). Use when a stored fact has changed "
"or was wrong — correct it in place instead of adding a duplicate.",
{"memory_id": _param("string", "Memory UUID to update."), "text": _param("string", "New text content.")},
"Replace the text of an existing memory by its ID (take the ID from a mem0_search result). Use when a stored fact has changed or was wrong — correct it in place instead of adding a duplicate.",
{"memory_id": ("string", "Memory UUID to update."), "text": ("string", "New text content.")},
["memory_id", "text"],
)
DELETE_SCHEMA = _schema(
"mem0_delete",
"Delete a memory by its ID (take the ID from a mem0_search "
"result). Use when a stored fact is obsolete or the user asks you to "
"forget it; prefer mem0_update if the fact merely changed.",
{"memory_id": _param("string", "Memory UUID to delete.")},
"Delete a memory by its ID (take the ID from a mem0_search result). Use when a stored fact is obsolete or the user asks you to forget it; prefer mem0_update if the fact merely changed.",
{"memory_id": ("string", "Memory UUID to delete.")},
["memory_id"],
)
_PROMPT_BODY = (
"You have persistent memory of this user from past conversations. You should call mem0_search before answering anything that could depend on prior context (the user's preferences, facts, history, people, projects, or earlier decisions) — do not rely on the chat window alone, and do not assume you have no memory.\n"
"For multi-part or multi-hop questions, run several searches with different wording/angles and follow-up searches on what the first results surface; one search is rarely enough. Keep searching until you have every fact the question needs before you answer.\n"
"Tools: mem0_search to find memories, mem0_add to store facts, mem0_update and mem0_delete to manage by ID."
)
# ---------------------------------------------------------------------------
# MemoryProvider implementation
@@ -157,8 +139,7 @@ class Mem0MemoryProvider(MemoryProvider):
cfg = _load_config()
if cfg.get("mode", "platform") == "oss":
return bool(cfg.get("oss", {}).get("vector_store"))
# Platform needs an api_key; self-hosted needs a host (api_key optional
# when the server runs with AUTH_DISABLED).
# Platform needs an api_key; self-hosted needs a host (api_key optional when the server runs with AUTH_DISABLED).
return bool(cfg.get("api_key") or cfg.get("host"))
def save_config(self, values, hermes_home):
@@ -183,9 +164,11 @@ class Mem0MemoryProvider(MemoryProvider):
from ._setup import post_setup
post_setup(hermes_home, config)
def _vs_provider(self, default: str) -> str:
"""Configured OSS vector-store provider name (for error hints)."""
return self._config.get("oss", {}).get("vector_store", {}).get("provider", default)
def _oss_hint(self, template: str, default: str = "vector store") -> str:
"""OSS-only hint; ``{vs}`` is the configured vector-store provider. "" in other modes."""
if self._mode != "oss":
return ""
return template.format(vs=self._config.get("oss", {}).get("vector_store", {}).get("provider", default))
def _create_backend(self):
# Lazy-install the mem0 SDK before either backend imports it. ensure() honors
@@ -222,10 +205,8 @@ class Mem0MemoryProvider(MemoryProvider):
def _format_error(self, prefix: str, exc: Exception) -> str:
msg = f"{prefix}: {exc}"
if self._mode == "oss":
err_str = str(exc).lower()
if "connection" in err_str or "refused" in err_str or "timeout" in err_str:
msg += f" (check that {self._vs_provider('vector store')} is running)"
if any(s in str(exc).lower() for s in ("connection", "refused", "timeout")):
msg += self._oss_hint(" (check that {vs} is running)")
return msg
def _record_success(self):
@@ -239,12 +220,8 @@ class Mem0MemoryProvider(MemoryProvider):
if tripped:
self._breaker_open_until = time.monotonic() + _BREAKER_COOLDOWN_SECS
if tripped:
hint = f" Check that your {self._vs_provider('unknown')} vector store is running and reachable." if self._mode == "oss" else ""
logger.warning(
"Mem0 circuit breaker tripped after %d consecutive failures. "
"Pausing API calls for %ds.%s",
count, _BREAKER_COOLDOWN_SECS, hint,
)
hint = self._oss_hint(" Check that your {vs} vector store is running and reachable.", "unknown")
logger.warning("Mem0 circuit breaker tripped after %d consecutive failures. Pausing API calls for %ds.%s", count, _BREAKER_COOLDOWN_SECS, hint)
def initialize(self, session_id: str, **kwargs) -> None:
self._config = _load_config()
@@ -259,43 +236,29 @@ class Mem0MemoryProvider(MemoryProvider):
self._agent_id = self._config.get("agent_id", "hermes")
# Persisted rerank preference: DEFAULT for mem0_search when the model doesn't
# pass ``rerank``; per-call args win. Platform-only; other backends ignore it.
_rr = self._config.get("rerank", False)
self._rerank_default = _rr.lower() in ("true", "1", "yes") if isinstance(_rr, str) else bool(_rr)
self._rerank_default = _truthy(self._config.get("rerank", False))
self._channel = kwargs.get("platform") or "cli"
self._backend = self._create_backend()
if self._backend and not self._atexit_registered:
atexit.register(self._shutdown_backend)
self._atexit_registered = True
def _read_filters(self) -> Dict[str, Any]:
def _search(self, query: str, top_k: int = 10, rerank: bool = False, backend=None) -> list:
# Scoped to user_id only — by design — so recall surfaces memories from any
# gateway/agent under this principal; writes attach agent_id and metadata.channel
# (dashboard per-channel filtering) so narrower views remain possible at query time.
return {"user_id": self._user_id}
return (backend or self._backend).search(query, filters={"user_id": self._user_id}, top_k=top_k, rerank=rerank)
def _write_metadata(self) -> Dict[str, Any]:
return {"channel": self._channel} if self._channel else {}
def _add(self, messages: list, infer: bool):
metadata = {"channel": self._channel} if self._channel else {}
return self._backend.add(messages, user_id=self._user_id, agent_id=self._agent_id, infer=infer, metadata=metadata)
def system_prompt_block(self) -> str:
# Mirror _create_backend precedence (oss > host > platform) so the label names
# the backend that actually runs. Rerank is a Mem0 Platform feature only.
mode_label = "OSS (self-hosted)" if self._mode == "oss" else "self-hosted (HTTP API)" if self._host else "platform (cloud API)"
rerank_note = " Rerank is available on search." if (self._mode == "platform" and not self._host) else ""
return (
"# Mem0 Memory\n"
f"Active. Mode: {mode_label}. User: {self._user_id}.\n"
"You have persistent memory of this user from past conversations. "
"You should call mem0_search before answering anything that could depend "
"on prior context (the user's preferences, facts, history, people, "
"projects, or earlier decisions) — do not rely on the chat window "
"alone, and do not assume you have no memory.\n"
"For multi-part or multi-hop questions, run several searches with "
"different wording/angles and follow-up searches on what the first "
"results surface; one search is rarely enough. Keep searching until "
"you have every fact the question needs before you answer.\n"
"Tools: mem0_search to find memories, mem0_add to store facts, "
f"mem0_update and mem0_delete to manage by ID.{rerank_note}"
)
return f"# Mem0 Memory\nActive. Mode: {mode_label}. User: {self._user_id}.\n{_PROMPT_BODY}{rerank_note}"
def on_turn_start(self, turn_number: int, message: str, **kwargs) -> None:
self._start_prefetch(message)
@@ -313,17 +276,14 @@ class Mem0MemoryProvider(MemoryProvider):
return
with self._prefetch_lock:
# Same query already answered or still in flight: don't restart it.
if self._prefetch_query == query and (
self._prefetch_done or (self._prefetch_thread and self._prefetch_thread.is_alive())
):
if self._prefetch_query == query and (self._prefetch_done or (self._prefetch_thread and self._prefetch_thread.is_alive())):
return
self._prefetch_query, self._prefetch_result, self._prefetch_done = query, "", False
def _run():
body = ""
try:
results = backend.search(query, filters=self._read_filters(), top_k=10, rerank=False)
lines = [r.get("memory", "") for r in (results or []) if r.get("memory")]
lines = [r.get("memory", "") for r in (self._search(query, backend=backend) or []) if r.get("memory")]
if lines:
body = "## Mem0 Memory\n" + "\n".join(f"- {l}" for l in lines)
self._record_success()
@@ -332,8 +292,7 @@ class Mem0MemoryProvider(MemoryProvider):
logger.debug("Mem0 prefetch failed: %s", e)
with self._prefetch_lock:
if self._prefetch_query == query:
self._prefetch_result = body
self._prefetch_done = True
self._prefetch_result, self._prefetch_done = body, True
t = threading.Thread(target=_run, daemon=True, name="mem0-prefetch")
with self._prefetch_lock:
@@ -359,12 +318,10 @@ class Mem0MemoryProvider(MemoryProvider):
return
def _sync():
backend = self._backend
if backend is None:
if self._backend is None:
return
try:
messages = [{"role": "user", "content": user_content}, {"role": "assistant", "content": assistant_content}]
backend.add(messages, user_id=self._user_id, agent_id=self._agent_id, infer=True, metadata=self._write_metadata())
self._add([{"role": "user", "content": user_content}, {"role": "assistant", "content": assistant_content}], infer=True)
self._record_success()
except Exception as e:
self._record_failure()
@@ -382,86 +339,56 @@ class Mem0MemoryProvider(MemoryProvider):
def get_tool_schemas(self) -> List[Dict[str, Any]]:
return [SEARCH_SCHEMA, ADD_SCHEMA, UPDATE_SCHEMA, DELETE_SCHEMA]
# -- tool handlers -------------------------------------------------------
# -- tool handlers: (required params, error label, body, client-error policy) ---
# Client errors (bad ID / not found) never trip the breaker, except for mem0_add
# where they count as failures; update/delete answer them with "Memory not found".
def _tool_search(self, args: dict) -> str:
query = args.get("query", "")
if not query:
return tool_error("Missing required parameter: query")
try:
top_k = max(1, min(int(args.get("top_k", 10)), 50))
rerank_raw = args.get("rerank", self._rerank_default)
rerank = rerank_raw.lower() not in ("false", "0", "no") if isinstance(rerank_raw, str) else bool(rerank_raw)
results = self._backend.search(query, filters=self._read_filters(), top_k=top_k, rerank=rerank)
self._record_success()
if not results:
return json.dumps({"result": "No relevant memories found."})
items = [{"id": r.get("id"), "memory": r.get("memory", ""), "score": r.get("score", 0)} for r in results]
return json.dumps({"results": items, "count": len(items)})
except Exception as e:
if not _is_client_error(e):
self._record_failure()
return tool_error(self._format_error("Search failed", e))
top_k = max(1, min(int(args.get("top_k", 10)), 50))
rerank = _truthy(args.get("rerank", self._rerank_default), falsy_strings=("false", "0", "no"))
results = self._search(args["query"], top_k, rerank)
if not results:
return json.dumps({"result": "No relevant memories found."})
items = [{"id": r.get("id"), "memory": r.get("memory", ""), "score": r.get("score", 0)} for r in results]
return json.dumps({"results": items, "count": len(items)})
def _tool_add(self, args: dict) -> str:
content = args.get("content", "")
if not content:
return tool_error("Missing required parameter: content")
try:
result = self._backend.add(
[{"role": "user", "content": content}],
user_id=self._user_id, agent_id=self._agent_id, infer=False, metadata=self._write_metadata(),
)
self._record_success()
event_id = result.get("event_id") if isinstance(result, dict) else None
# Cloud add is async (server-side extraction); OSS and self-hosted store synchronously.
msg = "Fact stored." if (self._mode == "oss" or self._host) else "Fact queued for storage."
return json.dumps({"result": msg, "event_id": event_id})
except Exception as e:
self._record_failure()
return tool_error(self._format_error("Failed to store", e))
def _tool_by_id(self, args: dict, required: tuple[str, ...], label: str, method: str) -> str:
"""Shared update/delete shape: required-param check, then backend.<method>(*values)."""
values = [args.get(k, "") for k in required]
for k, v in zip(required, values):
if not v:
return tool_error(f"Missing required parameter: {k}")
try:
result = getattr(self._backend, method)(*values)
self._record_success()
return json.dumps(result)
except Exception as e:
if _is_client_error(e):
return tool_error(f"Memory not found: {values[0]}")
self._record_failure()
return tool_error(self._format_error(label, e))
def _tool_update(self, args: dict) -> str:
return self._tool_by_id(args, ("memory_id", "text"), "Update failed", "update")
def _tool_delete(self, args: dict) -> str:
return self._tool_by_id(args, ("memory_id",), "Delete failed", "delete")
result = self._add([{"role": "user", "content": args["content"]}], infer=False)
event_id = result.get("event_id") if isinstance(result, dict) else None
# Cloud add is async (server-side extraction); OSS and self-hosted store synchronously.
msg = "Fact stored." if (self._mode == "oss" or self._host) else "Fact queued for storage."
return json.dumps({"result": msg, "event_id": event_id})
_TOOL_HANDLERS = {
"mem0_search": _tool_search,
"mem0_add": _tool_add,
"mem0_update": _tool_update,
"mem0_delete": _tool_delete,
"mem0_search": (("query",), "Search failed", _tool_search, "skip"),
"mem0_add": (("content",), "Failed to store", _tool_add, "count"),
"mem0_update": (("memory_id", "text"), "Update failed", lambda self, a: json.dumps(self._backend.update(a["memory_id"], a["text"])), "not_found"),
"mem0_delete": (("memory_id",), "Delete failed", lambda self, a: json.dumps(self._backend.delete(a["memory_id"])), "not_found"),
}
def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
if self._backend is None:
err = getattr(self, "_init_error", "unknown error")
hint = f" Check that {self._vs_provider('vector store')} is running and reachable." if self._mode == "oss" else ""
return json.dumps({"error": f"Mem0 backend not initialized: {err}.{hint}"})
return json.dumps({"error": f"Mem0 backend not initialized: {err}.{self._oss_hint(' Check that {vs} is running and reachable.')}"})
if self._is_breaker_open():
hint = f" Check that your {self._vs_provider('vector store')} is running." if self._mode == "oss" else ""
return json.dumps({"error": f"Mem0 temporarily unavailable (multiple consecutive failures). Will retry automatically.{hint}"})
handler = self._TOOL_HANDLERS.get(tool_name)
if handler is None:
return json.dumps({"error": f"Mem0 temporarily unavailable (multiple consecutive failures). Will retry automatically.{self._oss_hint(' Check that your {vs} is running.')}"})
if tool_name not in self._TOOL_HANDLERS:
return tool_error(f"Unknown tool: {tool_name}")
return handler(self, args)
required, label, body, on_client_error = self._TOOL_HANDLERS[tool_name]
for k in required:
if not args.get(k, ""):
return tool_error(f"Missing required parameter: {k}")
try:
result = body(self, args)
self._record_success()
return result
except Exception as e:
client = _is_client_error(e)
if client and on_client_error == "not_found":
return tool_error(f"Memory not found: {args['memory_id']}")
if not client or on_client_error == "count":
self._record_failure()
return tool_error(self._format_error(label, e))
def _shutdown_backend(self):
try:
+6 -23
View File
@@ -81,16 +81,12 @@ class SelfHostedBackend(Mem0Backend):
def __init__(self, api_key: str, host: str, transport=None):
import httpx
headers = {"Content-Type": "application/json"}
if api_key:
headers["X-API-Key"] = api_key # omitted only for AUTH_DISABLED servers
# Connect-level retries keep a single dropped SYN from counting toward the
# provider failure breaker. ``transport`` is injectable for tests.
self._client = httpx.Client(
base_url=host.rstrip("/"), headers=headers, timeout=30.0,
transport=transport or httpx.HTTPTransport(retries=2),
)
self._client = httpx.Client(base_url=host.rstrip("/"), headers=headers, timeout=30.0, transport=transport or httpx.HTTPTransport(retries=2))
def _json(self, method: str, path: str, **kwargs) -> Any:
resp = self._client.request(method, path, **kwargs)
@@ -105,8 +101,7 @@ class SelfHostedBackend(Mem0Backend):
return _unwrap_results(self._json("POST", "/search", json=body))
def add(self, messages: list, *, user_id: str, agent_id: str, infer: bool = False, metadata: dict | None = None) -> dict:
body: dict[str, Any] = {"messages": messages, **_add_kwargs(user_id, agent_id, infer, metadata)}
return self._json("POST", "/memories", json=body)
return self._json("POST", "/memories", json={"messages": messages, **_add_kwargs(user_id, agent_id, infer, metadata)})
def _update(self, memory_id: str, text: str) -> None:
self._json("PUT", f"/memories/{memory_id}", json={"text": text})
@@ -133,10 +128,7 @@ def _register_direct_openai_provider() -> None:
provider_map = getattr(LlmFactory, "provider_to_class", None)
register_provider = getattr(LlmFactory, "register_provider", None)
if not isinstance(provider_map, dict) or not callable(register_provider):
raise RuntimeError(
"mem0 LlmFactory does not support the provider registration required "
"for the Hermes OpenAI OSS backend"
)
raise RuntimeError("mem0 LlmFactory does not support the provider registration required for the Hermes OpenAI OSS backend")
if provider_map.get(_DIRECT_OPENAI_PROVIDER) != (_DIRECT_OPENAI_CLASS_PATH, OpenAIConfig):
register_provider(_DIRECT_OPENAI_PROVIDER, _DIRECT_OPENAI_CLASS_PATH, OpenAIConfig)
@@ -166,7 +158,6 @@ class OSSBackend(Mem0Backend):
vs_config = dict(vector_store.get("config", {}))
if "path" in vs_config:
vs_config["path"] = os.path.expanduser(vs_config["path"])
embedder_config = oss_config.get("embedder", {}).get("config", {})
dims = embedder_config.get("embedding_dims") or KNOWN_DIMS.get(embedder_config.get("model", ""))
if dims:
@@ -174,12 +165,7 @@ class OSSBackend(Mem0Backend):
self._recreate_collection_if_dims_changed(vector_store.get("provider", "qdrant"), vs_config, dims)
vector_store["config"] = vs_config
config = {
"vector_store": vector_store,
"llm": _provider_block("llm"),
"embedder": _provider_block("embedder"),
"version": "v1.1",
}
config = {"vector_store": vector_store, "llm": _provider_block("llm"), "embedder": _provider_block("embedder"), "version": "v1.1"}
if str(config["llm"].get("provider") or "").strip().lower() == "openai":
# mem0 validates LlmConfig.provider before its factory lookup: build the
# supported OpenAI config first, then swap the provider on the validated object.
@@ -189,10 +175,7 @@ class OSSBackend(Mem0Backend):
try:
memory_config.llm.provider = _DIRECT_OPENAI_PROVIDER
except (AttributeError, TypeError) as exc:
raise RuntimeError(
"mem0 MemoryConfig does not expose a mutable llm.provider "
"for the Hermes OpenAI OSS backend"
) from exc
raise RuntimeError("mem0 MemoryConfig does not expose a mutable llm.provider for the Hermes OpenAI OSS backend") from exc
self._memory = Memory(memory_config)
else:
self._memory = Memory.from_config(config)
@@ -215,7 +198,7 @@ class OSSBackend(Mem0Backend):
if not client.collection_exists(collection_name):
return
vectors = client.get_collection(collection_name).config.params.vectors
# Named-vector collections expose a dict; unnamed expose an object with .size.
# Named-vector collections expose a dict; unnamed expose an object with .size.
if isinstance(vectors, dict):
vectors = next(iter(vectors.values()), None)
current_dims = getattr(vectors, "size", None)
+8 -14
View File
@@ -11,6 +11,10 @@ from mem0.configs.llms.openai import OpenAIConfig
from mem0.llms.base import LLMBase
from mem0.llms.openai import OpenAILLM
# BaseLlmConfig fields copied into OpenAIConfig; the last two may be absent on older mem0.
_COPIED_FIELDS = ("model", "temperature", "api_key", "max_tokens", "top_p", "top_k", "enable_vision", "vision_details", "http_client_proxies")
_OPTIONAL_FIELDS = ("reasoning_effort", "is_reasoning_model")
class DirectOpenAILLM(OpenAILLM):
"""Use OpenAI credentials and requests regardless of router environment."""
@@ -21,15 +25,9 @@ class DirectOpenAILLM(OpenAILLM):
elif isinstance(config, dict):
config = OpenAIConfig(**config)
elif isinstance(config, BaseLlmConfig) and not isinstance(config, OpenAIConfig):
config = OpenAIConfig(
model=config.model, temperature=config.temperature, api_key=config.api_key,
max_tokens=config.max_tokens, top_p=config.top_p, top_k=config.top_k,
enable_vision=config.enable_vision, vision_details=config.vision_details,
reasoning_effort=getattr(config, "reasoning_effort", None),
http_client_proxies=config.http_client_proxies,
is_reasoning_model=getattr(config, "is_reasoning_model", None),
)
fields = {k: getattr(config, k) for k in _COPIED_FIELDS}
fields.update({k: getattr(config, k, None) for k in _OPTIONAL_FIELDS})
config = OpenAIConfig(**fields)
if not config.model:
config.model = "gpt-5-mini"
# Configs predating the setup marker: keep the default model reasoning-safe
@@ -39,17 +37,13 @@ class DirectOpenAILLM(OpenAILLM):
# Bypass OpenAILLM.__init__ (it picks OpenRouter when OPENROUTER_API_KEY is
# set); LLMBase still owns validation and supported-parameter filtering.
LLMBase.__init__(self, config)
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OpenAI API key is required for the Hermes Mem0 OSS provider")
from openai import OpenAI
self.client = OpenAI(api_key=api_key, base_url=self.config.openai_base_url or os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1")
def generate_response(
self, messages: List[Dict[str, str]], response_format=None,
tools: Optional[List[Dict]] = None, tool_choice: str = "auto", **kwargs,
):
def generate_response(self, messages: List[Dict[str, str]], response_format=None, tools: Optional[List[Dict]] = None, tool_choice: str = "auto", **kwargs):
params = self._get_supported_params(messages=messages, **kwargs)
params.update({"model": self.config.model, "messages": messages})
# No OpenRouter-only fields; ``store`` is opt-in so OpenAI-compatible
+6 -25
View File
@@ -6,33 +6,17 @@ import os
from typing import Any
LLM_PROVIDERS: dict[str, dict[str, Any]] = {
"openai": {
"label": "OpenAI", "needs_key": True, "env_var": "OPENAI_API_KEY",
"default_model": "gpt-5-mini", "base_url_key": "openai_base_url",
},
"ollama": {
"label": "Ollama (local)", "needs_key": False, "default_model": "llama3.1:8b",
"default_url": "http://localhost:11434", "base_url_key": "ollama_base_url", "pip_dep": "ollama",
},
"openai": {"label": "OpenAI", "needs_key": True, "env_var": "OPENAI_API_KEY", "default_model": "gpt-5-mini", "base_url_key": "openai_base_url"},
"ollama": {"label": "Ollama (local)", "needs_key": False, "default_model": "llama3.1:8b", "default_url": "http://localhost:11434", "base_url_key": "ollama_base_url", "pip_dep": "ollama"},
}
EMBEDDER_PROVIDERS: dict[str, dict[str, Any]] = {
"openai": {
"label": "OpenAI", "needs_key": True, "env_var": "OPENAI_API_KEY",
"default_model": "text-embedding-3-small", "base_url_key": "openai_base_url", "dims": 1536,
},
"ollama": {
"label": "Ollama (local)", "needs_key": False, "default_model": "nomic-embed-text",
"default_url": "http://localhost:11434", "base_url_key": "ollama_base_url", "dims": 768, "pip_dep": "ollama",
},
"openai": {"label": "OpenAI", "needs_key": True, "env_var": "OPENAI_API_KEY", "default_model": "text-embedding-3-small", "base_url_key": "openai_base_url", "dims": 1536},
"ollama": {"label": "Ollama (local)", "needs_key": False, "default_model": "nomic-embed-text", "default_url": "http://localhost:11434", "base_url_key": "ollama_base_url", "dims": 768, "pip_dep": "ollama"},
}
VECTOR_PROVIDERS: dict[str, dict[str, Any]] = {
"qdrant": {
"label": "Qdrant",
"default_config": {"path": os.path.expanduser("~/.hermes/mem0_qdrant")},
"pip_dep": "qdrant-client",
},
"qdrant": {"label": "Qdrant", "default_config": {"path": os.path.expanduser("~/.hermes/mem0_qdrant")}, "pip_dep": "qdrant-client"},
"pgvector": {
"label": "PGVector",
"default_config": {"host": "localhost", "port": 5432, "user": os.getenv("USER", "postgres"), "dbname": "postgres"},
@@ -40,9 +24,7 @@ VECTOR_PROVIDERS: dict[str, dict[str, Any]] = {
},
}
KNOWN_DIMS: dict[str, int] = {
"text-embedding-3-small": 1536, "text-embedding-3-large": 3072, "text-embedding-ada-002": 1536, "nomic-embed-text": 768,
}
KNOWN_DIMS: dict[str, int] = {"text-embedding-3-small": 1536, "text-embedding-3-large": 3072, "text-embedding-ada-002": 1536, "nomic-embed-text": 768}
SECTION_REGISTRIES = (("llm", LLM_PROVIDERS), ("embedder", EMBEDDER_PROVIDERS), ("vector_store", VECTOR_PROVIDERS))
@@ -56,7 +38,6 @@ def validate_oss_config(oss_config: dict) -> list[str]:
errors.append(f"Missing required section: {section}")
elif block.get("provider", "") not in registry:
errors.append(f"Unknown {section} provider '{block.get('provider', '')}'. Valid: {', '.join(registry.keys())}")
vs = oss_config.get("vector_store", {})
if vs.get("provider") == "pgvector" and not vs.get("config", {}).get("user"):
errors.append("PGVector requires 'user' in vector_store.config")