mem-os-python 1.6.27__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.
Files changed (253) hide show
  1. mem_os_python-1.6.27.dist-info/METADATA +1032 -0
  2. mem_os_python-1.6.27.dist-info/RECORD +253 -0
  3. mem_os_python-1.6.27.dist-info/WHEEL +4 -0
  4. mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
  5. mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
  6. memos/__init__.py +16 -0
  7. memos/_js/adapters/anthropic.d.ts +279 -0
  8. memos/_js/adapters/anthropic.d.ts.map +1 -0
  9. memos/_js/adapters/anthropic.js +348 -0
  10. memos/_js/adapters/anthropic.js.map +1 -0
  11. memos/_js/adapters/base.d.ts +10 -0
  12. memos/_js/adapters/base.d.ts.map +1 -0
  13. memos/_js/adapters/base.js +2 -0
  14. memos/_js/adapters/base.js.map +1 -0
  15. memos/_js/adapters/openai.d.ts +264 -0
  16. memos/_js/adapters/openai.d.ts.map +1 -0
  17. memos/_js/adapters/openai.js +342 -0
  18. memos/_js/adapters/openai.js.map +1 -0
  19. memos/_js/apps/explorer.d.ts +244 -0
  20. memos/_js/apps/explorer.d.ts.map +1 -0
  21. memos/_js/apps/explorer.js +738 -0
  22. memos/_js/apps/explorer.js.map +1 -0
  23. memos/_js/citations.d.ts +65 -0
  24. memos/_js/citations.d.ts.map +1 -0
  25. memos/_js/citations.js +143 -0
  26. memos/_js/citations.js.map +1 -0
  27. memos/_js/cli-browse.d.ts +3 -0
  28. memos/_js/cli-browse.d.ts.map +1 -0
  29. memos/_js/cli-browse.js +129 -0
  30. memos/_js/cli-browse.js.map +1 -0
  31. memos/_js/cli-completion.d.ts +31 -0
  32. memos/_js/cli-completion.d.ts.map +1 -0
  33. memos/_js/cli-completion.js +340 -0
  34. memos/_js/cli-completion.js.map +1 -0
  35. memos/_js/cli-help.d.ts +18 -0
  36. memos/_js/cli-help.d.ts.map +1 -0
  37. memos/_js/cli-help.js +357 -0
  38. memos/_js/cli-help.js.map +1 -0
  39. memos/_js/cli.d.ts +28 -0
  40. memos/_js/cli.d.ts.map +1 -0
  41. memos/_js/cli.js +2880 -0
  42. memos/_js/cli.js.map +1 -0
  43. memos/_js/compression.d.ts +25 -0
  44. memos/_js/compression.d.ts.map +1 -0
  45. memos/_js/compression.js +319 -0
  46. memos/_js/compression.js.map +1 -0
  47. memos/_js/confidence-machine.d.ts +99 -0
  48. memos/_js/confidence-machine.d.ts.map +1 -0
  49. memos/_js/confidence-machine.js +222 -0
  50. memos/_js/confidence-machine.js.map +1 -0
  51. memos/_js/context-pack.d.ts +506 -0
  52. memos/_js/context-pack.d.ts.map +1 -0
  53. memos/_js/context-pack.js +1435 -0
  54. memos/_js/context-pack.js.map +1 -0
  55. memos/_js/contradictions.d.ts +160 -0
  56. memos/_js/contradictions.d.ts.map +1 -0
  57. memos/_js/contradictions.js +255 -0
  58. memos/_js/contradictions.js.map +1 -0
  59. memos/_js/dashboard.d.ts +30 -0
  60. memos/_js/dashboard.d.ts.map +1 -0
  61. memos/_js/dashboard.js +302 -0
  62. memos/_js/dashboard.js.map +1 -0
  63. memos/_js/ebbinghaus.d.ts +29 -0
  64. memos/_js/ebbinghaus.d.ts.map +1 -0
  65. memos/_js/ebbinghaus.js +32 -0
  66. memos/_js/ebbinghaus.js.map +1 -0
  67. memos/_js/embedding-queue.d.ts +190 -0
  68. memos/_js/embedding-queue.d.ts.map +1 -0
  69. memos/_js/embedding-queue.js +316 -0
  70. memos/_js/embedding-queue.js.map +1 -0
  71. memos/_js/embeddings.d.ts +187 -0
  72. memos/_js/embeddings.d.ts.map +1 -0
  73. memos/_js/embeddings.js +633 -0
  74. memos/_js/embeddings.js.map +1 -0
  75. memos/_js/entity-extraction.d.ts +54 -0
  76. memos/_js/entity-extraction.d.ts.map +1 -0
  77. memos/_js/entity-extraction.js +325 -0
  78. memos/_js/entity-extraction.js.map +1 -0
  79. memos/_js/entity-index.d.ts +93 -0
  80. memos/_js/entity-index.d.ts.map +1 -0
  81. memos/_js/entity-index.js +165 -0
  82. memos/_js/entity-index.js.map +1 -0
  83. memos/_js/event-memory.d.ts +147 -0
  84. memos/_js/event-memory.d.ts.map +1 -0
  85. memos/_js/event-memory.js +290 -0
  86. memos/_js/event-memory.js.map +1 -0
  87. memos/_js/external-import.d.ts +98 -0
  88. memos/_js/external-import.d.ts.map +1 -0
  89. memos/_js/external-import.js +669 -0
  90. memos/_js/external-import.js.map +1 -0
  91. memos/_js/fidelity.d.ts +367 -0
  92. memos/_js/fidelity.d.ts.map +1 -0
  93. memos/_js/fidelity.js +911 -0
  94. memos/_js/fidelity.js.map +1 -0
  95. memos/_js/graph-expansion.d.ts +119 -0
  96. memos/_js/graph-expansion.d.ts.map +1 -0
  97. memos/_js/graph-expansion.js +213 -0
  98. memos/_js/graph-expansion.js.map +1 -0
  99. memos/_js/graph-mermaid.d.ts +13 -0
  100. memos/_js/graph-mermaid.d.ts.map +1 -0
  101. memos/_js/graph-mermaid.js +32 -0
  102. memos/_js/graph-mermaid.js.map +1 -0
  103. memos/_js/graph.d.ts +170 -0
  104. memos/_js/graph.d.ts.map +1 -0
  105. memos/_js/graph.js +482 -0
  106. memos/_js/graph.js.map +1 -0
  107. memos/_js/harness.d.ts +63 -0
  108. memos/_js/harness.d.ts.map +1 -0
  109. memos/_js/harness.js +93 -0
  110. memos/_js/harness.js.map +1 -0
  111. memos/_js/importance.d.ts +118 -0
  112. memos/_js/importance.d.ts.map +1 -0
  113. memos/_js/importance.js +144 -0
  114. memos/_js/importance.js.map +1 -0
  115. memos/_js/index.d.ts +51 -0
  116. memos/_js/index.d.ts.map +1 -0
  117. memos/_js/index.js +53 -0
  118. memos/_js/index.js.map +1 -0
  119. memos/_js/mcp.d.ts +58 -0
  120. memos/_js/mcp.d.ts.map +1 -0
  121. memos/_js/mcp.js +1853 -0
  122. memos/_js/mcp.js.map +1 -0
  123. memos/_js/memory.d.ts +1762 -0
  124. memos/_js/memory.d.ts.map +1 -0
  125. memos/_js/memory.js +6283 -0
  126. memos/_js/memory.js.map +1 -0
  127. memos/_js/procedural.d.ts +74 -0
  128. memos/_js/procedural.d.ts.map +1 -0
  129. memos/_js/procedural.js +125 -0
  130. memos/_js/procedural.js.map +1 -0
  131. memos/_js/project-config.d.ts +68 -0
  132. memos/_js/project-config.d.ts.map +1 -0
  133. memos/_js/project-config.js +59 -0
  134. memos/_js/project-config.js.map +1 -0
  135. memos/_js/proof-of-forgetting.d.ts +57 -0
  136. memos/_js/proof-of-forgetting.d.ts.map +1 -0
  137. memos/_js/proof-of-forgetting.js +80 -0
  138. memos/_js/proof-of-forgetting.js.map +1 -0
  139. memos/_js/provenance.d.ts +122 -0
  140. memos/_js/provenance.d.ts.map +1 -0
  141. memos/_js/provenance.js +182 -0
  142. memos/_js/provenance.js.map +1 -0
  143. memos/_js/quarantine-commands.d.ts +23 -0
  144. memos/_js/quarantine-commands.d.ts.map +1 -0
  145. memos/_js/quarantine-commands.js +42 -0
  146. memos/_js/quarantine-commands.js.map +1 -0
  147. memos/_js/quarantine.d.ts +120 -0
  148. memos/_js/quarantine.d.ts.map +1 -0
  149. memos/_js/quarantine.js +873 -0
  150. memos/_js/quarantine.js.map +1 -0
  151. memos/_js/redact.d.ts +12 -0
  152. memos/_js/redact.d.ts.map +1 -0
  153. memos/_js/redact.js +26 -0
  154. memos/_js/redact.js.map +1 -0
  155. memos/_js/repl.d.ts +57 -0
  156. memos/_js/repl.d.ts.map +1 -0
  157. memos/_js/repl.js +295 -0
  158. memos/_js/repl.js.map +1 -0
  159. memos/_js/rerank-local.d.ts +61 -0
  160. memos/_js/rerank-local.d.ts.map +1 -0
  161. memos/_js/rerank-local.js +210 -0
  162. memos/_js/rerank-local.js.map +1 -0
  163. memos/_js/retain-filter.d.ts +98 -0
  164. memos/_js/retain-filter.d.ts.map +1 -0
  165. memos/_js/retain-filter.js +232 -0
  166. memos/_js/retain-filter.js.map +1 -0
  167. memos/_js/retrieval.d.ts +265 -0
  168. memos/_js/retrieval.d.ts.map +1 -0
  169. memos/_js/retrieval.js +1018 -0
  170. memos/_js/retrieval.js.map +1 -0
  171. memos/_js/revert.d.ts +77 -0
  172. memos/_js/revert.d.ts.map +1 -0
  173. memos/_js/revert.js +252 -0
  174. memos/_js/revert.js.map +1 -0
  175. memos/_js/scope.d.ts +30 -0
  176. memos/_js/scope.d.ts.map +1 -0
  177. memos/_js/scope.js +71 -0
  178. memos/_js/scope.js.map +1 -0
  179. memos/_js/semantic-screen.d.ts +152 -0
  180. memos/_js/semantic-screen.d.ts.map +1 -0
  181. memos/_js/semantic-screen.js +290 -0
  182. memos/_js/semantic-screen.js.map +1 -0
  183. memos/_js/session-hooks.d.ts +127 -0
  184. memos/_js/session-hooks.d.ts.map +1 -0
  185. memos/_js/session-hooks.js +482 -0
  186. memos/_js/session-hooks.js.map +1 -0
  187. memos/_js/storage/sqlite.d.ts +427 -0
  188. memos/_js/storage/sqlite.d.ts.map +1 -0
  189. memos/_js/storage/sqlite.js +2428 -0
  190. memos/_js/storage/sqlite.js.map +1 -0
  191. memos/_js/sync.d.ts +213 -0
  192. memos/_js/sync.d.ts.map +1 -0
  193. memos/_js/sync.js +389 -0
  194. memos/_js/sync.js.map +1 -0
  195. memos/_js/tamper-log.d.ts +98 -0
  196. memos/_js/tamper-log.d.ts.map +1 -0
  197. memos/_js/tamper-log.js +327 -0
  198. memos/_js/tamper-log.js.map +1 -0
  199. memos/_js/temporal.d.ts +92 -0
  200. memos/_js/temporal.d.ts.map +1 -0
  201. memos/_js/temporal.js +536 -0
  202. memos/_js/temporal.js.map +1 -0
  203. memos/_js/time-travel.d.ts +38 -0
  204. memos/_js/time-travel.d.ts.map +1 -0
  205. memos/_js/time-travel.js +46 -0
  206. memos/_js/time-travel.js.map +1 -0
  207. memos/_js/timestamp-format.d.ts +18 -0
  208. memos/_js/timestamp-format.d.ts.map +1 -0
  209. memos/_js/timestamp-format.js +52 -0
  210. memos/_js/timestamp-format.js.map +1 -0
  211. memos/_js/triple-extraction.d.ts +31 -0
  212. memos/_js/triple-extraction.d.ts.map +1 -0
  213. memos/_js/triple-extraction.js +116 -0
  214. memos/_js/triple-extraction.js.map +1 -0
  215. memos/_js/types.d.ts +2474 -0
  216. memos/_js/types.d.ts.map +1 -0
  217. memos/_js/types.js +20 -0
  218. memos/_js/types.js.map +1 -0
  219. memos/_js/updater.d.ts +95 -0
  220. memos/_js/updater.d.ts.map +1 -0
  221. memos/_js/updater.js +268 -0
  222. memos/_js/updater.js.map +1 -0
  223. memos/_js/version.d.ts +17 -0
  224. memos/_js/version.d.ts.map +1 -0
  225. memos/_js/version.js +34 -0
  226. memos/_js/version.js.map +1 -0
  227. memos/_js/vision/clip-query-cache.d.ts +58 -0
  228. memos/_js/vision/clip-query-cache.d.ts.map +1 -0
  229. memos/_js/vision/clip-query-cache.js +79 -0
  230. memos/_js/vision/clip-query-cache.js.map +1 -0
  231. memos/_js/vision/clip.d.ts +113 -0
  232. memos/_js/vision/clip.d.ts.map +1 -0
  233. memos/_js/vision/clip.js +224 -0
  234. memos/_js/vision/clip.js.map +1 -0
  235. memos/_js/vision/fake-clip.d.ts +55 -0
  236. memos/_js/vision/fake-clip.d.ts.map +1 -0
  237. memos/_js/vision/fake-clip.js +122 -0
  238. memos/_js/vision/fake-clip.js.map +1 -0
  239. memos/_js/vision/index.d.ts +15 -0
  240. memos/_js/vision/index.d.ts.map +1 -0
  241. memos/_js/vision/index.js +13 -0
  242. memos/_js/vision/index.js.map +1 -0
  243. memos/_js/vision/store.d.ts +137 -0
  244. memos/_js/vision/store.d.ts.map +1 -0
  245. memos/_js/vision/store.js +383 -0
  246. memos/_js/vision/store.js.map +1 -0
  247. memos/adapters/__init__.py +0 -0
  248. memos/adapters/crewai.py +385 -0
  249. memos/adapters/langchain.py +301 -0
  250. memos/adapters/ollama.py +396 -0
  251. memos/server/__init__.py +0 -0
  252. memos/server/main.py +547 -0
  253. memos/server/routes.py +360 -0
