mayamemo 0.1.0__py3-none-any.whl
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/__init__.py +7 -0
- mayamemo/client.py +284 -0
- mayamemo/integrations.py +147 -0
- mayamemo-0.1.0.dist-info/METADATA +103 -0
- mayamemo-0.1.0.dist-info/RECORD +8 -0
- mayamemo-0.1.0.dist-info/WHEEL +5 -0
- mayamemo-0.1.0.dist-info/licenses/LICENSE +21 -0
- mayamemo-0.1.0.dist-info/top_level.txt +1 -0
mayamemo/__init__.py
ADDED
mayamemo/client.py
ADDED
|
@@ -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())
|
mayamemo/integrations.py
ADDED
|
@@ -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,8 @@
|
|
|
1
|
+
mayamemo/__init__.py,sha256=R_tDdarPEl2-TKTbG8zYS_t-YNi-51j9B3yABKjj5kY,234
|
|
2
|
+
mayamemo/client.py,sha256=5KBQDwpjJ2vjLVhJgKFLy4sUWrBK_nDWsA_ZputDIu0,10896
|
|
3
|
+
mayamemo/integrations.py,sha256=aIrUitXqvyUw7-BP3sS7CQhFss_8AbXYrDLw_Nsji9Q,5761
|
|
4
|
+
mayamemo-0.1.0.dist-info/licenses/LICENSE,sha256=aqJQLMTP-zx3yi6Is0mwUPdhtiOASiamQjbzWMoA7s4,1065
|
|
5
|
+
mayamemo-0.1.0.dist-info/METADATA,sha256=agMNPiUECaZJoSKRrOgH34vJ_x1lzoBZRscc_3ZkqDU,3926
|
|
6
|
+
mayamemo-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
mayamemo-0.1.0.dist-info/top_level.txt,sha256=JMvWa37JFkZloMR7HKZ-LpygTv6vPWJQtg-Rzmr1gZA,9
|
|
8
|
+
mayamemo-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
mayamemo
|