mayamemo 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.
mayamemo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MayaMemo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: mayamemo
3
+ Version: 0.1.0
4
+ Summary: MayaMemo Python SDK — evidence-backed context, memories and cross-agent checkpoints for AI coding agents. Zero dependencies.
5
+ Author: MayaMemo
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Coin1511/mayamemo
8
+ Project-URL: Repository, https://github.com/Coin1511/mayamemo/tree/main/sdk/python
9
+ Keywords: mayamemo,ai-agents,memory,context,checkpoints,mcp
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: langchain
18
+ Requires-Dist: langchain-core>=0.2; extra == "langchain"
19
+ Dynamic: license-file
20
+
21
+ # MayaMemo — Python SDK
22
+
23
+ Zero-dependency Python client for the [MayaMemo](../..) API: projects, memories
24
+ (with tags), semantic search, the context compiler, checkpoints and cross-agent resume.
25
+
26
+ ```bash
27
+ # from the repo
28
+ pip install -e sdk/python # or: pip install -e "sdk/python[langchain]"
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ from mayamemo import MayaMemo
35
+
36
+ client = MayaMemo(api_key="mm_sk_...", api_url="http://localhost:4000")
37
+
38
+ project = client.create_project("my-app")
39
+
40
+ client.add_memory(
41
+ project["id"],
42
+ "Checkout retries go through an idempotency table keyed by cart_id + attempt",
43
+ type="decision",
44
+ tags=["checkout", "billing"],
45
+ )
46
+
47
+ hits = client.search_memories(project["id"], "how do we avoid double charges?")
48
+ print(hits[0]["statement"], hits[0]["score"])
49
+
50
+ checkpoint = client.create_checkpoint(
51
+ project["id"], "billing groundwork",
52
+ "Idempotency decision captured; outbox worker noted.",
53
+ next_steps=["wire refund metrics"], agent="my-agent",
54
+ )
55
+
56
+ briefing = client.resume_checkpoint(checkpoint["id"], agent="another-agent")["briefing_markdown"]
57
+ print(briefing)
58
+ ```
59
+
60
+ Environment defaults: `MAYAMEMO_API_KEY` (or `MAYAMEMO_TOKEN`) and `MAYAMEMO_API_URL`
61
+ (`http://localhost:4000`). API keys are created in the workspace (`POST /v1/keys`),
62
+ scoped per workspace and revocable.
63
+
64
+ ## Framework adapters
65
+
66
+ `mayamemo.integrations` ships a LangChain-style retriever and chat-memory adapter.
67
+ They subclass the real `langchain-core` classes when it is installed (true drop-in)
68
+ and work standalone otherwise.
69
+
70
+ ```python
71
+ from mayamemo import MayaMemo, MayaMemoRetriever, MayaMemoMemory
72
+
73
+ retriever = MayaMemoRetriever(client, project["id"], tags=["checkout"])
74
+ docs = retriever.invoke("how do retries work?") # [{page_content, metadata}, …]
75
+
76
+ memory = MayaMemoMemory(client, project["id"]) # BaseMemory-compatible
77
+ memory.load_memory_variables({"input": "how do deploys work?"}) # {"mayamemo_context": "…"}
78
+ memory.save_context({"input": "…"}, {"output": "…"}) # recorded as a `discovery` memory
79
+ ```
80
+
81
+ `MayaMemoMemory.clear()` is a no-op by design — the MayaMemo ledger is append-only
82
+ (use retention windows or supersession; history is evidence).
83
+
84
+ ## Tests
85
+
86
+ Live tests run against a real API (they skip without a key):
87
+
88
+ ```bash
89
+ cd sdk/python
90
+ MAYAMEMO_TEST_KEY_FILE=/path/to/.ctxkey python -m pytest tests -q
91
+ ```
92
+
93
+ Each run creates its own project (`sdk-suite-<run>`) — semantic dedupe (0.92 cosine)
94
+ correctly treats near-identical statements as duplicates, so per-run isolation keeps
95
+ re-runs meaningful. Point the test key at a workspace with project-quota headroom
96
+ (the runs consume project slots by design). `examples/quickstart.py` is idempotent:
97
+ a duplicate is reported as `already captured — semantic dedupe` and reused.
98
+
99
+ ## Coverage
100
+
101
+ `projects` · `memories` (add / search / list / patch / tags) · `compile_context` ·
102
+ `checkpoints` (create / list / resume) · `events` · `public_status` · branded
103
+ `MayaMemoError(status, code, message, request_id)` on every non-2xx.
@@ -0,0 +1,83 @@
1
+ # MayaMemo — Python SDK
2
+
3
+ Zero-dependency Python client for the [MayaMemo](../..) API: projects, memories
4
+ (with tags), semantic search, the context compiler, checkpoints and cross-agent resume.
5
+
6
+ ```bash
7
+ # from the repo
8
+ pip install -e sdk/python # or: pip install -e "sdk/python[langchain]"
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from mayamemo import MayaMemo
15
+
16
+ client = MayaMemo(api_key="mm_sk_...", api_url="http://localhost:4000")
17
+
18
+ project = client.create_project("my-app")
19
+
20
+ client.add_memory(
21
+ project["id"],
22
+ "Checkout retries go through an idempotency table keyed by cart_id + attempt",
23
+ type="decision",
24
+ tags=["checkout", "billing"],
25
+ )
26
+
27
+ hits = client.search_memories(project["id"], "how do we avoid double charges?")
28
+ print(hits[0]["statement"], hits[0]["score"])
29
+
30
+ checkpoint = client.create_checkpoint(
31
+ project["id"], "billing groundwork",
32
+ "Idempotency decision captured; outbox worker noted.",
33
+ next_steps=["wire refund metrics"], agent="my-agent",
34
+ )
35
+
36
+ briefing = client.resume_checkpoint(checkpoint["id"], agent="another-agent")["briefing_markdown"]
37
+ print(briefing)
38
+ ```
39
+
40
+ Environment defaults: `MAYAMEMO_API_KEY` (or `MAYAMEMO_TOKEN`) and `MAYAMEMO_API_URL`
41
+ (`http://localhost:4000`). API keys are created in the workspace (`POST /v1/keys`),
42
+ scoped per workspace and revocable.
43
+
44
+ ## Framework adapters
45
+
46
+ `mayamemo.integrations` ships a LangChain-style retriever and chat-memory adapter.
47
+ They subclass the real `langchain-core` classes when it is installed (true drop-in)
48
+ and work standalone otherwise.
49
+
50
+ ```python
51
+ from mayamemo import MayaMemo, MayaMemoRetriever, MayaMemoMemory
52
+
53
+ retriever = MayaMemoRetriever(client, project["id"], tags=["checkout"])
54
+ docs = retriever.invoke("how do retries work?") # [{page_content, metadata}, …]
55
+
56
+ memory = MayaMemoMemory(client, project["id"]) # BaseMemory-compatible
57
+ memory.load_memory_variables({"input": "how do deploys work?"}) # {"mayamemo_context": "…"}
58
+ memory.save_context({"input": "…"}, {"output": "…"}) # recorded as a `discovery` memory
59
+ ```
60
+
61
+ `MayaMemoMemory.clear()` is a no-op by design — the MayaMemo ledger is append-only
62
+ (use retention windows or supersession; history is evidence).
63
+
64
+ ## Tests
65
+
66
+ Live tests run against a real API (they skip without a key):
67
+
68
+ ```bash
69
+ cd sdk/python
70
+ MAYAMEMO_TEST_KEY_FILE=/path/to/.ctxkey python -m pytest tests -q
71
+ ```
72
+
73
+ Each run creates its own project (`sdk-suite-<run>`) — semantic dedupe (0.92 cosine)
74
+ correctly treats near-identical statements as duplicates, so per-run isolation keeps
75
+ re-runs meaningful. Point the test key at a workspace with project-quota headroom
76
+ (the runs consume project slots by design). `examples/quickstart.py` is idempotent:
77
+ a duplicate is reported as `already captured — semantic dedupe` and reused.
78
+
79
+ ## Coverage
80
+
81
+ `projects` · `memories` (add / search / list / patch / tags) · `compile_context` ·
82
+ `checkpoints` (create / list / resume) · `events` · `public_status` · branded
83
+ `MayaMemoError(status, code, message, request_id)` on every non-2xx.
@@ -0,0 +1,7 @@
1
+ """MayaMemo Python SDK."""
2
+
3
+ from .client import MayaMemo, MayaMemoError
4
+ from .integrations import MayaMemoMemory, MayaMemoRetriever
5
+
6
+ __all__ = ["MayaMemo", "MayaMemoError", "MayaMemoMemory", "MayaMemoRetriever"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,284 @@
1
+ """MayaMemo Python SDK — thin, zero-dependency client for the MayaMemo API.
2
+
3
+ pip install -e sdk/python (or copy the `mayamemo/` package — no deps)
4
+
5
+ Quickstart:
6
+ from mayamemo import MayaMemo
7
+ client = MayaMemo(api_key="mm_sk_...", api_url="http://localhost:4000")
8
+ project = client.create_project("my-app")
9
+ client.add_memory(project["id"], "Retries go through an idempotency table", type="decision",
10
+ tags=["checkout"])
11
+ hits = client.search_memories(project["id"], "how do we avoid double charges?")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import urllib.error
19
+ import urllib.parse
20
+ import urllib.request
21
+ from typing import Any
22
+
23
+ __all__ = ["MayaMemo", "MayaMemoError"]
24
+
25
+ __version__ = "0.1.0"
26
+
27
+
28
+ class MayaMemoError(Exception):
29
+ """Branded API error: {status} {code}: {message} — request_id for support.
30
+
31
+ `details` carries any structured extras the API attached, e.g. a 409
32
+ DUPLICATE_MEMORY ships `{"duplicate_of": "<memory id>"}`.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ status: int,
38
+ code: str,
39
+ message: str,
40
+ request_id: str | None = None,
41
+ details: dict[str, Any] | None = None,
42
+ ):
43
+ super().__init__(f"{status} {code}: {message}")
44
+ self.status = status
45
+ self.code = code
46
+ self.message = message
47
+ self.request_id = request_id
48
+ self.details = details or {}
49
+
50
+ @property
51
+ def duplicate_of(self) -> str | None:
52
+ value = self.details.get("duplicate_of")
53
+ return value if isinstance(value, str) else None
54
+
55
+
56
+ class MayaMemo:
57
+ """Synchronous zero-dependency client. Auth: workspace API key (mm_sk_…)."""
58
+
59
+ def __init__(self, api_key: str | None = None, api_url: str | None = None, timeout: float = 60.0):
60
+ self.api_key = api_key or os.environ.get("MAYAMEMO_API_KEY") or os.environ.get("MAYAMEMO_TOKEN")
61
+ if not self.api_key:
62
+ raise ValueError("api_key is required (or set MAYAMEMO_API_KEY / MAYAMEMO_TOKEN)")
63
+ self.api_url = (api_url or os.environ.get("MAYAMEMO_API_URL") or "http://localhost:4000").rstrip("/")
64
+ self.timeout = timeout
65
+
66
+ # ------------------------------------------------------------------ HTTP
67
+
68
+ def _request(
69
+ self,
70
+ method: str,
71
+ path: str,
72
+ body: Any = None,
73
+ params: dict[str, Any] | None = None,
74
+ ) -> Any:
75
+ url = f"{self.api_url}{path}"
76
+ if params:
77
+ clean = {k: v for k, v in params.items() if v is not None}
78
+ if clean:
79
+ url += "?" + urllib.parse.urlencode(clean, doseq=True)
80
+ data = json.dumps(body).encode() if body is not None else None
81
+ req = urllib.request.Request(url, data=data, method=method)
82
+ req.add_header("Authorization", f"Bearer {self.api_key}")
83
+ req.add_header("Accept", "application/json")
84
+ if data is not None:
85
+ req.add_header("Content-Type", "application/json")
86
+ try:
87
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
88
+ raw = res.read().decode()
89
+ return json.loads(raw) if raw.strip() else None
90
+ except urllib.error.HTTPError as exc:
91
+ raw = exc.read().decode(errors="replace")
92
+ code, message, request_id, details = "HTTP_ERROR", raw[:300], None, {}
93
+ try:
94
+ err = dict((json.loads(raw) or {}).get("error") or {})
95
+ code = err.pop("code", code)
96
+ message = err.pop("message", message)
97
+ request_id = err.pop("request_id", None)
98
+ details = err
99
+ except ValueError:
100
+ pass
101
+ raise MayaMemoError(exc.code, code, message, request_id, details) from None
102
+
103
+ # -------------------------------------------------------------- projects
104
+
105
+ def list_projects(self) -> list[dict[str, Any]]:
106
+ return self._request("GET", "/v1/projects")
107
+
108
+ def create_project(self, name: str, description: str | None = None) -> dict[str, Any]:
109
+ body: dict[str, Any] = {"name": name}
110
+ if description is not None:
111
+ body["description"] = description
112
+ return self._request("POST", "/v1/projects", body)
113
+
114
+ # -------------------------------------------------------------- memories
115
+
116
+ def add_memory(
117
+ self,
118
+ project_id: str,
119
+ statement: str,
120
+ *,
121
+ type: str = "fact", # noqa: A002 - mirrors the API field name
122
+ tags: list[str] | None = None,
123
+ confidence: float | None = None,
124
+ importance: float | None = None,
125
+ authority: str | None = None,
126
+ source_session_id: str | None = None,
127
+ source_event_id: str | None = None,
128
+ entities: list[dict[str, Any]] | None = None,
129
+ ) -> dict[str, Any]:
130
+ body: dict[str, Any] = {"project_id": project_id, "statement": statement, "type": type}
131
+ for key, value in (
132
+ ("tags", tags),
133
+ ("confidence", confidence),
134
+ ("importance", importance),
135
+ ("authority", authority),
136
+ ("source_session_id", source_session_id),
137
+ ("source_event_id", source_event_id),
138
+ ("entities", entities),
139
+ ):
140
+ if value is not None:
141
+ body[key] = value
142
+ return self._request("POST", "/v1/memories", body)
143
+
144
+ def search_memories(
145
+ self,
146
+ project_id: str,
147
+ query: str,
148
+ *,
149
+ tags: list[str] | None = None,
150
+ limit: int = 10,
151
+ status: str | None = None,
152
+ ) -> list[dict[str, Any]]:
153
+ body: dict[str, Any] = {"project_id": project_id, "query": query, "limit": limit}
154
+ if tags:
155
+ body["tags"] = tags
156
+ if status:
157
+ body["status"] = status
158
+ return self._request("POST", "/v1/memories/search", body)
159
+
160
+ def list_memories(
161
+ self,
162
+ project_id: str,
163
+ *,
164
+ tag: str | None = None,
165
+ type: str | None = None, # noqa: A002
166
+ status: str | None = None,
167
+ limit: int | None = None,
168
+ ) -> list[dict[str, Any]]:
169
+ return self._request(
170
+ "GET",
171
+ "/v1/memories",
172
+ params={"project_id": project_id, "tag": tag, "type": type, "status": status, "limit": limit},
173
+ )
174
+
175
+ def patch_memory(self, memory_id: str, **changes: Any) -> dict[str, Any]:
176
+ return self._request("PATCH", f"/v1/memories/{memory_id}", changes)
177
+
178
+ # ----------------------------------------------------------- checkpoints
179
+
180
+ def create_checkpoint(
181
+ self,
182
+ project_id: str,
183
+ title: str,
184
+ summary: str,
185
+ *,
186
+ next_steps: list[str] | None = None,
187
+ blockers: list[str] | None = None,
188
+ agent: str | None = None,
189
+ session_id: str | None = None,
190
+ token_budget: int | None = None,
191
+ ) -> dict[str, Any]:
192
+ body: dict[str, Any] = {"project_id": project_id, "title": title, "summary": summary}
193
+ for key, value in (
194
+ ("next_steps", next_steps),
195
+ ("blockers", blockers),
196
+ ("agent", agent),
197
+ ("session_id", session_id),
198
+ ("token_budget", token_budget),
199
+ ):
200
+ if value is not None:
201
+ body[key] = value
202
+ return self._request("POST", "/v1/checkpoints", body)
203
+
204
+ def list_checkpoints(self, project_id: str) -> list[dict[str, Any]]:
205
+ data = self._request("GET", f"/v1/projects/{project_id}/checkpoints")
206
+ return (data or {}).get("checkpoints", [])
207
+
208
+ def resume_checkpoint(self, checkpoint_id: str, *, agent: str) -> dict[str, Any]:
209
+ """Resume as `agent` (any agent key). Returns checkpoint, resumed_session and briefing_markdown."""
210
+ return self._request("POST", f"/v1/checkpoints/{checkpoint_id}/resume", {"agent": agent})
211
+
212
+ # --------------------------------------------------------------- context
213
+
214
+ def compile_context(
215
+ self,
216
+ project_id: str,
217
+ title: str,
218
+ *,
219
+ description: str | None = None,
220
+ token_budget: int | None = None,
221
+ branch: str | None = None,
222
+ ) -> dict[str, Any]:
223
+ body: dict[str, Any] = {"project_id": project_id, "title": title}
224
+ for key, value in (("description", description), ("token_budget", token_budget), ("branch", branch)):
225
+ if value is not None:
226
+ body[key] = value
227
+ return self._request("POST", "/v1/context/compile", body)
228
+
229
+ # ----------------------------------------------------------------- graph
230
+
231
+ def observe_entities(
232
+ self,
233
+ project_id: str,
234
+ entities: list[dict[str, Any]],
235
+ relations: list[dict[str, Any]] | None = None,
236
+ ) -> dict[str, Any]:
237
+ """Observe code entities + relations (idempotent by kind+name).
238
+
239
+ entities: [{"kind": "file", "name": "src/cart.js"}, ...] (max 50)
240
+ relations: [{"source": {...}, "target": {...}, "kind": "imports"}, ...] (max 100)
241
+ """
242
+ body: dict[str, Any] = {"project_id": project_id, "entities": entities}
243
+ if relations:
244
+ body["relations"] = relations
245
+ return self._request("POST", "/v1/entities/observe", body)
246
+
247
+ def list_entities(
248
+ self,
249
+ project_id: str,
250
+ *,
251
+ kind: str | None = None,
252
+ q: str | None = None,
253
+ limit: int | None = None,
254
+ ) -> list[dict[str, Any]]:
255
+ data = self._request(
256
+ "GET",
257
+ f"/v1/projects/{project_id}/entities",
258
+ params={"kind": kind, "q": q, "limit": limit},
259
+ )
260
+ return (data or {}).get("entities", [])
261
+
262
+ def get_entity(self, entity_id: str) -> dict[str, Any]:
263
+ """Entity detail: relations (direction, other) + linked active memories."""
264
+ return self._request("GET", f"/v1/entities/{entity_id}")
265
+
266
+ def entity_neighbors(self, entity_id: str, *, depth: int = 1) -> dict[str, Any]:
267
+ """BFS over relations (depth 1..2): {root, nodes, edges, truncated}."""
268
+ return self._request("GET", f"/v1/entities/{entity_id}/neighbors", params={"depth": depth})
269
+
270
+ # ---------------------------------------------------------------- events
271
+
272
+ def list_events(self, project_id: str, *, limit: int = 50, cursor: int | None = None) -> dict[str, Any]:
273
+ return self._request(
274
+ "GET", f"/v1/projects/{project_id}/events", params={"limit": limit, "cursor": cursor}
275
+ )
276
+
277
+ # ----------------------------------------------------------------- misc
278
+
279
+ @classmethod
280
+ def public_status(cls, api_url: str | None = None, timeout: float = 15.0) -> dict[str, Any]:
281
+ """Public /v1/status — no API key required."""
282
+ url = (api_url or os.environ.get("MAYAMEMO_API_URL") or "http://localhost:4000").rstrip("/")
283
+ with urllib.request.urlopen(urllib.request.Request(f"{url}/v1/status"), timeout=timeout) as res:
284
+ return json.loads(res.read().decode())
@@ -0,0 +1,147 @@
1
+ """Framework adapters — LangChain-style retriever + chat memory.
2
+
3
+ Duck-typed by design: if `langchain-core` is installed these subclass the real
4
+ base classes (true drop-in); if not, they still work standalone and the module
5
+ imports without any dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from .client import MayaMemo
14
+
15
+ try: # optional — real LangChain integration when present
16
+ from langchain_core.memory import BaseMemory as _LCBaseMemory # type: ignore[import-not-found]
17
+ except Exception: # pragma: no cover - exercised implicitly everywhere langchain is absent
18
+ _LCBaseMemory = object # type: ignore[assignment,misc]
19
+
20
+ try:
21
+ from langchain_core.retrievers import BaseRetriever as _LCBaseRetriever # type: ignore[import-not-found]
22
+ except Exception: # pragma: no cover
23
+ _LCBaseRetriever = object # type: ignore[assignment,misc]
24
+
25
+ __all__ = ["MayaMemoRetriever", "MayaMemoMemory"]
26
+
27
+
28
+ class MayaMemoRetriever(_LCBaseRetriever): # type: ignore[misc]
29
+ """Retrieve project memories as documents for RAG-style prompt building."""
30
+
31
+ def __init__(self, client: MayaMemo, project_id: str, *, limit: int = 8, tags: list[str] | None = None):
32
+ self._client = client
33
+ self._project_id = project_id
34
+ self._limit = limit
35
+ self._tags = tags
36
+
37
+ def _to_document(self, hit: dict[str, Any]) -> dict[str, Any]:
38
+ return {
39
+ "page_content": hit.get("statement", ""),
40
+ "metadata": {
41
+ "id": hit.get("id"),
42
+ "type": hit.get("type"),
43
+ "tags": hit.get("tags", []),
44
+ "confidence": hit.get("confidence"),
45
+ "importance": hit.get("importance"),
46
+ "score": hit.get("score"),
47
+ "created_at": hit.get("createdAt"),
48
+ },
49
+ }
50
+
51
+ def invoke(self, query: Any, config: Any = None) -> list[dict[str, Any]]:
52
+ q = query if isinstance(query, str) else str((query or {}).get("query", ""))
53
+ return self._get_relevant_documents(q)
54
+
55
+ def _get_relevant_documents(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:
56
+ hits = self._client.search_memories(
57
+ self._project_id, query, limit=self._limit, tags=self._tags
58
+ )
59
+ return [self._to_document(hit) for hit in hits]
60
+
61
+ def get_relevant_documents(self, query: str) -> list[dict[str, Any]]: # legacy LangChain name
62
+ return self._get_relevant_documents(query)
63
+
64
+
65
+ class MayaMemoMemory(_LCBaseMemory): # type: ignore[misc]
66
+ """Chat-memory adapter: load compiles project context; save records the exchange.
67
+
68
+ The MayaMemo ledger is append-only by design — `clear()` cannot delete history;
69
+ it resets only local buffering (documented behavior, not a bug).
70
+ """
71
+
72
+ memory_variables: list[str] = ["mayamemo_context"]
73
+
74
+ def __init__(
75
+ self,
76
+ client: MayaMemo,
77
+ project_id: str,
78
+ *,
79
+ token_budget: int = 4000,
80
+ query_key: str = "input",
81
+ memory_type: str = "discovery",
82
+ tags: list[str] | None = None,
83
+ ):
84
+ self._client = client
85
+ self._project_id = project_id
86
+ self._token_budget = token_budget
87
+ self._query_key = query_key
88
+ self._memory_type = memory_type
89
+ self._tags = tags or ["langchain"]
90
+
91
+ # -- BaseMemory surface -------------------------------------------------
92
+
93
+ def load_memory_variables(self, inputs: dict[str, Any]) -> dict[str, Any]:
94
+ query = str(inputs.get(self._query_key) or inputs.get("question") or "").strip()
95
+ if not query:
96
+ return {"mayamemo_context": ""}
97
+ try:
98
+ compiled = self._client.compile_context(
99
+ self._project_id, query, token_budget=self._token_budget
100
+ )
101
+ except Exception: # never break the host chain on a context hiccup
102
+ return {"mayamemo_context": ""}
103
+ lines: list[str] = []
104
+ for item in compiled.get("items", []):
105
+ reason = item.get("reason") or {}
106
+ preview = reason.get("preview") or item.get("statement") or ""
107
+ if preview:
108
+ lines.append(f"- [{item.get('source_type', 'item')}] {preview}")
109
+ return {"mayamemo_context": "\n".join(lines)}
110
+
111
+ def save_context(self, inputs: dict[str, Any], outputs: dict[str, Any]) -> None:
112
+ statement = self._summarize(inputs, outputs)
113
+ if not statement:
114
+ return
115
+ try:
116
+ self._client.add_memory(
117
+ self._project_id,
118
+ statement[:2000],
119
+ type=self._memory_type,
120
+ tags=self._tags,
121
+ authority="model",
122
+ )
123
+ except Exception: # best-effort, like all memory capture
124
+ pass
125
+
126
+ def clear(self) -> None:
127
+ """No-op: the ledger is append-only. Kept for interface compatibility."""
128
+
129
+ # -- helpers ------------------------------------------------------------
130
+
131
+ @staticmethod
132
+ def _stringify(value: Any) -> str:
133
+ if isinstance(value, str):
134
+ return value
135
+ if isinstance(value, list):
136
+ return " / ".join(MayaMemoMemory._stringify(v) for v in value)
137
+ if isinstance(value, dict):
138
+ return json.dumps(value, ensure_ascii=False)
139
+ return str(value)
140
+
141
+ def _summarize(self, inputs: dict[str, Any], outputs: dict[str, Any]) -> str:
142
+ user = self._stringify(inputs.get(self._query_key) or inputs.get("question") or "")
143
+ reply = self._stringify(outputs.get("output") or outputs.get("answer") or outputs)
144
+ user, reply = user.strip(), reply.strip()
145
+ if not user and not reply:
146
+ return ""
147
+ return f"Exchange — user: {user[:400]} · assistant: {reply[:800]}"
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: mayamemo
3
+ Version: 0.1.0
4
+ Summary: MayaMemo Python SDK — evidence-backed context, memories and cross-agent checkpoints for AI coding agents. Zero dependencies.
5
+ Author: MayaMemo
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Coin1511/mayamemo
8
+ Project-URL: Repository, https://github.com/Coin1511/mayamemo/tree/main/sdk/python
9
+ Keywords: mayamemo,ai-agents,memory,context,checkpoints,mcp
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: langchain
18
+ Requires-Dist: langchain-core>=0.2; extra == "langchain"
19
+ Dynamic: license-file
20
+
21
+ # MayaMemo — Python SDK
22
+
23
+ Zero-dependency Python client for the [MayaMemo](../..) API: projects, memories
24
+ (with tags), semantic search, the context compiler, checkpoints and cross-agent resume.
25
+
26
+ ```bash
27
+ # from the repo
28
+ pip install -e sdk/python # or: pip install -e "sdk/python[langchain]"
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ from mayamemo import MayaMemo
35
+
36
+ client = MayaMemo(api_key="mm_sk_...", api_url="http://localhost:4000")
37
+
38
+ project = client.create_project("my-app")
39
+
40
+ client.add_memory(
41
+ project["id"],
42
+ "Checkout retries go through an idempotency table keyed by cart_id + attempt",
43
+ type="decision",
44
+ tags=["checkout", "billing"],
45
+ )
46
+
47
+ hits = client.search_memories(project["id"], "how do we avoid double charges?")
48
+ print(hits[0]["statement"], hits[0]["score"])
49
+
50
+ checkpoint = client.create_checkpoint(
51
+ project["id"], "billing groundwork",
52
+ "Idempotency decision captured; outbox worker noted.",
53
+ next_steps=["wire refund metrics"], agent="my-agent",
54
+ )
55
+
56
+ briefing = client.resume_checkpoint(checkpoint["id"], agent="another-agent")["briefing_markdown"]
57
+ print(briefing)
58
+ ```
59
+
60
+ Environment defaults: `MAYAMEMO_API_KEY` (or `MAYAMEMO_TOKEN`) and `MAYAMEMO_API_URL`
61
+ (`http://localhost:4000`). API keys are created in the workspace (`POST /v1/keys`),
62
+ scoped per workspace and revocable.
63
+
64
+ ## Framework adapters
65
+
66
+ `mayamemo.integrations` ships a LangChain-style retriever and chat-memory adapter.
67
+ They subclass the real `langchain-core` classes when it is installed (true drop-in)
68
+ and work standalone otherwise.
69
+
70
+ ```python
71
+ from mayamemo import MayaMemo, MayaMemoRetriever, MayaMemoMemory
72
+
73
+ retriever = MayaMemoRetriever(client, project["id"], tags=["checkout"])
74
+ docs = retriever.invoke("how do retries work?") # [{page_content, metadata}, …]
75
+
76
+ memory = MayaMemoMemory(client, project["id"]) # BaseMemory-compatible
77
+ memory.load_memory_variables({"input": "how do deploys work?"}) # {"mayamemo_context": "…"}
78
+ memory.save_context({"input": "…"}, {"output": "…"}) # recorded as a `discovery` memory
79
+ ```
80
+
81
+ `MayaMemoMemory.clear()` is a no-op by design — the MayaMemo ledger is append-only
82
+ (use retention windows or supersession; history is evidence).
83
+
84
+ ## Tests
85
+
86
+ Live tests run against a real API (they skip without a key):
87
+
88
+ ```bash
89
+ cd sdk/python
90
+ MAYAMEMO_TEST_KEY_FILE=/path/to/.ctxkey python -m pytest tests -q
91
+ ```
92
+
93
+ Each run creates its own project (`sdk-suite-<run>`) — semantic dedupe (0.92 cosine)
94
+ correctly treats near-identical statements as duplicates, so per-run isolation keeps
95
+ re-runs meaningful. Point the test key at a workspace with project-quota headroom
96
+ (the runs consume project slots by design). `examples/quickstart.py` is idempotent:
97
+ a duplicate is reported as `already captured — semantic dedupe` and reused.
98
+
99
+ ## Coverage
100
+
101
+ `projects` · `memories` (add / search / list / patch / tags) · `compile_context` ·
102
+ `checkpoints` (create / list / resume) · `events` · `public_status` · branded
103
+ `MayaMemoError(status, code, message, request_id)` on every non-2xx.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ mayamemo/__init__.py
5
+ mayamemo/client.py
6
+ mayamemo/integrations.py
7
+ mayamemo.egg-info/PKG-INFO
8
+ mayamemo.egg-info/SOURCES.txt
9
+ mayamemo.egg-info/dependency_links.txt
10
+ mayamemo.egg-info/requires.txt
11
+ mayamemo.egg-info/top_level.txt
12
+ tests/test_sdk.py
@@ -0,0 +1,3 @@
1
+
2
+ [langchain]
3
+ langchain-core>=0.2
@@ -0,0 +1 @@
1
+ mayamemo
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "mayamemo"
3
+ version = "0.1.0"
4
+ description = "MayaMemo Python SDK — evidence-backed context, memories and cross-agent checkpoints for AI coding agents. Zero dependencies."
5
+ requires-python = ">=3.10"
6
+ readme = "README.md"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "MayaMemo" }]
9
+ keywords = ["mayamemo", "ai-agents", "memory", "context", "checkpoints", "mcp"]
10
+ classifiers = [
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ langchain = ["langchain-core>=0.2"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Coin1511/mayamemo"
22
+ Repository = "https://github.com/Coin1511/mayamemo/tree/main/sdk/python"
23
+
24
+ [build-system]
25
+ requires = ["setuptools>=68"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [tool.setuptools]
29
+ packages = ["mayamemo"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,214 @@
1
+ """Live SDK tests — run against a real MayaMemo API.
2
+
3
+ MAYAMEMO_TEST_KEY=mm_sk_... (or MAYAMEMO_TEST_KEY_FILE=/path/to/key)
4
+ python -m pytest tests -q
5
+
6
+ Design notes:
7
+ - ONE find-or-create project per workspace ("sdk-suite-live"): live tests must not
8
+ create a project per test — the free plan's project quota (correctly) rejects that.
9
+ - Every run tags its memories with a unique `run-<hex>` tag, so count assertions are
10
+ stable across re-runs against the same project.
11
+ - Tests skip (not fail) when no key is configured, so the suite is CI-safe.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import uuid
18
+
19
+ import pytest
20
+
21
+ from mayamemo import MayaMemo, MayaMemoError
22
+
23
+
24
+ def _load_key() -> str | None:
25
+ key = os.environ.get("MAYAMEMO_TEST_KEY")
26
+ if key:
27
+ return key.strip()
28
+ path = os.environ.get("MAYAMEMO_TEST_KEY_FILE")
29
+ if path and os.path.exists(path):
30
+ with open(path, encoding="utf-8") as fh:
31
+ return fh.read().strip()
32
+ return None
33
+
34
+
35
+ KEY = _load_key()
36
+ pytestmark = pytest.mark.skipif(KEY is None, reason="no MAYAMEMO_TEST_KEY / _FILE configured")
37
+
38
+ RUN = uuid.uuid4().hex[:8]
39
+ RUN_TAG = f"run-{RUN}"
40
+
41
+ # Each run gets its own project. Reusing one project across runs fights semantic
42
+ # dedupe head-on (0.92 cosine correctly flags paraphrases), so instead every run
43
+ # is isolated — the test workspace just needs project quota headroom (see README).
44
+ PROJECT_NAME = f"sdk-suite-{RUN}"
45
+
46
+
47
+ @pytest.fixture(scope="module")
48
+ def client() -> MayaMemo:
49
+ return MayaMemo(api_key=KEY, api_url=os.environ.get("MAYAMEMO_API_URL", "http://localhost:4000"))
50
+
51
+
52
+ @pytest.fixture(scope="module")
53
+ def project(client: MayaMemo) -> dict:
54
+ try:
55
+ return client.create_project(PROJECT_NAME)
56
+ except MayaMemoError as exc:
57
+ if exc.status == 402:
58
+ raise RuntimeError(
59
+ "test workspace project quota is full — give the MAYAMEMO_TEST_KEY workspace "
60
+ "project headroom (live runs create one project per run by design)"
61
+ ) from exc
62
+ raise
63
+
64
+
65
+ def test_projects_and_memories_roundtrip(client: MayaMemo, project: dict) -> None:
66
+ created = client.add_memory(
67
+ project["id"],
68
+ "Checkout retries go through an idempotency table keyed by cart_id + attempt",
69
+ type="decision",
70
+ tags=[RUN_TAG, "SDK", "checkout"],
71
+ confidence=0.9,
72
+ importance=0.8,
73
+ )
74
+ assert created["tags"] == [RUN_TAG, "sdk", "checkout"], "tags must lowercase + dedupe, order-preserving"
75
+ assert created["confidence"] == pytest.approx(0.9)
76
+
77
+ # semantic search: different wording, no shared keywords
78
+ hits = client.search_memories(project["id"], "how do we avoid double charging customers?", tags=[RUN_TAG])
79
+ assert len(hits) == 1
80
+ assert "idempotency" in hits[0]["statement"]
81
+ assert hits[0].get("score") is not None
82
+
83
+ # tag-filtered variants
84
+ assert len(client.list_memories(project["id"], tag=RUN_TAG)) == 1
85
+ assert len(client.list_memories(project["id"], tag="definitely-not-a-tag")) == 0
86
+ assert len(client.search_memories(project["id"], "retry strategy", tags=[RUN_TAG])) == 1
87
+ assert len(client.search_memories(project["id"], "retry strategy", tags=["other-not-a-tag"])) == 0
88
+
89
+ # patch replaces tags
90
+ patched = client.patch_memory(created["id"], tags=["release"])
91
+ assert patched["tags"] == ["release"]
92
+
93
+
94
+ def test_checkpoint_create_list_resume(client: MayaMemo, project: dict) -> None:
95
+ client.add_memory(project["id"], "Auth uses refresh-token rotation", type="architecture")
96
+ checkpoint = client.create_checkpoint(
97
+ project["id"],
98
+ f"sdk handoff {RUN}",
99
+ "SDK created memories and a checkpoint end-to-end",
100
+ next_steps=["resume with another agent"],
101
+ agent="python-sdk",
102
+ )
103
+ assert checkpoint["status"] == "ACTIVE"
104
+
105
+ listed = client.list_checkpoints(project["id"])
106
+ assert any(c["id"] == checkpoint["id"] for c in listed)
107
+
108
+ resumed = client.resume_checkpoint(checkpoint["id"], agent="langchain-adapter")
109
+ assert resumed["resumed_session"]["agent"] == "langchain-adapter"
110
+ briefing = resumed["briefing_markdown"]
111
+ assert f"sdk handoff {RUN}" in briefing and "Next steps" in briefing
112
+
113
+
114
+ def test_compile_context_and_events(client: MayaMemo, project: dict) -> None:
115
+ client.add_memory(project["id"], "The billing worker runs every 30 seconds", type="fact")
116
+ compiled = client.compile_context(project["id"], f"billing worker cadence {RUN}", token_budget=2000)
117
+ assert compiled["items"], "compiler should surface at least one item"
118
+ assert compiled["token_count"] >= 0
119
+
120
+ events = client.list_events(project["id"], limit=30)
121
+ types = [e["event_type"] for e in events["events"]]
122
+ assert "project.created" in types
123
+ assert "memory.created" in types
124
+ assert "context.compiled" in types
125
+
126
+
127
+ def test_semantic_dedupe_surfaces_duplicate_of(client: MayaMemo, project: dict) -> None:
128
+ first = client.add_memory(project["id"], "Sessions expire after thirty minutes of inactivity", type="fact")
129
+ with pytest.raises(MayaMemoError) as excinfo:
130
+ client.add_memory(
131
+ project["id"],
132
+ "Sessions expire after thirty minutes of idle time", # near-identical wording
133
+ type="fact",
134
+ )
135
+ assert excinfo.value.status == 409
136
+ assert excinfo.value.code == "DUPLICATE_MEMORY"
137
+ assert excinfo.value.duplicate_of == first["id"]
138
+
139
+
140
+ def test_graph_entities_relations_and_memory_links(client: MayaMemo, project: dict) -> None:
141
+ observed = client.observe_entities(
142
+ project["id"],
143
+ [
144
+ {"kind": "file", "name": "sdk/graph_a.py"},
145
+ {"kind": "file", "name": "sdk/graph_b.py"},
146
+ ],
147
+ relations=[
148
+ {
149
+ "source": {"kind": "file", "name": "sdk/graph_a.py"},
150
+ "target": {"kind": "file", "name": "sdk/graph_b.py"},
151
+ "kind": "imports",
152
+ }
153
+ ],
154
+ )
155
+ assert observed["entity_count"] == 2 and observed["relation_count"] == 1
156
+
157
+ listed = client.list_entities(project["id"], q="sdk/graph")
158
+ assert len(listed) == 2
159
+ a = next(e for e in listed if e["name"].endswith("graph_a.py"))
160
+
161
+ detail = client.get_entity(a["id"])
162
+ assert any(r["other"]["name"].endswith("graph_b.py") for r in detail["relations"])
163
+
164
+ neighbors = client.entity_neighbors(a["id"], depth=1)
165
+ assert {n["name"] for n in neighbors["nodes"]} == {"sdk/graph_a.py", "sdk/graph_b.py"}
166
+
167
+ memory = client.add_memory(
168
+ project["id"],
169
+ "SDK graph proof: graph_a imports graph_b for the pipeline",
170
+ type="fact",
171
+ entities=[{"kind": "file", "name": "sdk/graph_a.py"}],
172
+ )
173
+ detail2 = client.get_entity(a["id"])
174
+ assert any(m["id"] == memory["id"] for m in detail2["memories"])
175
+
176
+
177
+ def test_errors_are_branded(client: MayaMemo) -> None:
178
+ bad = MayaMemo(api_key="mm_sk_thisisnotarealkey000000000000000000", api_url=client.api_url)
179
+ with pytest.raises(MayaMemoError) as excinfo:
180
+ bad.list_projects()
181
+ assert excinfo.value.status == 401
182
+ assert excinfo.value.code == "UNAUTHENTICATED"
183
+
184
+ with pytest.raises(MayaMemoError) as excinfo:
185
+ client.list_memories("00000000-0000-4000-8000-000000000000")
186
+ assert excinfo.value.status in (404, 400)
187
+
188
+
189
+ def test_retriever_and_memory_adapters(client: MayaMemo, project: dict) -> None:
190
+ from mayamemo import MayaMemoMemory, MayaMemoRetriever
191
+
192
+ client.add_memory(
193
+ project["id"],
194
+ "Deploys are blue-green via the proxy swap",
195
+ type="architecture",
196
+ tags=[RUN_TAG, "deploy"],
197
+ )
198
+
199
+ retriever = MayaMemoRetriever(client, project["id"], limit=5, tags=[RUN_TAG])
200
+ docs = retriever.invoke("how do releases go out?")
201
+ assert docs and "page_content" in docs[0] and "metadata" in docs[0]
202
+
203
+ memory = MayaMemoMemory(client, project["id"])
204
+ variables = memory.load_memory_variables({"input": f"how are deploys performed {RUN}?"})
205
+ assert isinstance(variables["mayamemo_context"], str)
206
+ assert "blue-green" in variables["mayamemo_context"]
207
+
208
+ memory.save_context(
209
+ {"input": "which proxy swaps traffic during an incident?"},
210
+ {"output": "the edge proxy does it"},
211
+ )
212
+ stored = client.search_memories(project["id"], "edge proxy swaps traffic incident")
213
+ assert any(m.get("type") == "discovery" for m in stored)
214
+ memory.clear() # no-op by design