Merge branch 'main' into fix/together-base-url-py

This commit is contained in:
kartik-mem0
2026-08-14 17:13:36 +05:30
107 changed files with 3188 additions and 1187 deletions
+24
View File
@@ -0,0 +1,24 @@
# Python SDK tests (`tests/`)
pytest suite for the `mem0/` package.
## Commands
```bash
make install_all # optional deps; several tests need them
make test # pytest tests/
make test-py-3.9 # pin a Python version (3.9 through 3.12)
pytest tests/llms/test_openai.py::test_generate_response # single test
```
## Conventions
- Files are named `test_<module>.py`.
- Provider tests mirror the source tree: `tests/<category>/<provider_name>/`.
- pytest-mock for mocks, pytest-asyncio for the async surface.
- Ruff line length **120**, matching `mem0/`. See [`../mem0/AGENTS.md`](../mem0/AGENTS.md).
- Mock the provider SDK, never the code under test. A test that asserts the implementation back at itself is worse than no test.
- Bug fixes need a regression test that fails without the fix. Write it first and watch it fail.
Tests for other packages live with those packages: `mem0-ts/` (jest), `cli/python/tests/` (pytest), `cli/node/` (vitest), and each directory under `integrations/`.
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+55 -1
View File
@@ -6,7 +6,6 @@ from mem0.configs.llms.aws_bedrock import AWSBedrockConfig
from mem0.llms.aws_bedrock import AWSBedrockLLM, extract_provider
from mem0.utils.factory import LlmFactory
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -70,6 +69,23 @@ class TestExtractProvider:
with pytest.raises(ValueError, match="Unknown provider"):
extract_provider("unknown-vendor.some-model-v1:0")
def test_application_inference_profile_arn_without_override_raises(self):
arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz"
with pytest.raises(ValueError, match="Unknown provider"):
extract_provider(arn)
def test_application_inference_profile_arn_with_explicit_provider(self):
arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz"
assert extract_provider(arn, "anthropic") == "anthropic"
def test_explicit_provider_takes_precedence_over_regex(self):
assert extract_provider("anthropic.claude-3-5-sonnet-20240620-v1:0", "amazon") == "amazon"
def test_explicit_provider_typo_raises(self):
arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz"
with pytest.raises(ValueError, match="Unknown provider_override 'anthorpic'"):
extract_provider(arn, "anthorpic")
# ---------------------------------------------------------------------------
# AWSBedrockConfig
@@ -114,6 +130,44 @@ class TestAWSBedrockConfig:
)
assert config.aws_region == "us-east-2"
def test_provider_override_defaults_to_none(self):
config = AWSBedrockConfig(model="anthropic.claude-3-5-sonnet-20240620-v1:0")
assert config.provider_override is None
def test_provider_override_stored(self):
arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz"
config = AWSBedrockConfig(model=arn, provider_override="anthropic")
assert config.provider_override == "anthropic"
# ---------------------------------------------------------------------------
# AWSBedrockLLM with application inference profile ARNs
# ---------------------------------------------------------------------------
class TestApplicationInferenceProfileArn:
ARN = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz"
def test_arn_without_provider_override_raises(self, mock_boto3):
with pytest.raises(ValueError, match="Unknown provider"):
_make_llm(self.ARN, mock_boto3)
def test_arn_with_provider_override_resolves(self, mock_boto3):
llm = _make_llm(self.ARN, mock_boto3, provider_override="anthropic")
assert llm.provider == "anthropic"
assert llm.supports_tools is True
def test_plain_model_id_unaffected(self, mock_boto3):
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
assert llm.provider == "anthropic"
def test_cross_region_inference_profile_unaffected(self, mock_boto3):
llm = _make_llm("us.anthropic.claude-haiku-4-5-20251001-v1:0", mock_boto3)
assert llm.provider == "anthropic"
def test_arn_with_misspelled_provider_override_raises(self, mock_boto3):
with pytest.raises(ValueError, match="Unknown provider_override 'anthorpic'"):
_make_llm(self.ARN, mock_boto3, provider_override="anthorpic")
# ---------------------------------------------------------------------------
# LlmFactory
+54
View File
@@ -4,6 +4,7 @@ import pytest
from mem0 import AsyncMemory, Memory
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.vllm import VllmConfig
from mem0.llms.vllm import VllmLLM
@@ -15,6 +16,59 @@ def mock_vllm_client():
yield mock_client
def test_base_url_explicit_arg_wins_over_env(mock_vllm_client, monkeypatch):
monkeypatch.setenv("VLLM_BASE_URL", "http://from-env:8000/v1")
config = VllmConfig(vllm_base_url="http://from-arg:8000/v1")
VllmLLM(config)
assert config.vllm_base_url == "http://from-arg:8000/v1"
def test_base_url_env_wins_over_default(mock_vllm_client, monkeypatch):
monkeypatch.setenv("VLLM_BASE_URL", "http://from-env:8000/v1")
config = VllmConfig()
VllmLLM(config)
assert config.vllm_base_url == "http://from-env:8000/v1"
def test_base_url_default_when_neither_set(mock_vllm_client, monkeypatch):
monkeypatch.delenv("VLLM_BASE_URL", raising=False)
config = VllmConfig()
VllmLLM(config)
assert config.vllm_base_url == "http://localhost:8000/v1"
def test_openai_client_receives_explicit_base_url(monkeypatch):
monkeypatch.delenv("VLLM_BASE_URL", raising=False)
with patch("mem0.llms.vllm.OpenAI") as mock_openai:
VllmLLM(VllmConfig(vllm_base_url="http://from-arg:8000/v1"))
assert mock_openai.call_args.kwargs["base_url"] == "http://from-arg:8000/v1"
def test_openai_client_receives_base_url_from_env(monkeypatch):
monkeypatch.setenv("VLLM_BASE_URL", "http://from-env:8000/v1")
with patch("mem0.llms.vllm.OpenAI") as mock_openai:
VllmLLM(VllmConfig())
base_url = mock_openai.call_args.kwargs["base_url"]
assert base_url == "http://from-env:8000/v1"
assert base_url != "http://localhost:8000/v1"
def test_openai_client_receives_base_url_from_env_via_base_llm_config(monkeypatch):
"""Original #6256 repro: BaseLlmConfig (not VllmConfig) plus VLLM_BASE_URL only."""
monkeypatch.setenv("VLLM_BASE_URL", "http://remote-vllm:8000/v1")
with patch("mem0.llms.vllm.OpenAI") as mock_openai:
VllmLLM(BaseLlmConfig(model="Qwen/Qwen2.5-32B-Instruct"))
base_url = mock_openai.call_args.kwargs["base_url"]
assert base_url == "http://remote-vllm:8000/v1"
assert base_url != "http://localhost:8000/v1"
def test_generate_response_without_tools(mock_vllm_client):
config = BaseLlmConfig(model="Qwen/Qwen2.5-32B-Instruct", temperature=0.7, max_tokens=100, top_p=1.0)
llm = VllmLLM(config)
+145
View File
@@ -8,6 +8,7 @@ import httpx
import pytest
import requests
from mem0.client.main import AsyncMemoryClient
from mem0.client.types import GetAllMemoryOptions, SearchMemoryOptions
@@ -181,6 +182,150 @@ class TestGetAllTypedOptionsParity:
)
class TestGetAllPageSizeQueryParams:
"""MEM-6227: page and page_size must reach the API as query params independently."""
def test_get_all_page_size_alone_lands_in_query_params(self, mock_memory_client):
"""get_all(page_size=...) without page should send page_size as a query param."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"}, page_size=50)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page_size": 50},
)
def test_get_all_page_alone_lands_in_query_params(self, mock_memory_client):
"""get_all(page=...) without page_size should send page as a query param."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"}, page=2)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page": 2},
)
def test_get_all_page_and_page_size_together(self, mock_memory_client):
"""get_all(page=..., page_size=...) should send both as query params."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"}, page=2, page_size=50)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page": 2, "page_size": 50},
)
def test_get_all_neither_page_nor_page_size_sends_no_query_params(self, mock_memory_client):
"""get_all() without page or page_size should not pass a params kwarg."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"})
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
)
def test_async_get_all_page_size_alone_lands_in_query_params(self):
asyncio.run(self._assert_async_get_all_page_size_alone())
async def _assert_async_get_all_page_size_alone(self):
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
client.async_client.post = AsyncMock(return_value=mock_response)
with patch("mem0.client.main.capture_client_event"):
await client.get_all(filters={"user_id": "u1"}, page_size=50)
client.async_client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page_size": 50},
)
def test_async_get_all_page_alone_lands_in_query_params(self):
asyncio.run(self._assert_async_get_all_page_alone())
async def _assert_async_get_all_page_alone(self):
"""get_all(page=0) without page_size should still send page=0 as a query param."""
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
client.async_client.post = AsyncMock(return_value=mock_response)
with patch("mem0.client.main.capture_client_event"):
await client.get_all(filters={"user_id": "u1"}, page=0)
client.async_client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page": 0},
)
def test_async_get_all_page_and_page_size_together(self):
asyncio.run(self._assert_async_get_all_page_and_page_size_together())
async def _assert_async_get_all_page_and_page_size_together(self):
"""get_all(page=0, page_size=0) should send both as query params despite being falsy."""
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
client.async_client.post = AsyncMock(return_value=mock_response)
with patch("mem0.client.main.capture_client_event"):
await client.get_all(filters={"user_id": "u1"}, page=0, page_size=0)
client.async_client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
params={"page": 0, "page_size": 0},
)
def test_async_get_all_neither_page_nor_page_size_sends_no_query_params(self):
asyncio.run(self._assert_async_get_all_neither_page_nor_page_size())
async def _assert_async_get_all_neither_page_nor_page_size(self):
client = AsyncMemoryClient.__new__(AsyncMemoryClient)
client.async_client = MagicMock()
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
client.async_client.post = AsyncMock(return_value=mock_response)
with patch("mem0.client.main.capture_client_event"):
await client.get_all(filters={"user_id": "u1"})
client.async_client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}},
)
class TestUpdateExpirationDate:
"""Tests for update expiration_date payload handling."""
+16 -2
View File
@@ -249,9 +249,23 @@ REDIS_HOST = os.environ.get("REDIS_HOST", "localhost")
REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379"))
def _redis_search_available(host, port, timeout=2):
"""RediSearch (the FT.* command family) is an optional module, not part of vanilla Redis."""
if not _tcp_reachable(host, port, timeout=timeout):
return False
try:
import redis
client = redis.Redis(host=host, port=port, socket_connect_timeout=timeout, socket_timeout=timeout)
modules = {m["name"].lower() for m in client.module_list()}
return "search" in modules
except Exception:
return False
@pytest.mark.skipif(
not _tcp_reachable(REDIS_HOST, REDIS_PORT),
reason=f"Redis not reachable at {REDIS_HOST}:{REDIS_PORT}",
not _redis_search_available(REDIS_HOST, REDIS_PORT),
reason=f"RediSearch module not available at {REDIS_HOST}:{REDIS_PORT} (requires Redis Stack, not vanilla Redis)",
)
class TestRedisThreshold:
def test_threshold_filtering(self):
@@ -239,9 +239,23 @@ REDIS_HOST = os.environ.get("REDIS_HOST", "localhost")
REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379"))
def _redis_search_available(host, port, timeout=2):
"""RediSearch (the FT.* command family) is an optional module, not part of vanilla Redis."""
if not _tcp_reachable(host, port, timeout=timeout):
return False
try:
import redis
client = redis.Redis(host=host, port=port, socket_connect_timeout=timeout, socket_timeout=timeout)
modules = {m["name"].lower() for m in client.module_list()}
return "search" in modules
except Exception:
return False
@pytest.mark.skipif(
not _tcp_reachable(REDIS_HOST, REDIS_PORT),
reason=f"Redis not reachable at {REDIS_HOST}:{REDIS_PORT}",
not _redis_search_available(REDIS_HOST, REDIS_PORT),
reason=f"RediSearch module not available at {REDIS_HOST}:{REDIS_PORT} (requires Redis Stack, not vanilla Redis)",
)
class TestRedis:
"""Redis returns cosine distance [0,2]. Conversion: score = max(0, 1 - dist)."""