lisa-pack-mem0 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: lisa-pack-mem0
3
+ Version: 0.1.0
4
+ Summary: Lisa Mem0 provider pack - external memory backend and MCP server manifest
5
+ Requires-Python: >=3.11
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
8
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
9
+ Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
@@ -0,0 +1,64 @@
1
+ # lisa-pack-mem0
2
+
3
+ Lisa Mem0 provider pack — external memory backend and MCP server manifest.
4
+
5
+ The pack owns the generic `memory.add` / `memory.search` / `memory.list`
6
+ aliases and routes them to Mem0, so any Lisa operation that reads or writes
7
+ long-term memory works against Mem0 without changing.
8
+
9
+ ## What it provides
10
+
11
+ | Layer | Path | Role |
12
+ | --- | --- | --- |
13
+ | MCP manifest | `mcp_servers.yaml` | Declares the Mem0 backend, its result maps, and the tool policy. |
14
+ | Result maps | `result_maps.py` | Normalize Mem0 responses to canonical Lisa `MemoryRecord` JSON. |
15
+
16
+ ### Result maps
17
+
18
+ - **`mem0_add_result_map`** — normalizes the add response to a confirmation
19
+ string.
20
+ - **`mem0_search_result_map`** — maps the Mem0 `score` field to `salience`.
21
+ - **`mem0_list_result_map`** — unwraps the paginated envelope into a flat
22
+ JSON array of records.
23
+
24
+ ### Tool policy
25
+
26
+ The manifest denies `delete_all_memories`, `delete_entitites`, and
27
+ `delete_memory`, so a model cannot erase the user's memory through the pack.
28
+
29
+ ## Configuration
30
+
31
+ | Variable | Meaning |
32
+ | --- | --- |
33
+ | `MEM0_API_KEY` | Bearer token for the Mem0 MCP server. |
34
+
35
+ Select the pack as the memory owner via session context:
36
+
37
+ ```yaml
38
+ providers:
39
+ capabilities:
40
+ memory: lisa-pack-mem0
41
+ ```
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install lisa-pack-mem0
47
+ ```
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ uv run ruff format --check lisa_packs/mem0
53
+ uv run ruff check lisa_packs/mem0
54
+ uv run mypy lisa_packs/mem0
55
+ uv run pytest -m "not live"
56
+ ```
57
+
58
+ Run the checks from a sibling framework checkout so the shared
59
+ `[tool.ruff]` and `[tool.mypy]` settings apply; `pytest` runs from the pack
60
+ root so `pythonpath = ["."]` makes `lisa_packs.mem0` importable.
61
+
62
+ ## License
63
+
64
+ MIT.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: lisa-pack-mem0
3
+ Version: 0.1.0
4
+ Summary: Lisa Mem0 provider pack - external memory backend and MCP server manifest
5
+ Requires-Python: >=3.11
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
8
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
9
+ Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ lisa_pack_mem0.egg-info/PKG-INFO
4
+ lisa_pack_mem0.egg-info/SOURCES.txt
5
+ lisa_pack_mem0.egg-info/dependency_links.txt
6
+ lisa_pack_mem0.egg-info/entry_points.txt
7
+ lisa_pack_mem0.egg-info/requires.txt
8
+ lisa_pack_mem0.egg-info/top_level.txt
9
+ lisa_packs/__init__.py
10
+ lisa_packs/mem0/__init__.py
11
+ lisa_packs/mem0/mcp_servers.yaml
12
+ lisa_packs/mem0/result_maps.py
13
+ lisa_packs/mem0/tests/__init__.py
14
+ lisa_packs/mem0/tests/test_manifest.py
15
+ lisa_packs/mem0/tests/test_result_maps.py
@@ -0,0 +1,2 @@
1
+ [lisa.extensions.content_roots]
2
+ mem0 = lisa_packs.mem0:get_content_root
@@ -0,0 +1,5 @@
1
+
2
+ [dev]
3
+ pytest>=8.0.0
4
+ pytest-asyncio>=0.23.0
5
+ pytest-mock>=3.14.0
@@ -0,0 +1 @@
1
+ lisa_packs
@@ -0,0 +1,5 @@
1
+ """Lisa external pack namespace."""
2
+
3
+ from pkgutil import extend_path
4
+
5
+ __path__ = extend_path(__path__, __name__)
@@ -0,0 +1,10 @@
1
+ """External Mem0 memory provider pack for Lisa."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def get_content_root() -> Path:
9
+ """Return the content root directory for this pack."""
10
+ return Path(__file__).parent
@@ -0,0 +1,40 @@
1
+ ---
2
+ version: 1
3
+
4
+ # External Mem0 provider for memory capability ownership.
5
+ # When selected by host policy, this pack owns memory aliases and routes
6
+ # memory.add/search/list through Mem0.
7
+ memory:
8
+ default_backend: mem0
9
+ backends:
10
+ - id: mem0
11
+ kind: http
12
+ url: https://mcp.mem0.ai/mcp
13
+ headers:
14
+ Authorization: "Bearer ${MEM0_API_KEY}"
15
+ agent_id: lisa
16
+ tool_names:
17
+ add: add_memory
18
+ search: search_memories
19
+ list: get_memories
20
+ result_maps:
21
+ add: lisa_packs.mem0.result_maps.mem0_add_result_map
22
+ search: lisa_packs.mem0.result_maps.mem0_search_result_map
23
+ list: lisa_packs.mem0.result_maps.mem0_list_result_map
24
+
25
+ servers:
26
+ - server_id: mem0
27
+ runtime_kind: http
28
+ url: https://mcp.mem0.ai/mcp
29
+ auth:
30
+ type: bearer
31
+ token: "${MEM0_API_KEY}"
32
+ tool_policy:
33
+ denied_tools:
34
+ - delete_all_memories
35
+ - delete_entitites
36
+ - delete_memory
37
+ proxy_policy:
38
+ ambient:
39
+ user_id: user_slug
40
+ structured_filters: true
@@ -0,0 +1,147 @@
1
+ """Output-normalization adapters for Mem0 MCP tool responses.
2
+
3
+ These functions are declared in ``mcp_servers.yaml`` under
4
+ ``memory.backends[].result_maps`` and resolved by
5
+ :func:`hosts.core.memory_aliases._backend_result_maps` at alias-registration
6
+ time. Each one receives the raw ``output_text`` from the Mem0 MCP tool and
7
+ returns a normalized string in the **canonical memory shape** — a JSON array
8
+ of ``MemoryRecord``-compatible dicts — so the learning-pack operations
9
+ (``recall_memory``, ``synthesize_and_store_learning``) consume a uniform
10
+ shape regardless of which backend is active.
11
+
12
+ Mem0 response shapes (v3):
13
+ - ``get_memories`` (list): ``{"count": N, "next": ..., "previous": ..., "results": [...]}``
14
+ - ``search_memories`` (search): ``{"results": [...]}`` — items include a ``score`` field
15
+ - ``add_memory`` (add): ``{"results": [{"id": ..., "memory": ..., "event_id": ...}]}``
16
+
17
+ Each result item has:
18
+ - ``id``: UUID string (not int)
19
+ - ``memory``: the memory text (not ``content``)
20
+ - ``created_at``: ISO datetime string
21
+ - ``score`` (search only): float 0..1
22
+ - ``user_id``: the scoped user
23
+ - ``metadata``: dict
24
+ - ``categories``: list[str]
25
+ - ``updated_at``: ISO datetime or null
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import logging
32
+ from typing import Any
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _unwrap_results(raw_text: str) -> list[dict[str, Any]]:
38
+ """Parse the raw MCP tool output and extract the results list.
39
+
40
+ Handles three shapes:
41
+ 1. Mem0 paginated envelope: ``{"results": [...], "count": ...}``
42
+ 2. Bare list of items (already canonical from some other backend)
43
+ 3. Empty or invalid → returns ``[]``
44
+ """
45
+ if not raw_text or not raw_text.strip():
46
+ return []
47
+ try:
48
+ data: Any = json.loads(raw_text)
49
+ except (json.JSONDecodeError, TypeError):
50
+ logger.debug("result_map: failed to parse JSON, returning empty list")
51
+ return []
52
+ if isinstance(data, list):
53
+ return [item for item in data if isinstance(item, dict)]
54
+ if isinstance(data, dict):
55
+ results = data.get("results")
56
+ if isinstance(results, list):
57
+ return [item for item in results if isinstance(item, dict)]
58
+ return []
59
+
60
+
61
+ def _mem0_item_to_record(item: dict[str, Any]) -> dict[str, Any]:
62
+ """Map one Mem0 result item to a canonical ``MemoryRecord``-compatible dict.
63
+
64
+ Mem0 fields → canonical fields:
65
+ id (UUID str) → id (kept as str; MemoryRecord.id accepts int|str)
66
+ memory → content
67
+ created_at → created_at
68
+ user_id → user_slug
69
+ score → salience (search responses only)
70
+ metadata → content_json (merged as-is)
71
+ categories → stored in content_json for preservation
72
+ """
73
+ content_json: dict[str, Any] = {}
74
+ metadata = item.get("metadata")
75
+ if isinstance(metadata, dict):
76
+ content_json["metadata"] = metadata
77
+ categories = item.get("categories")
78
+ if isinstance(categories, list):
79
+ content_json["categories"] = categories
80
+
81
+ return {
82
+ "id": str(item.get("id", "")),
83
+ "content": str(item.get("memory", "")),
84
+ "created_at": str(item.get("created_at", "")),
85
+ "user_slug": str(item.get("user_id", "")),
86
+ "user_id": str(item.get("user_id", "")),
87
+ "salience": float(item.get("score", 0.0) or 0.0),
88
+ "content_json": content_json,
89
+ "memory_scope": "personal",
90
+ "memory_tier": "long_term",
91
+ "surface_id": "",
92
+ "source_user_slug": str(item.get("user_id", "")),
93
+ "operation_id": "mem0.add_memory",
94
+ "agent_id": "lisa",
95
+ "workspace_id": "default",
96
+ "memory_kind": "episodic",
97
+ "confidence": 1.0,
98
+ "evidence_ids": [],
99
+ "supersedes_id": None,
100
+ "valid_until": None,
101
+ "last_accessed_at": None,
102
+ "access_count": 0,
103
+ "status": "active",
104
+ }
105
+
106
+
107
+ def mem0_list_result_map(raw_text: str) -> str:
108
+ """Normalize ``get_memories`` output into a canonical JSON array of records.
109
+
110
+ Mem0's ``get_memories`` returns a paginated envelope:
111
+ ``{"count": N, "next": ..., "previous": ..., "results": [...]}``
112
+
113
+ This unwraps ``results``, maps each item to the ``MemoryRecord`` shape,
114
+ and returns a flat JSON array string.
115
+ """
116
+ items = _unwrap_results(raw_text)
117
+ records = [_mem0_item_to_record(item) for item in items]
118
+ return json.dumps(records, default=str)
119
+
120
+
121
+ def mem0_search_result_map(raw_text: str) -> str:
122
+ """Normalize ``search_memories`` output into a canonical JSON array of records.
123
+
124
+ Same envelope as ``get_memories`` (``{"results": [...]}``), but each
125
+ search item includes a ``score`` field (float 0..1) that maps to
126
+ ``salience`` in the canonical record.
127
+ """
128
+ items = _unwrap_results(raw_text)
129
+ records = [_mem0_item_to_record(item) for item in items]
130
+ return json.dumps(records, default=str)
131
+
132
+
133
+ def mem0_add_result_map(raw_text: str) -> str:
134
+ """Normalize ``add_memory`` output into a simple confirmation string.
135
+
136
+ Mem0's ``add_memory`` returns ``{"results": [{"id": ..., "memory": ..., "event_id": ...}]}``
137
+ (an async job accepted response). Normalize to a simple confirmation
138
+ so operations checking ``tool_result.error`` see a clean success.
139
+ """
140
+ items = _unwrap_results(raw_text)
141
+ if not items:
142
+ return "Memory stored."
143
+ first = items[0]
144
+ memory_id = first.get("id", "")
145
+ if memory_id:
146
+ return f"Memory stored with ID {memory_id}"
147
+ return "Memory stored."
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ from kernel.mcp_launch.server_manifest import load_mcp_server_manifest
5
+
6
+ from lisa_packs.mem0 import get_content_root
7
+
8
+
9
+ def test_mem0_pack_manifest_loads_and_owns_memory(monkeypatch: pytest.MonkeyPatch) -> None:
10
+ # The manifest expands ${MEM0_API_KEY} at load time and the manifest
11
+ # loader rejects an empty bearer token, so a value must be present even
12
+ # though this test never contacts the server.
13
+ monkeypatch.setenv("MEM0_API_KEY", "test-key")
14
+
15
+ root = get_content_root()
16
+ specs, memory = load_mcp_server_manifest(root / "mcp_servers.yaml")
17
+
18
+ assert memory.default_backend == "mem0"
19
+ assert any(backend.id == "mem0" for backend in memory.backends)
20
+ assert any(spec.server_id == "mem0" for spec in specs)
@@ -0,0 +1,147 @@
1
+ """Tests for lisa_packs.mem0.result_maps — Mem0 response normalization adapters.
2
+
3
+ Covers:
4
+ - mem0_list_result_map unwraps the paginated envelope into a flat JSON array.
5
+ - mem0_search_result_map maps score → salience.
6
+ - mem0_add_result_map normalizes the add response to a confirmation string.
7
+ - Edge cases: empty input, bare list, invalid JSON, missing fields.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ from lisa_packs.mem0.result_maps import (
15
+ mem0_add_result_map,
16
+ mem0_list_result_map,
17
+ mem0_search_result_map,
18
+ )
19
+
20
+ _EXPECTED_RESULT_COUNT = 2
21
+ _EXPECTED_SEARCH_SALIENCE = 0.82
22
+
23
+
24
+ # ── Realistic Mem0 response payloads ──────────────────────────────────────
25
+
26
+
27
+ _MEM0_LIST_RESPONSE = json.dumps(
28
+ {
29
+ "count": 2,
30
+ "next": None,
31
+ "previous": None,
32
+ "results": [
33
+ {
34
+ "id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
35
+ "memory": "User moved to San Francisco from New York in January 2026",
36
+ "metadata": {"category": "location"},
37
+ "categories": ["location"],
38
+ "created_at": "2026-01-15T10:30:00Z",
39
+ "updated_at": "2026-01-15T10:30:00Z",
40
+ },
41
+ {
42
+ "id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
43
+ "memory": "Alex prefers vegetarian restaurants",
44
+ "metadata": {},
45
+ "categories": [],
46
+ "created_at": "2026-07-05T15:30:00Z",
47
+ "updated_at": "2026-07-05T15:30:00Z",
48
+ },
49
+ ],
50
+ }
51
+ )
52
+
53
+ _MEM0_SEARCH_RESPONSE = json.dumps(
54
+ {
55
+ "results": [
56
+ {
57
+ "id": "ea925981-272f-40dd-b576-be64e4871429",
58
+ "memory": "Likes to play cricket and plays cricket on weekends.",
59
+ "user_id": "alice",
60
+ "metadata": {"category": "hobbies"},
61
+ "categories": ["hobbies"],
62
+ "score": 0.82,
63
+ "expiration_date": None,
64
+ "created_at": "2024-07-26T10:29:36.630547-07:00",
65
+ "updated_at": None,
66
+ },
67
+ ],
68
+ }
69
+ )
70
+
71
+ _MEM0_ADD_RESPONSE = json.dumps(
72
+ {
73
+ "results": [
74
+ {
75
+ "id": "new-mem-uuid-1234",
76
+ "memory": "The user likes hiking",
77
+ "event_id": "evt-5678",
78
+ },
79
+ ],
80
+ }
81
+ )
82
+
83
+
84
+ # ── Tests ─────────────────────────────────────────────────────────────────
85
+
86
+
87
+ def test_mem0_list_result_map_unwraps_envelope() -> None:
88
+ """The paginated dict envelope is unwrapped to a flat JSON array."""
89
+ raw = mem0_list_result_map(_MEM0_LIST_RESPONSE)
90
+ data = json.loads(raw)
91
+ assert isinstance(data, list)
92
+ assert len(data) == _EXPECTED_RESULT_COUNT
93
+ assert data[0]["content"] == "User moved to San Francisco from New York in January 2026"
94
+ assert data[0]["id"] == "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c"
95
+ assert data[0]["created_at"] == "2026-01-15T10:30:00Z"
96
+ # metadata preserved in content_json
97
+ assert data[0]["content_json"]["metadata"] == {"category": "location"}
98
+ assert data[0]["content_json"]["categories"] == ["location"]
99
+
100
+
101
+ def test_mem0_search_result_map_maps_score_to_salience() -> None:
102
+ """The search score field maps to salience in the canonical record."""
103
+ raw = mem0_search_result_map(_MEM0_SEARCH_RESPONSE)
104
+ data = json.loads(raw)
105
+ assert isinstance(data, list)
106
+ assert len(data) == 1
107
+ assert data[0]["content"] == "Likes to play cricket and plays cricket on weekends."
108
+ assert data[0]["salience"] == _EXPECTED_SEARCH_SALIENCE
109
+ assert data[0]["user_slug"] == "alice"
110
+
111
+
112
+ def test_mem0_add_result_map_returns_confirmation() -> None:
113
+ """The add response normalizes to a simple confirmation string."""
114
+ raw = mem0_add_result_map(_MEM0_ADD_RESPONSE)
115
+ assert raw == "Memory stored with ID new-mem-uuid-1234"
116
+
117
+
118
+ def test_mem0_list_result_map_empty_input() -> None:
119
+ """Empty input returns an empty JSON array."""
120
+ assert json.loads(mem0_list_result_map("")) == []
121
+
122
+
123
+ def test_mem0_list_result_map_bare_list() -> None:
124
+ """A bare list (already canonical) passes through after normalization."""
125
+ bare = json.dumps([{"id": 1, "memory": "test", "created_at": "2026-01-01"}])
126
+ data = json.loads(mem0_list_result_map(bare))
127
+ assert isinstance(data, list)
128
+ assert len(data) == 1
129
+ assert data[0]["content"] == "test"
130
+
131
+
132
+ def test_mem0_list_result_map_invalid_json() -> None:
133
+ """Invalid JSON returns an empty array."""
134
+ assert json.loads(mem0_list_result_map("not json")) == []
135
+
136
+
137
+ def test_mem0_search_result_map_missing_score() -> None:
138
+ """Items without a score field default salience to 0.0."""
139
+ raw = json.dumps({"results": [{"id": "x", "memory": "no score", "created_at": "2026-01-01"}]})
140
+ data = json.loads(mem0_search_result_map(raw))
141
+ assert data[0]["salience"] == 0.0
142
+
143
+
144
+ def test_mem0_add_result_map_empty() -> None:
145
+ """Empty add response returns a generic confirmation."""
146
+ assert mem0_add_result_map("") == "Memory stored."
147
+ assert mem0_add_result_map("{}") == "Memory stored."
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where = ["."]
7
+ include = ["lisa_packs*", "lisa_packs.mem0*"]
8
+
9
+ [tool.setuptools.package-data]
10
+ "lisa_packs" = ["*"]
11
+ "lisa_packs.mem0" = ["mcp_servers.yaml"]
12
+
13
+ [project]
14
+ name = "lisa-pack-mem0"
15
+ version = "0.1.0"
16
+ description = "Lisa Mem0 provider pack - external memory backend and MCP server manifest"
17
+ requires-python = ">=3.11"
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8.0.0",
22
+ "pytest-asyncio>=0.23.0",
23
+ "pytest-mock>=3.14.0",
24
+ ]
25
+
26
+ [tool.pytest.ini_options]
27
+ pythonpath = ["."]
28
+ testpaths = ["lisa_packs/mem0/tests"]
29
+ markers = [
30
+ "live: hits real external signal or provider APIs and prints a tester-visible summary",
31
+ "integration: multi-module in-process test",
32
+ "eval: deterministic agent regression evals (no model calls, no I/O)",
33
+ ]
34
+
35
+ [project.entry-points."lisa.extensions.content_roots"]
36
+ mem0 = "lisa_packs.mem0:get_content_root"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+