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
@@ -0,0 +1,385 @@
1
+ """
2
+ MemOS CrewAI Adapter — persistent memory for CrewAI agents and crews.
3
+
4
+ Provides a CrewAI-compatible memory implementation backed by MemOS.
5
+ Drop it into any CrewAI crew to give all agents persistent, graph-based
6
+ memory with zero configuration.
7
+
8
+ Installation:
9
+ pip install mem-os-python crewai
10
+
11
+ Usage:
12
+ from memos.adapters.crewai import MemOSMemory
13
+
14
+ # As CrewAI short-term memory
15
+ from crewai import Agent, Crew, Task, Process
16
+ from memos.adapters.crewai import MemOSMemory
17
+
18
+ memory = MemOSMemory()
19
+
20
+ researcher = Agent(
21
+ role="Researcher",
22
+ goal="Find accurate information",
23
+ backstory="You are a meticulous researcher.",
24
+ memory=True,
25
+ # CrewAI will use the memory system automatically
26
+ )
27
+
28
+ # Or use the memory tool directly
29
+ from memos.adapters.crewai import MemOSTool
30
+
31
+ tool = MemOSTool()
32
+ result = tool._run("search", query="dark mode")
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import ipaddress
38
+ import json
39
+ import os
40
+ import socket
41
+ import urllib.error
42
+ import urllib.parse
43
+ import urllib.request
44
+ from typing import Any
45
+
46
+
47
+ def _validate_url(url: str) -> str:
48
+ """SSRF guard: require http(s) with a host, and reject requests to
49
+ link-local / cloud-metadata addresses after DNS resolution.
50
+
51
+ Loopback and private ranges are intentionally allowed — this is a
52
+ local-first product whose services live on the user's own machine.
53
+ """
54
+ parsed = urllib.parse.urlparse(url)
55
+ if parsed.scheme not in ("http", "https") or not parsed.hostname:
56
+ raise ValueError(f"Unsupported URL (must be http/https with a host): {url!r}")
57
+ try:
58
+ infos = socket.getaddrinfo(
59
+ parsed.hostname,
60
+ parsed.port or (443 if parsed.scheme == "https" else 80),
61
+ type=socket.SOCK_STREAM,
62
+ )
63
+ except socket.gaierror as exc:
64
+ raise ValueError(f"Cannot resolve host in URL: {url!r}") from exc
65
+ for info in infos:
66
+ ip = ipaddress.ip_address(info[4][0])
67
+ if ip.is_link_local:
68
+ raise ValueError(
69
+ f"Refusing request to link-local/metadata address {ip} (SSRF guard): {url!r}"
70
+ )
71
+ return url
72
+
73
+
74
+ class MemOSTool:
75
+ """
76
+ CrewAI-compatible tool for interacting with MemOS.
77
+
78
+ Provides store, search, retrieve, and forget operations
79
+ that agents can use during their tasks.
80
+ """
81
+
82
+ name: str = "memos_memory"
83
+ description: str = (
84
+ "Access persistent memory. Use this to store important findings, "
85
+ "search for previously stored information, or retrieve specific memories. "
86
+ "Input should be a JSON string with 'action' and relevant parameters."
87
+ )
88
+
89
+ memos_url: str
90
+ """URL of the MemOS HTTP server."""
91
+
92
+ def __init__(self, memos_url: str | None = None) -> None:
93
+ """Create a MemOS tool bound to an HTTP server."""
94
+ self.memos_url = memos_url or os.environ.get("MEMOS_URL", "http://localhost:7400")
95
+
96
+ def _run(self, action: str, **kwargs: Any) -> str:
97
+ """
98
+ Execute a memory action.
99
+
100
+ Args:
101
+ action: One of 'store', 'search', 'retrieve', 'forget', 'summarize'.
102
+ **kwargs: Action-specific parameters.
103
+
104
+ Returns:
105
+ JSON string with the result.
106
+ """
107
+ try:
108
+ if action == "store":
109
+ result = self._memos_store(
110
+ content=kwargs.get("content", ""),
111
+ type=kwargs.get("type", "fact"),
112
+ tags=kwargs.get("tags", []),
113
+ )
114
+ return json.dumps(
115
+ {
116
+ "status": "success",
117
+ "id": result.get("node", {}).get("id"),
118
+ "summary": result.get("node", {}).get("summary"),
119
+ }
120
+ )
121
+
122
+ elif action == "search":
123
+ results = self._memos_search(
124
+ query=kwargs.get("query", ""),
125
+ limit=kwargs.get("limit", 5),
126
+ )
127
+ memories = []
128
+ for r in results:
129
+ node = r.get("node", r)
130
+ memories.append(
131
+ {
132
+ "content": node.get("content"),
133
+ "type": node.get("type"),
134
+ "score": r.get("score", 0),
135
+ "id": node.get("id"),
136
+ }
137
+ )
138
+ return json.dumps({"status": "success", "results": memories})
139
+
140
+ elif action == "retrieve":
141
+ node = self._memos_retrieve(kwargs.get("id", ""))
142
+ if node:
143
+ return json.dumps({"status": "success", "memory": node})
144
+ return json.dumps({"status": "not_found"})
145
+
146
+ elif action == "forget":
147
+ deleted = self._memos_forget(kwargs.get("id", ""))
148
+ return json.dumps({"status": "success", "deleted": deleted})
149
+
150
+ elif action == "summarize":
151
+ result = self._memos_post("/api/mem/summarize", {})
152
+ return json.dumps(
153
+ {
154
+ "status": "success",
155
+ "summary": result.get("summary", ""),
156
+ }
157
+ )
158
+
159
+ else:
160
+ return json.dumps({"status": "error", "message": f"Unknown action: {action}"})
161
+
162
+ except Exception as e:
163
+ return json.dumps({"status": "error", "message": str(e)})
164
+
165
+ def remember(self, content: str, **kwargs: Any) -> dict[str, Any]:
166
+ """
167
+ Manually store a memory.
168
+
169
+ Args:
170
+ content: Text to remember.
171
+ **kwargs: Additional parameters (type, tags, metadata).
172
+
173
+ Returns:
174
+ The created memory node.
175
+ """
176
+ return self._memos_store(content, **kwargs)
177
+
178
+ def recall(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
179
+ """
180
+ Search for relevant memories.
181
+
182
+ Args:
183
+ query: Search query.
184
+ limit: Maximum results.
185
+
186
+ Returns:
187
+ List of scored memory nodes.
188
+ """
189
+ return self._memos_search(query, limit=limit)
190
+
191
+ def forget(self, memory_id: str) -> bool:
192
+ """
193
+ Delete a specific memory.
194
+
195
+ Args:
196
+ memory_id: ID of the memory to forget.
197
+
198
+ Returns:
199
+ True if deleted.
200
+ """
201
+ return self._memos_forget(memory_id)
202
+
203
+ def summarize_all(self) -> str:
204
+ """
205
+ Get a summary of all stored memories.
206
+
207
+ Returns:
208
+ Summary string.
209
+ """
210
+ result = self._memos_post("/api/mem/summarize", {})
211
+ return result.get("summary", "")
212
+
213
+ # ------------------------------------------------------------------
214
+ # Internal
215
+ # ------------------------------------------------------------------
216
+
217
+ def _memos_store(self, content: str, **kwargs: Any) -> dict[str, Any]:
218
+ """Store a memory via MemOS API."""
219
+ payload = {"content": content, **kwargs}
220
+ return self._memos_post("/api/mem/store", payload)
221
+
222
+ def _memos_search(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
223
+ """Search memories via MemOS API."""
224
+ payload = {"query": query, "limit": limit}
225
+ result = self._memos_post("/api/mem/search", payload)
226
+ if isinstance(result, list):
227
+ return result
228
+ return []
229
+
230
+ def _memos_retrieve(self, memory_id: str) -> dict[str, Any] | None:
231
+ """Retrieve a memory via MemOS API."""
232
+ try:
233
+ result = self._memos_post("/api/mem/retrieve", {"id": memory_id})
234
+ return result
235
+ except Exception:
236
+ return None
237
+
238
+ def _memos_forget(self, memory_id: str) -> bool:
239
+ """Delete a memory via MemOS API."""
240
+ try:
241
+ self._memos_post("/api/mem/forget", {"id": memory_id})
242
+ return True
243
+ except Exception:
244
+ return False
245
+
246
+ def _memos_post(self, path: str, data: dict[str, Any]) -> Any:
247
+ """POST to the MemOS server."""
248
+ url = _validate_url(f"{self.memos_url}{path}")
249
+ body = json.dumps(data).encode("utf-8")
250
+ req = urllib.request.Request(
251
+ url,
252
+ data=body,
253
+ headers={"Content-Type": "application/json"},
254
+ method="POST",
255
+ )
256
+ try:
257
+ with urllib.request.urlopen(req, timeout=30) as resp:
258
+ return json.loads(resp.read().decode())
259
+ except urllib.error.URLError as exc:
260
+ raise ConnectionError(
261
+ f"Cannot reach MemOS at {self.memos_url}. "
262
+ f"Start it with: npx @mem-os/sdk serve. Error: {exc}"
263
+ ) from exc
264
+
265
+
266
+ class MemOSMemory:
267
+ """
268
+ CrewAI-compatible memory class backed by MemOS.
269
+
270
+ Provides short-term and long-term memory for CrewAI agents
271
+ by storing and retrieving memories from the MemOS graph.
272
+
273
+ Usage with CrewAI:
274
+ from crewai import Agent, Crew, Task, Process
275
+ from memos.adapters.crewai import MemOSMemory
276
+
277
+ memory = MemOSMemory()
278
+
279
+ agent = Agent(
280
+ role="Researcher",
281
+ goal="Find information",
282
+ backstory="You are a researcher.",
283
+ memory=True,
284
+ )
285
+
286
+ # The memory will be used automatically by CrewAI
287
+ """
288
+
289
+ memos_url: str = os.environ.get("MEMOS_URL", "http://localhost:7400")
290
+ """URL of the MemOS HTTP server."""
291
+
292
+ max_context_memories: int = 5
293
+ """Maximum number of memories to inject into context."""
294
+
295
+ auto_store: bool = True
296
+ """Automatically store agent observations."""
297
+
298
+ session_id: str = ""
299
+ """Session identifier for memory grouping."""
300
+
301
+ class Config:
302
+ """Pydantic config."""
303
+
304
+ arbitrary_types_allowed = True
305
+
306
+ def __init__(self, **kwargs: Any):
307
+ """Create a new MemOSMemory instance."""
308
+ for key, value in kwargs.items():
309
+ if hasattr(self, key):
310
+ setattr(self, key, value)
311
+ self._tool = MemOSTool(memos_url=self.memos_url)
312
+
313
+ def save(self, content: str, **kwargs: Any) -> dict[str, Any]:
314
+ """
315
+ Store a memory from an agent's observation.
316
+
317
+ Args:
318
+ content: Text content to remember.
319
+ **kwargs: Additional parameters (type, tags, metadata).
320
+
321
+ Returns:
322
+ The created memory node.
323
+ """
324
+ metadata = kwargs.pop("metadata", {})
325
+ if self.session_id:
326
+ metadata["session_id"] = self.session_id
327
+ metadata["source"] = "crewai"
328
+ return self._tool.remember(content, metadata=metadata, **kwargs)
329
+
330
+ def search(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
331
+ """
332
+ Search for relevant memories.
333
+
334
+ Args:
335
+ query: Search query.
336
+ limit: Maximum results.
337
+
338
+ Returns:
339
+ List of scored memory nodes.
340
+ """
341
+ return self._tool.recall(query, limit=limit)
342
+
343
+ def get_context(self, query: str) -> str:
344
+ """
345
+ Get memory context for injection into agent prompts.
346
+
347
+ Args:
348
+ query: The current task or question.
349
+
350
+ Returns:
351
+ Formatted memory context string.
352
+ """
353
+ memories = self.search(query, limit=self.max_context_memories)
354
+ if not memories:
355
+ return ""
356
+
357
+ lines = ["Relevant memories:"]
358
+ for i, m in enumerate(memories, 1):
359
+ node = m.get("node", m)
360
+ content = node.get("content", "")
361
+ mem_type = node.get("type", "fact")
362
+ lines.append(f" {i}. [{mem_type}] {content}")
363
+
364
+ return "\n".join(lines)
365
+
366
+ def forget(self, memory_id: str) -> bool:
367
+ """
368
+ Delete a specific memory.
369
+
370
+ Args:
371
+ memory_id: ID of the memory to forget.
372
+
373
+ Returns:
374
+ True if deleted.
375
+ """
376
+ return self._tool.forget(memory_id)
377
+
378
+ def summarize(self) -> str:
379
+ """
380
+ Get a summary of all stored memories.
381
+
382
+ Returns:
383
+ Summary string.
384
+ """
385
+ return self._tool.summarize_all()
@@ -0,0 +1,301 @@
1
+ """
2
+ MemOS LangChain Adapter — memory integration for LangChain chains and agents.
3
+
4
+ Provides a LangChain-compatible `BaseMemory` implementation backed by
5
+ MemOS. Drop it into any LangChain chain or agent to give it persistent,
6
+ graph-based memory with zero configuration.
7
+
8
+ Installation:
9
+ pip install memos langchain
10
+
11
+ Usage:
12
+ from langchain_openai import ChatOpenAI
13
+ from langchain.chains import ConversationChain
14
+ from memos.adapters.langchain import MemOSMemory
15
+
16
+ memory = MemOSMemory()
17
+ chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
18
+
19
+ chain.invoke({"input": "I prefer dark mode"})
20
+ chain.invoke({"input": "What theme do I like?"})
21
+ # → The chain recalls "dark mode" from MemOS
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import ipaddress
27
+ import json
28
+ import os
29
+ import socket
30
+ import urllib.parse
31
+ import urllib.request
32
+ from typing import Any
33
+
34
+ from langchain.memory.chat_memory import BaseMemory
35
+ from langchain.schema import AIMessage, BaseMessage, HumanMessage
36
+ from pydantic import Field
37
+
38
+
39
+ def _validate_url(url: str) -> str:
40
+ """SSRF guard: require http(s) with a host, and reject requests to
41
+ link-local / cloud-metadata addresses after DNS resolution.
42
+
43
+ Loopback and private ranges are intentionally allowed — this is a
44
+ local-first product whose services live on the user's own machine.
45
+ """
46
+ parsed = urllib.parse.urlparse(url)
47
+ if parsed.scheme not in ("http", "https") or not parsed.hostname:
48
+ raise ValueError(f"Unsupported URL (must be http/https with a host): {url!r}")
49
+ try:
50
+ infos = socket.getaddrinfo(
51
+ parsed.hostname,
52
+ parsed.port or (443 if parsed.scheme == "https" else 80),
53
+ type=socket.SOCK_STREAM,
54
+ )
55
+ except socket.gaierror as exc:
56
+ raise ValueError(f"Cannot resolve host in URL: {url!r}") from exc
57
+ for info in infos:
58
+ ip = ipaddress.ip_address(info[4][0])
59
+ if ip.is_link_local:
60
+ raise ValueError(
61
+ f"Refusing request to link-local/metadata address {ip} (SSRF guard): {url!r}"
62
+ )
63
+ return url
64
+
65
+
66
+ class MemOSMemory(BaseMemory):
67
+ """
68
+ LangChain `BaseMemory` implementation backed by MemOS.
69
+
70
+ Stores conversation context and extracted facts in the MemOS
71
+ graph, providing persistent memory across sessions.
72
+
73
+ All data stays local — no cloud dependencies.
74
+ """
75
+
76
+ memos_url: str = Field(
77
+ default_factory=lambda: os.environ.get("MEMOS_URL", "http://localhost:7400")
78
+ )
79
+ """URL of the MemOS HTTP server."""
80
+
81
+ max_context_memories: int = 5
82
+ """Maximum number of memories to inject into context."""
83
+
84
+ auto_extract_facts: bool = True
85
+ """Automatically extract and store facts from user messages."""
86
+
87
+ min_message_length: int = 15
88
+ """Minimum message length to trigger fact extraction."""
89
+
90
+ memory_key: str = "history"
91
+ """Key used in the chain's prompt variables."""
92
+
93
+ return_messages: bool = True
94
+ """Whether to return messages as BaseMessage objects or strings."""
95
+
96
+ # Internal state
97
+ _conversation_buffer: list[BaseMessage] = []
98
+ _session_id: str = ""
99
+
100
+ class Config:
101
+ """Pydantic config."""
102
+
103
+ arbitrary_types_allowed = True
104
+
105
+ def __init__(self, **kwargs: Any):
106
+ """
107
+ Create a new MemOSMemory instance.
108
+
109
+ Args:
110
+ **kwargs: Configuration overrides (memos_url, max_context_memories, etc.).
111
+ """
112
+ super().__init__(**kwargs)
113
+ self._conversation_buffer = []
114
+ self._session_id = kwargs.get("session_id", "")
115
+
116
+ @property
117
+ def memory_variables(self) -> list[str]:
118
+ """Return the memory variables this class adds to the prompt."""
119
+ return [self.memory_key]
120
+
121
+ def load_memory_variables(self, inputs: dict[str, Any] | None = None) -> dict[str, Any]:
122
+ """
123
+ Load memory variables for chain injection.
124
+
125
+ Retrieves relevant memories from MemOS based on the current
126
+ input, then combines them with the conversation buffer.
127
+
128
+ Args:
129
+ inputs: The current chain inputs (used for relevance search).
130
+
131
+ Returns:
132
+ Dict with the memory_key containing memory context.
133
+ """
134
+ memories = []
135
+
136
+ # Search for relevant memories based on current input
137
+ if inputs:
138
+ query = inputs.get("input", "") or inputs.get("query", "") or ""
139
+ if query:
140
+ memories = self._memos_search(query, limit=self.max_context_memories)
141
+
142
+ # Build context from memories
143
+ memory_texts = []
144
+ for m in memories:
145
+ node = m.get("node", m)
146
+ content = node.get("content", "")
147
+ mem_type = node.get("type", "fact")
148
+ memory_texts.append(f"[{mem_type}] {content}")
149
+
150
+ # Combine with conversation buffer
151
+ if self.return_messages:
152
+ result_messages = list(self._conversation_buffer)
153
+ if memory_texts:
154
+ memory_context = "Relevant memories:\n" + "\n".join(memory_texts)
155
+ result_messages.insert(0, HumanMessage(content=f"(System: {memory_context})"))
156
+ return {self.memory_key: result_messages}
157
+ else:
158
+ buffer_str = self._buffer_as_str()
159
+ if memory_texts:
160
+ memory_context = "Relevant memories:\n" + "\n".join(memory_texts)
161
+ return {self.memory_key: f"{memory_context}\n\n{buffer_str}"}
162
+ return {self.memory_key: buffer_str}
163
+
164
+ def save_context(self, inputs: dict[str, Any], outputs: dict[str, str]) -> None:
165
+ """
166
+ Save the context of this chain run to memory.
167
+
168
+ Called by LangChain after each chain execution. Stores the
169
+ interaction in both the conversation buffer and MemOS.
170
+
171
+ Args:
172
+ inputs: The inputs to the chain (typically has 'input' key).
173
+ outputs: The outputs from the chain (typically has 'output' key).
174
+ """
175
+ input_str = inputs.get("input", "") or inputs.get("query", "") or ""
176
+ output_str = outputs.get("output", "") or outputs.get("response", "") or ""
177
+
178
+ # Add to conversation buffer
179
+ if input_str:
180
+ self._conversation_buffer.append(HumanMessage(content=input_str))
181
+ if output_str:
182
+ self._conversation_buffer.append(AIMessage(content=output_str))
183
+
184
+ # Auto-extract facts from user messages
185
+ if self.auto_extract_facts and input_str and len(input_str) >= self.min_message_length:
186
+ self._memos_store(
187
+ content=input_str,
188
+ type="fact",
189
+ metadata={
190
+ "source": "langchain",
191
+ "session_id": self._session_id,
192
+ },
193
+ )
194
+
195
+ def clear(self) -> None:
196
+ """Clear the conversation buffer. Does not delete MemOS memories."""
197
+ self._conversation_buffer.clear()
198
+
199
+ # ------------------------------------------------------------------
200
+ # Public helpers
201
+ # ------------------------------------------------------------------
202
+
203
+ def remember(self, content: str, **kwargs: Any) -> dict[str, Any]:
204
+ """
205
+ Manually store a memory.
206
+
207
+ Args:
208
+ content: Text to remember.
209
+ **kwargs: Additional memory parameters (type, metadata, importance).
210
+
211
+ Returns:
212
+ The created memory node.
213
+ """
214
+ return self._memos_store(content, **kwargs)
215
+
216
+ def recall(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
217
+ """
218
+ Search for relevant memories.
219
+
220
+ Args:
221
+ query: Search query.
222
+ limit: Maximum results.
223
+
224
+ Returns:
225
+ List of scored memory nodes.
226
+ """
227
+ return self._memos_search(query, limit=limit)
228
+
229
+ def forget(self, memory_id: str) -> bool:
230
+ """
231
+ Delete a specific memory.
232
+
233
+ Args:
234
+ memory_id: ID of the memory to forget.
235
+
236
+ Returns:
237
+ True if deleted.
238
+ """
239
+ try:
240
+ self._memos_post("/api/mem/forget", {"id": memory_id})
241
+ return True
242
+ except Exception:
243
+ return False
244
+
245
+ def summarize_all(self) -> str:
246
+ """
247
+ Get a summary of all stored memories.
248
+
249
+ Returns:
250
+ Summary string.
251
+ """
252
+ result = self._memos_post("/api/mem/summarize", {})
253
+ return result.get("summary", "")
254
+
255
+ # ------------------------------------------------------------------
256
+ # Internal
257
+ # ------------------------------------------------------------------
258
+
259
+ def _buffer_as_str(self) -> str:
260
+ """Convert conversation buffer to a string representation."""
261
+ lines = []
262
+ for msg in self._conversation_buffer:
263
+ if isinstance(msg, HumanMessage):
264
+ lines.append(f"Human: {msg.content}")
265
+ elif isinstance(msg, AIMessage):
266
+ lines.append(f"AI: {msg.content}")
267
+ else:
268
+ lines.append(f"{msg.type}: {msg.content}")
269
+ return "\n".join(lines)
270
+
271
+ def _memos_search(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
272
+ """Search memories via MemOS API."""
273
+ payload = {"query": query, "limit": limit}
274
+ result = self._memos_post("/api/mem/search", payload)
275
+ if isinstance(result, list):
276
+ return result
277
+ return []
278
+
279
+ def _memos_store(self, content: str, **kwargs: Any) -> dict[str, Any]:
280
+ """Store a memory via MemOS API."""
281
+ payload = {"content": content, **kwargs}
282
+ return self._memos_post("/api/mem/store", payload)
283
+
284
+ def _memos_post(self, path: str, data: dict[str, Any]) -> Any:
285
+ """POST to the MemOS server."""
286
+ url = _validate_url(f"{self.memos_url}{path}")
287
+ body = json.dumps(data).encode("utf-8")
288
+ req = urllib.request.Request(
289
+ url,
290
+ data=body,
291
+ headers={"Content-Type": "application/json"},
292
+ method="POST",
293
+ )
294
+ try:
295
+ with urllib.request.urlopen(req, timeout=30) as resp:
296
+ return json.loads(resp.read().decode())
297
+ except urllib.error.URLError as exc:
298
+ raise ConnectionError(
299
+ f"Cannot reach MemOS at {self.memos_url}. "
300
+ f"Start it with: npx @memos/sdk serve. Error: {exc}"
301
+ ) from exc