Skip to content

Commit fc198b8

Browse files
authored
Merge pull request #40 from beersoccer/feature/memory-lifecycle-cleanup
chore: update mem0ai dependency constraints and enhance async memory resolution
2 parents c5b9a1d + 24567ed commit fc198b8

6 files changed

Lines changed: 121 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.2.10"
44
description = "Mem0 Dify plugin"
55
requires-python = ">=3.12"
66
dependencies = [
7-
"mem0ai>=1.0.2",
7+
"mem0ai>=1.0.2,<=1.0.11",
88
"openai",
99
"azure-identity",
1010
"langchain-neo4j",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mem0ai>=1.0.2
1+
mem0ai>=1.0.2,<=1.0.11
22
openai
33
azure-identity
44
langchain-neo4j
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from unittest.mock import MagicMock
5+
6+
from provider.mem0ai import Mem0Provider
7+
8+
9+
def test_validate_credentials_async_mode_uses_async_client_search(monkeypatch) -> None:
10+
import provider.mem0ai as provider_mod
11+
12+
captured: dict[str, object] = {}
13+
fake_loop = object()
14+
fake_future = MagicMock()
15+
fake_future.result.return_value = {"results": []}
16+
17+
class FakeClient:
18+
def ensure_bg_loop(self) -> object:
19+
captured["ensure_bg_loop_called"] = True
20+
return fake_loop
21+
22+
async def search(self, payload: dict[str, object], timeout_s: int) -> dict[str, object]:
23+
captured["search_payload"] = payload
24+
captured["search_timeout"] = timeout_s
25+
return {"results": []}
26+
27+
def _fake_run_coroutine_threadsafe(coro, loop): # noqa: ANN001
28+
assert asyncio.iscoroutine(coro)
29+
captured["loop"] = loop
30+
coro.close()
31+
return fake_future
32+
33+
monkeypatch.setattr(provider_mod, "get_async_client", lambda _credentials: FakeClient())
34+
monkeypatch.setattr(
35+
provider_mod.asyncio,
36+
"run_coroutine_threadsafe",
37+
_fake_run_coroutine_threadsafe,
38+
)
39+
40+
provider = object.__new__(Mem0Provider)
41+
provider._validate_credentials({"async_mode": True, "log_level": "INFO"})
42+
43+
assert captured["ensure_bg_loop_called"] is True
44+
assert captured["loop"] is fake_loop
45+
assert fake_future.result.call_count == 1
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from utils.mem0_client import AsyncMem0Client
8+
9+
10+
@pytest.mark.asyncio
11+
async def test_create_supports_async_from_config(monkeypatch: pytest.MonkeyPatch) -> None:
12+
import utils.mem0_client as mem0_client
13+
14+
fake_memory = MagicMock()
15+
fake_memory.llm = None
16+
17+
monkeypatch.setattr(mem0_client, "build_local_mem0_config", lambda _c: {})
18+
19+
async def _fake_from_config(config: dict[str, object]) -> object:
20+
assert config == {}
21+
return fake_memory
22+
23+
monkeypatch.setattr(mem0_client.AsyncMemory, "from_config", _fake_from_config)
24+
25+
client = AsyncMem0Client({})
26+
27+
try:
28+
created = await client.create()
29+
assert created is fake_memory
30+
assert client.memory is fake_memory
31+
finally:
32+
await client.aclose()
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_create_supports_sync_from_config(monkeypatch: pytest.MonkeyPatch) -> None:
37+
import utils.mem0_client as mem0_client
38+
39+
fake_memory = MagicMock()
40+
fake_memory.llm = None
41+
42+
monkeypatch.setattr(mem0_client, "build_local_mem0_config", lambda _c: {})
43+
44+
def _fake_from_config(config: dict[str, object]) -> object:
45+
assert config == {}
46+
return fake_memory
47+
48+
monkeypatch.setattr(mem0_client.AsyncMemory, "from_config", _fake_from_config)
49+
50+
client = AsyncMem0Client({})
51+
52+
try:
53+
created = await client.create()
54+
assert created is fake_memory
55+
assert client.memory is fake_memory
56+
finally:
57+
await client.aclose()

utils/mem0_client.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@
4343
_mem0_init_lock = threading.Lock()
4444

4545

46+
async def _resolve_async_memory_from_config(config: dict[str, Any]) -> AsyncMemory:
47+
"""Support both old and new mem0 AsyncMemory.from_config semantics.
48+
49+
Older mem0 releases exposed ``AsyncMemory.from_config`` as an async
50+
classmethod, while newer releases return an ``AsyncMemory`` instance
51+
directly. Dify's async validation path always calls ``create()``, so we
52+
normalize both forms here and keep the rest of the client code unchanged.
53+
"""
54+
memory_or_awaitable = AsyncMemory.from_config(config)
55+
if asyncio.iscoroutine(memory_or_awaitable):
56+
return await memory_or_awaitable
57+
return memory_or_awaitable
58+
59+
4660
def _patch_llm_compat(llm: Any) -> None:
4761
"""Patch LLM instances that lack _parse_response (e.g., structured providers)."""
4862
if llm is None or hasattr(llm, "_parse_response"):
@@ -834,7 +848,7 @@ async def create(self) -> AsyncMemory:
834848
loop = asyncio.get_event_loop()
835849
await loop.run_in_executor(None, _mem0_init_lock.acquire)
836850
try:
837-
self.memory = await AsyncMemory.from_config(self.config)
851+
self.memory = await _resolve_async_memory_from_config(self.config)
838852
finally:
839853
_mem0_init_lock.release()
840854
_patch_llm_compat(getattr(self.memory, "llm", None))

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)