memos/server/routes.py ADDED
@@ -0,0 +1,360 @@
1
+ """
2
+ MemOS REST API routes.
3
+
4
+ All endpoints are mounted under `/api/mem` and communicate with
5
+ the Node.js MemOS subprocess via JSON-RPC over stdin/stdout.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import json
12
+ import os
13
+ import zipfile
14
+ from typing import Any
15
+
16
+ from fastapi import APIRouter, HTTPException, Query
17
+ from fastapi.responses import StreamingResponse
18
+ from pydantic import BaseModel, Field
19
+
20
+ router = APIRouter()
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Request / Response Models
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ class StoreRequest(BaseModel):
29
+ """Request body for storing a new memory."""
30
+
31
+ content: str = Field(..., min_length=1, description="Text content to remember.")
32
+ type: str = Field(
33
+ "fact",
34
+ description="Memory type: fact, preference, context, relationship, entity, custom.",
35
+ )
36
+ metadata: dict[str, Any] = Field(default_factory=dict, description="Arbitrary metadata.")
37
+ summary: str | None = Field(None, description="Optional summary. Auto-generated if omitted.")
38
+ importance: float = Field(0.5, ge=0.0, le=1.0, description="Importance score [0, 1].")
39
+ ttl: int | None = Field(None, description="TTL in seconds.")
40
+ tags: list[str] = Field(default_factory=list, description="Custom tags.")
41
+
42
+
43
+ class SearchRequest(BaseModel):
44
+ """Request body for searching memories."""
45
+
46
+ query: str | None = Field(None, description="Full-text search query.")
47
+ type: str | None = Field(None, description="Filter by memory type.")
48
+ min_importance: float | None = Field(None, ge=0.0, le=1.0)
49
+ max_importance: float | None = Field(None, ge=0.0, le=1.0)
50
+ metadata: dict[str, Any] | None = Field(None, description="Metadata filter.")
51
+ limit: int = Field(20, ge=1, le=1000)
52
+ offset: int = Field(0, ge=0)
53
+ sort_by: str = Field("updated_at", description="Sort field.")
54
+ sort_order: str = Field("desc", description="Sort order: asc or desc.")
55
+ tags: list[str] = Field(default_factory=list, description="Filter by tags (AND logic).")
56
+
57
+
58
+ class LinkRequest(BaseModel):
59
+ """Request body for creating a manual link between memories."""
60
+
61
+ source_id: str = Field(..., description="Source node ID.")
62
+ target_id: str = Field(..., description="Target node ID.")
63
+ relation: str = Field("relates_to", description="Edge relation type.")
64
+ weight: float = Field(0.5, ge=0.0, le=1.0)
65
+
66
+
67
+ class UpdateRequest(BaseModel):
68
+ """Request body for updating an existing memory."""
69
+
70
+ content: str | None = None
71
+ summary: str | None = None
72
+ type: str | None = None
73
+ metadata: dict[str, Any] | None = None
74
+ importance: float | None = Field(None, ge=0.0, le=1.0)
75
+ tags: list[str] | None = None
76
+
77
+
78
+ class ForgetRequest(BaseModel):
79
+ """Request body for forgetting (deleting) a memory."""
80
+
81
+ id: str = Field(..., description="Memory node ID to forget.")
82
+
83
+
84
+ class TtlRequest(BaseModel):
85
+ """Request body for setting TTL."""
86
+
87
+ seconds: int = Field(..., ge=1, description="TTL in seconds.")
88
+
89
+
90
+ class TagsRequest(BaseModel):
91
+ """Request body for adding tags."""
92
+
93
+ tags: list[str] = Field(..., min_length=1, description="Tags to add.")
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Bridge communication
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def _get_bridge():
102
+ """Get the Node.js bridge subprocess from the app state."""
103
+ from .main import _node_process
104
+
105
+ if _node_process is None or _node_process.poll() is not None:
106
+ raise HTTPException(status_code=503, detail="MemOS bridge process not running.")
107
+ return _node_process
108
+
109
+
110
+ def _rpc_call(method: str, params: dict[str, Any] | None = None) -> Any:
111
+ """Send a JSON-RPC call to the Node.js bridge and return the result."""
112
+ import uuid as _uuid
113
+
114
+ bridge = _get_bridge()
115
+ msg_id = str(_uuid.uuid4())
116
+ payload = json.dumps({"id": msg_id, "method": method, "params": params or {}}) + "\n"
117
+
118
+ try:
119
+ bridge.stdin.write(payload)
120
+ bridge.stdin.flush()
121
+
122
+ line = bridge.stdout.readline()
123
+ if not line:
124
+ raise HTTPException(status_code=502, detail="No response from MemOS bridge.")
125
+
126
+ response = json.loads(line.strip())
127
+
128
+ if "error" in response and response["error"]:
129
+ raise HTTPException(status_code=400, detail=response["error"])
130
+
131
+ return response.get("result")
132
+ except BrokenPipeError as err:
133
+ raise HTTPException(status_code=503, detail="MemOS bridge process crashed.") from err
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Endpoints
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ @router.post("/store", summary="Store a new memory")
142
+ async def store_memory(req: StoreRequest):
143
+ """
144
+ Store a new memory node. Automatically generates a summary
145
+ and links related memories when auto-linking is enabled.
146
+ """
147
+ opts: dict[str, Any] = {
148
+ "type": req.type,
149
+ "metadata": req.metadata,
150
+ "importance": req.importance,
151
+ }
152
+ if req.summary:
153
+ opts["summary"] = req.summary
154
+ if req.ttl is not None:
155
+ opts["ttl"] = req.ttl
156
+ if req.tags:
157
+ opts["tags"] = req.tags
158
+
159
+ result = _rpc_call("store", {"content": req.content, "opts": opts})
160
+ return result
161
+
162
+
163
+ @router.post("/retrieve", summary="Retrieve a memory by ID")
164
+ async def retrieve_memory(body: dict[str, str]):
165
+ """Retrieve a single memory node by its unique ID."""
166
+ node_id = body.get("id")
167
+ if not node_id:
168
+ raise HTTPException(status_code=400, detail="Missing 'id' field.")
169
+ result = _rpc_call("retrieve", {"id": node_id})
170
+ if result is None:
171
+ raise HTTPException(status_code=404, detail=f"Memory not found: {node_id}")
172
+ return result
173
+
174
+
175
+ @router.post("/search", summary="Search memories")
176
+ async def search_memories(req: SearchRequest):
177
+ """Search memories by text query and/or structured filters."""
178
+ filter_obj: dict[str, Any] = {
179
+ "limit": req.limit,
180
+ "offset": req.offset,
181
+ "sortBy": req.sort_by,
182
+ "sortOrder": req.sort_order,
183
+ }
184
+ if req.query:
185
+ filter_obj["query"] = req.query
186
+ if req.type:
187
+ filter_obj["type"] = req.type
188
+ if req.min_importance is not None:
189
+ filter_obj["minImportance"] = req.min_importance
190
+ if req.max_importance is not None:
191
+ filter_obj["maxImportance"] = req.max_importance
192
+ if req.metadata:
193
+ filter_obj["metadata"] = req.metadata
194
+ if req.tags:
195
+ filter_obj["tags"] = req.tags
196
+
197
+ result = _rpc_call("search", {"filter": filter_obj})
198
+ return result
199
+
200
+
201
+ @router.post("/forget", summary="Forget (delete) a memory")
202
+ async def forget_memory(req: ForgetRequest):
203
+ """Permanently remove a memory and all its connected edges."""
204
+ result = _rpc_call("forget", {"id": req.id})
205
+ if not result:
206
+ raise HTTPException(status_code=404, detail=f"Memory not found: {req.id}")
207
+ return {"deleted": True, "id": req.id}
208
+
209
+
210
+ @router.post("/summarize", summary="Summarize all memories")
211
+ async def summarize_memories():
212
+ """Generate an extractive summary of all stored memories."""
213
+ result = _rpc_call("summarize")
214
+ return {"summary": result}
215
+
216
+
217
+ @router.post("/link", summary="Link two memories")
218
+ async def link_memories(req: LinkRequest):
219
+ """Manually create a link (edge) between two memory nodes."""
220
+ result = _rpc_call(
221
+ "link",
222
+ {
223
+ "sourceId": req.source_id,
224
+ "targetId": req.target_id,
225
+ "relation": req.relation,
226
+ "weight": req.weight,
227
+ },
228
+ )
229
+ return result
230
+
231
+
232
+ @router.get("/graph", summary="Get the full memory graph")
233
+ async def get_graph():
234
+ """Return all nodes and edges in the memory graph."""
235
+ result = _rpc_call("graph")
236
+ return result
237
+
238
+
239
+ @router.post("/neighbours", summary="Get neighbours of a memory")
240
+ async def get_neighbours(body: dict[str, str]):
241
+ """Get all nodes directly connected to the given node."""
242
+ node_id = body.get("nodeId")
243
+ if not node_id:
244
+ raise HTTPException(status_code=400, detail="Missing 'nodeId' field.")
245
+ result = _rpc_call("neighbours", {"nodeId": node_id})
246
+ return result
247
+
248
+
249
+ @router.get("/count", summary="Memory count")
250
+ async def get_count():
251
+ """Return the total number of stored memories."""
252
+ result = _rpc_call("count")
253
+ return {"count": result}
254
+
255
+
256
+ @router.post("/update", summary="Update an existing memory")
257
+ async def update_memory(node_id: str, req: UpdateRequest):
258
+ """Partially update an existing memory node."""
259
+ params: dict[str, Any] = {"id": node_id}
260
+ if req.content is not None:
261
+ params["content"] = req.content
262
+ if req.summary is not None:
263
+ params["summary"] = req.summary
264
+ if req.type is not None:
265
+ params["type"] = req.type
266
+ if req.metadata is not None:
267
+ params["metadata"] = req.metadata
268
+ if req.importance is not None:
269
+ params["importance"] = req.importance
270
+ if req.tags is not None:
271
+ params["tags"] = req.tags
272
+
273
+ result = _rpc_call("update", params)
274
+ if result is None:
275
+ raise HTTPException(status_code=404, detail=f"Memory not found: {node_id}")
276
+ return result
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # TTL Endpoints
281
+ # ---------------------------------------------------------------------------
282
+
283
+
284
+ @router.patch("/mem/{mem_id}/ttl", summary="Set TTL on a memory")
285
+ async def set_ttl(mem_id: str, req: TtlRequest):
286
+ """Set a time-to-live on a memory node."""
287
+ _rpc_call("setTTL", {"id": mem_id, "seconds": req.seconds})
288
+ return {"id": mem_id, "ttl": req.seconds}
289
+
290
+
291
+ @router.delete("/mem/{mem_id}/ttl", summary="Clear TTL on a memory")
292
+ async def clear_ttl(mem_id: str):
293
+ """Clear the TTL on a memory node."""
294
+ _rpc_call("clearTTL", {"id": mem_id})
295
+ return {"id": mem_id, "ttl": None}
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Tags Endpoints
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ @router.post("/mem/{mem_id}/tags", summary="Add tags to a memory")
304
+ async def add_tags(mem_id: str, req: TagsRequest):
305
+ """Add tags to a memory node."""
306
+ _rpc_call("tag", {"id": mem_id, "tags": req.tags})
307
+ return {"id": mem_id, "tags": req.tags}
308
+
309
+
310
+ @router.delete("/mem/{mem_id}/tags/{tag}", summary="Remove a tag from a memory")
311
+ async def remove_tag(mem_id: str, tag: str):
312
+ """Remove a tag from a memory node."""
313
+ _rpc_call("untag", {"id": mem_id, "tags": [tag]})
314
+ return {"id": mem_id, "removed": tag}
315
+
316
+
317
+ @router.get("/tags/{tag}", summary="List memories by tag")
318
+ async def list_by_tag(tag: str):
319
+ """List all memories that have a specific tag."""
320
+ result = _rpc_call("listByTag", {"tag": tag})
321
+ return result
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Export Endpoint
326
+ # ---------------------------------------------------------------------------
327
+
328
+
329
+ @router.get("/export", summary="Export memories")
330
+ async def export_memories(
331
+ format: str = Query("json", description="Export format: json, markdown, obsidian"),
332
+ tag: str | None = Query(None, description="Filter by tag"),
333
+ ):
334
+ """Export memories in the specified format. Returns JSON or a zip file."""
335
+ opts: dict[str, Any] = {"format": format}
336
+ if tag:
337
+ opts["tag"] = tag
338
+
339
+ result = _rpc_call("export", {"opts": opts})
340
+
341
+ if format == "json":
342
+ return json.loads(result["data"])
343
+
344
+ # For markdown/obsidian, create a zip file
345
+ export_dir = result["data"]
346
+ buf = io.BytesIO()
347
+ with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
348
+ if os.path.isdir(export_dir):
349
+ for root, _dirs, files in os.walk(export_dir):
350
+ for fname in files:
351
+ filepath = os.path.join(root, fname)
352
+ arcname = os.path.relpath(filepath, export_dir)
353
+ zf.write(filepath, arcname)
354
+
355
+ buf.seek(0)
356
+ return StreamingResponse(
357
+ buf,
358
+ media_type="application/zip",
359
+ headers={"Content-Disposition": f'attachment; filename="memos-export-{format}.zip"'},
360
+ )