hermes-agent/tests/plugins/memory/test_mem0_providers.py
Kartik 2e779d11a0
feat(mem0): v3 API, OSS mode, update/delete tools, telemetry & review fixes (#15624)
* fix: update to version 3 endpoints and adding update and delete tool

* chore: removing the test md file

* fix: prevent circuit breaker on client errors in Mem0 provider

* chore: add telemetry for platform version

* feat: add OSS mode support to Mem0 memory provider

* chore: bump mem0ai dependency to >=2.0.1 in memory plugin

* refactor: enhance dependency checks and embedder config in mem0 backend

* refactor: adjust fact storage message for OSS mode

* refactor: expand user paths, add collection recreation on dimension change for Qdrant

* fix(mem0): make MEM0_USER_ID override gateway-native ids and tag writes with channel

When MEM0_USER_ID was configured (env or mem0.json), the gateway-native id
from kwargs (Telegram numeric id, Discord snowflake, ...) still won, so the
same human ended up under different user_ids per channel and memories never
merged across CLI / Telegram / Slack / Discord. Mirrors openclaw's cfg.userId
pattern: configured override wins, gateway-native id is the fallback.

The legacy "hermes-user" placeholder default written by the setup wizard is
treated as unset to avoid silently bucketing every gateway user together.

Also tag every write with metadata.channel (cli/telegram/discord/...) so the
dashboard can offer per-channel filtered views without coupling identity to
the channel; document the read/write filter asymmetry as intentional
(reads scope to user_id only for cross-agent recall).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: improve Mem0 memory provider backend, pagination, config, and error handling

* refactor: update mem0 telemetry code, docs, and bump version

* fix(mem0): make get_config_schema() return unified schema with mode-aware required flag

Schema always includes api_key field so picker shows "API key / local" for
both modes. In OSS mode api_key.required=False so status won't mislead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: improve mem0 telemetry, add env var key and OSS mode detection

* chore: bump mem0ai lower bound to 2.0.4 (latest SDK release)

* refactor: set telemetry sample rate to 1.0 and update docs for opt‑out

* fix(mem0): resolve 15 correctness, thread-safety, and resource bugs

Thread safety:
- Protect circuit breaker counters with _breaker_lock (race between
  prefetch/sync daemon threads and main thread)
- Wrap sync_turn thread creation in _sync_lock; skip if previous sync
  is still alive after 5 s join to prevent duplicate memory ingestion
- Guard _schedule_flush timer creation under _queue_lock (TOCTOU race)
- Capture local `backend` reference in prefetch/sync closures so
  shutdown() nulling self._backend cannot crash in-flight threads

Correctness:
- Fix bool("false")==True for rerank param; parse string values explicitly
- Guard page/top_k with max(1,...) and move int() inside try blocks
- Fix fact_count=0 always in OSS mode (Memory.add returns list, not dict)
- Fix prefetch() not clearing result when thread still alive after timeout
- Fix atexit.register accumulating on repeated initialize() calls

Backend / setup:
- Handle Qdrant named-vector collections in _recreate_collection_if_dims_changed
  (vectors is a dict; .size access raised AttributeError, swallowed silently)
- Wrap QdrantClient and psycopg2 conn/cursor in try/finally to prevent leaks
- Resolve ollama_bin at top of _ensure_ollama; use it for ollama pull
- Fix embedder key lookup when LLM provider has no env_var (e.g. ollama)

Also: remove _telemetry_enabled cache (env var check is cheap), bump
required mem0ai to >=2.0.7, minor README wording fix.

* fix(mem0): fix brittle qdrant path test + add telemetry sample-rate docs

- Replace generator-throw lambda with a proper def in
  test_qdrant_path_not_writable; use tmp_path instead of a hardcoded
  /nonexistent path so the test is root-safe
- Add MEM0_TELEMETRY_SAMPLE_RATE to memory-providers.md (was only
  in the plugin README, not the user-guide docs)

* revert: remove MEM0_TELEMETRY_SAMPLE_RATE from user-guide docs

* refactor: remove telemetry from mem0 plugin and update documentation

* fix(mem0): set stdin=DEVNULL on setup subprocess calls

The TUI stdin guard (scripts/check_subprocess_stdin.py) requires every
subprocess call in plugin code to set stdin= so it can't inherit the
gateway's JSON-RPC stdin fd. Muzzle the docker/ollama calls in the OSS
setup wizard with stdin=subprocess.DEVNULL (none need interactive input).
Also covers the docker-inspect call the linter's regex misses.

---------

Co-authored-by: chaithanyak42 <chaithanya.kumar42a@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-22 12:30:47 +00:00

107 lines
3.7 KiB
Python

"""Tests for OSS provider definitions and validation."""
import pytest
from plugins.memory.mem0._oss_providers import (
LLM_PROVIDERS,
EMBEDDER_PROVIDERS,
VECTOR_PROVIDERS,
KNOWN_DIMS,
validate_oss_config,
)
class TestProviderDefinitions:
def test_llm_providers_have_required_keys(self):
for pid, p in LLM_PROVIDERS.items():
assert "label" in p
assert "needs_key" in p
assert "default_model" in p
def test_embedder_providers_have_required_keys(self):
for pid, p in EMBEDDER_PROVIDERS.items():
assert "label" in p
assert "needs_key" in p
assert "default_model" in p
assert "dims" in p
def test_embedder_provider_ids(self):
assert set(EMBEDDER_PROVIDERS.keys()) == {"openai", "ollama"}
def test_vector_providers_have_required_keys(self):
for pid, p in VECTOR_PROVIDERS.items():
assert "label" in p
assert "default_config" in p
def test_vector_provider_ids(self):
assert set(VECTOR_PROVIDERS.keys()) == {"qdrant", "pgvector"}
def test_known_dims_covers_defaults(self):
for pid, p in EMBEDDER_PROVIDERS.items():
assert p["default_model"] in KNOWN_DIMS
class TestValidation:
def test_valid_openai_config(self):
cfg = {
"llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}},
"embedder": {"provider": "openai", "config": {"model": "text-embedding-3-small"}},
"vector_store": {"provider": "qdrant", "config": {"path": "/tmp/test"}},
}
errors = validate_oss_config(cfg)
assert errors == []
def test_unknown_llm_provider(self):
cfg = {
"llm": {"provider": "gemini", "config": {}},
"embedder": {"provider": "openai", "config": {}},
"vector_store": {"provider": "qdrant", "config": {}},
}
errors = validate_oss_config(cfg)
assert any("llm" in e.lower() for e in errors)
def test_unknown_embedder_provider(self):
cfg = {
"llm": {"provider": "openai", "config": {}},
"embedder": {"provider": "cohere", "config": {}},
"vector_store": {"provider": "qdrant", "config": {}},
}
errors = validate_oss_config(cfg)
assert any("embedder" in e.lower() for e in errors)
def test_unknown_vector_provider(self):
cfg = {
"llm": {"provider": "openai", "config": {}},
"embedder": {"provider": "openai", "config": {}},
"vector_store": {"provider": "redis", "config": {}},
}
errors = validate_oss_config(cfg)
assert any("vector" in e.lower() for e in errors)
def test_missing_llm_section(self):
cfg = {
"embedder": {"provider": "openai", "config": {}},
"vector_store": {"provider": "qdrant", "config": {}},
}
errors = validate_oss_config(cfg)
assert any("llm" in e.lower() for e in errors)
def test_pgvector_needs_user(self):
cfg = {
"llm": {"provider": "openai", "config": {}},
"embedder": {"provider": "openai", "config": {}},
"vector_store": {"provider": "pgvector", "config": {"host": "localhost"}},
}
errors = validate_oss_config(cfg)
assert any("user" in e.lower() for e in errors)
def test_pgvector_with_user_valid(self):
cfg = {
"llm": {"provider": "openai", "config": {}},
"embedder": {"provider": "openai", "config": {}},
"vector_store": {"provider": "pgvector", "config": {"host": "localhost", "user": "pg"}},
}
errors = validate_oss_config(cfg)
assert errors == []