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,396 @@
1
+ """
2
+ MemOS Ollama Adapter — zero-config memory for Ollama chat completions.
3
+
4
+ Wraps the `ollama` Python client to automatically store and retrieve
5
+ relevant memories during chat interactions. No API keys, no cloud
6
+ dependencies — everything runs 100% locally.
7
+
8
+ Installation:
9
+ pip install memos ollama
10
+
11
+ Usage:
12
+ from memos.adapters.ollama import OllamaMemory
13
+
14
+ chat = OllamaMemory(model="llama3")
15
+ await chat.init()
16
+
17
+ # Memories are automatically injected and extracted
18
+ response = await chat.chat("I prefer dark mode in all my apps")
19
+ response = await chat.chat("What theme do I like?")
20
+ # → The model will recall "dark mode" from memory
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import ipaddress
26
+ import json
27
+ import os
28
+ import socket
29
+ import urllib.error
30
+ import urllib.parse
31
+ import urllib.request
32
+ from collections.abc import AsyncIterator
33
+ from dataclasses import dataclass, field
34
+ from typing import Any
35
+
36
+
37
+ @dataclass
38
+ class OllamaConfig:
39
+ """Configuration for the Ollama adapter."""
40
+
41
+ base_url: str = field(
42
+ default_factory=lambda: os.environ.get("OLLAMA_HOST", "http://localhost:11434")
43
+ )
44
+ model: str = "llama3"
45
+ memos_url: str = field(
46
+ default_factory=lambda: os.environ.get("MEMOS_URL", "http://localhost:7400")
47
+ )
48
+ """URL of the MemOS HTTP server."""
49
+ max_context_memories: int = 5
50
+ """Maximum number of memories to inject into the system prompt."""
51
+ auto_store: bool = True
52
+ """Automatically store user messages as memories."""
53
+ memory_types: list[str] = field(default_factory=lambda: ["preference", "fact", "context"])
54
+ """Memory types to store when auto-storing is enabled."""
55
+ store_threshold: int = 20
56
+ """Minimum message length to trigger auto-storage."""
57
+
58
+
59
+ class OllamaMemory:
60
+ """
61
+ Ollama chat wrapper with automatic memory management.
62
+
63
+ Connects to the Ollama API for chat completions and the MemOS
64
+ REST API for memory operations. All communication stays local.
65
+ """
66
+
67
+ def __init__(self, config: OllamaConfig | None = None, **kwargs: Any):
68
+ """
69
+ Create a new OllamaMemory instance.
70
+
71
+ Args:
72
+ config: Full configuration object. If not provided, defaults are used.
73
+ **kwargs: Override individual config fields (model, base_url, etc.).
74
+ """
75
+ if config is None:
76
+ config = OllamaConfig()
77
+ # Apply keyword overrides
78
+ for key, value in kwargs.items():
79
+ if hasattr(config, key):
80
+ setattr(config, key, value)
81
+ self.config = config
82
+ self._messages: list[dict[str, str]] = []
83
+ self._initialized = False
84
+
85
+ async def init(self) -> None:
86
+ """
87
+ Verify connectivity to both Ollama and MemOS servers.
88
+
89
+ Raises:
90
+ ConnectionError: If either server is unreachable.
91
+ """
92
+ self._check_ollama()
93
+ self._check_memos()
94
+ self._initialized = True
95
+
96
+ # ------------------------------------------------------------------
97
+ # Public API
98
+ # ------------------------------------------------------------------
99
+
100
+ async def chat(self, message: str, **ollama_kwargs: Any) -> str:
101
+ """
102
+ Send a message and get a response with memory-augmented context.
103
+
104
+ 1. Retrieves relevant memories for the message.
105
+ 2. Injects them into the system prompt.
106
+ 3. Sends the conversation to Ollama.
107
+ 4. Optionally stores the user message as a memory.
108
+
109
+ Args:
110
+ message: The user's message.
111
+ **ollama_kwargs: Additional parameters for Ollama (temperature, etc.).
112
+
113
+ Returns:
114
+ The model's response string.
115
+ """
116
+ self._assert_init()
117
+
118
+ # Retrieve relevant memories
119
+ memories = self._memos_search(message)
120
+ system_prompt = self._build_system_prompt(memories)
121
+
122
+ # Build messages
123
+ messages = [{"role": "system", "content": system_prompt}]
124
+ messages.extend(self._messages)
125
+ messages.append({"role": "user", "content": message})
126
+
127
+ # Call Ollama
128
+ response = self._ollama_chat(messages, **ollama_kwargs)
129
+ assistant_msg = response.get("message", {}).get("content", "")
130
+
131
+ # Update conversation history
132
+ self._messages.append({"role": "user", "content": message})
133
+ self._messages.append({"role": "assistant", "content": assistant_msg})
134
+
135
+ # Auto-store user message
136
+ if self.config.auto_store and len(message) >= self.config.store_threshold:
137
+ self._memos_store(
138
+ content=message,
139
+ metadata={"source": "ollama-chat", "model": self.config.model},
140
+ )
141
+
142
+ return assistant_msg
143
+
144
+ async def chat_stream(self, message: str, **ollama_kwargs: Any) -> AsyncIterator[str]:
145
+ """
146
+ Stream a chat response token by token.
147
+
148
+ Args:
149
+ message: The user's message.
150
+ **ollama_kwargs: Additional parameters for Ollama.
151
+
152
+ Yields:
153
+ Response tokens as they arrive.
154
+ """
155
+ self._assert_init()
156
+
157
+ memories = self._memos_search(message)
158
+ system_prompt = self._build_system_prompt(memories)
159
+
160
+ messages = [{"role": "system", "content": system_prompt}]
161
+ messages.extend(self._messages)
162
+ messages.append({"role": "user", "content": message})
163
+
164
+ ollama_kwargs["stream"] = True
165
+ full_response = ""
166
+
167
+ for chunk in self._ollama_chat_stream(messages, **ollama_kwargs):
168
+ token = chunk.get("message", {}).get("content", "")
169
+ full_response += token
170
+ yield token
171
+
172
+ self._messages.append({"role": "user", "content": message})
173
+ self._messages.append({"role": "assistant", "content": full_response})
174
+
175
+ if self.config.auto_store and len(message) >= self.config.store_threshold:
176
+ self._memos_store(
177
+ content=message,
178
+ metadata={"source": "ollama-chat", "model": self.config.model},
179
+ )
180
+
181
+ def remember(self, content: str, **kwargs: Any) -> dict[str, Any]:
182
+ """
183
+ Manually store a memory.
184
+
185
+ Args:
186
+ content: Text to remember.
187
+ **kwargs: Additional memory parameters (type, metadata, importance).
188
+
189
+ Returns:
190
+ The created memory node.
191
+ """
192
+ self._assert_init()
193
+ return self._memos_store(content, **kwargs)
194
+
195
+ def recall(self, query: str, limit: int = 5) -> list[dict[str, Any]]:
196
+ """
197
+ Search for relevant memories.
198
+
199
+ Args:
200
+ query: Search query.
201
+ limit: Maximum results to return.
202
+
203
+ Returns:
204
+ List of scored memory nodes.
205
+ """
206
+ self._assert_init()
207
+ return self._memos_search(query, limit=limit)
208
+
209
+ def forget(self, memory_id: str) -> bool:
210
+ """
211
+ Delete a specific memory.
212
+
213
+ Args:
214
+ memory_id: ID of the memory to forget.
215
+
216
+ Returns:
217
+ True if the memory was deleted.
218
+ """
219
+ self._assert_init()
220
+ return self._memos_forget(memory_id)
221
+
222
+ def clear_history(self) -> None:
223
+ """Clear the conversation history (does not affect memories)."""
224
+ self._messages.clear()
225
+
226
+ # ------------------------------------------------------------------
227
+ # Internal helpers
228
+ # ------------------------------------------------------------------
229
+
230
+ def _build_system_prompt(self, memories: list[dict[str, Any]]) -> str:
231
+ """Build a system prompt with injected memories."""
232
+ if not memories:
233
+ return "You are a helpful assistant. Answer based on the conversation."
234
+
235
+ mem_lines = []
236
+ for i, m in enumerate(memories, 1):
237
+ node = m.get("node", m)
238
+ content = node.get("content", "")
239
+ mem_type = node.get("type", "fact")
240
+ mem_lines.append(f" {i}. [{mem_type}] {content}")
241
+
242
+ mem_block = "\n".join(mem_lines)
243
+
244
+ return (
245
+ "You are a helpful assistant with access to the user's stored memories.\n"
246
+ "Use the following memories to personalize your responses. "
247
+ "Do not mention that you have access to memories unless asked.\n\n"
248
+ f"Relevant memories:\n{mem_block}\n\n"
249
+ "Answer naturally based on both the conversation and your memories."
250
+ )
251
+
252
+ def _check_ollama(self) -> None:
253
+ """Verify Ollama is running."""
254
+ try:
255
+ self._http_get(f"{self.config.base_url}/api/tags")
256
+ except (urllib.error.URLError, ConnectionError) as exc:
257
+ raise ConnectionError(
258
+ f"Cannot reach Ollama at {self.config.base_url}. Is Ollama running? Error: {exc}"
259
+ ) from exc
260
+
261
+ def _check_memos(self) -> None:
262
+ """Verify MemOS server is running."""
263
+ try:
264
+ self._http_get(f"{self.config.memos_url}/health")
265
+ except (urllib.error.URLError, ConnectionError) as exc:
266
+ raise ConnectionError(
267
+ f"Cannot reach MemOS at {self.config.memos_url}. "
268
+ f"Start it with: npx @memos/sdk serve. Error: {exc}"
269
+ ) from exc
270
+
271
+ def _ollama_chat(self, messages: list[dict[str, str]], **kwargs: Any) -> dict[str, Any]:
272
+ """Call Ollama chat completion API."""
273
+ payload = {
274
+ "model": self.config.model,
275
+ "messages": messages,
276
+ "stream": False,
277
+ **kwargs,
278
+ }
279
+ return self._http_post(f"{self.config.base_url}/api/chat", payload)
280
+
281
+ def _ollama_chat_stream(
282
+ self, messages: list[dict[str, str]], **kwargs: Any
283
+ ) -> list[dict[str, Any]]:
284
+ """Call Ollama chat with stream=True and collect chunks."""
285
+ payload = {
286
+ "model": self.config.model,
287
+ "messages": messages,
288
+ "stream": True,
289
+ **kwargs,
290
+ }
291
+ return self._http_post_stream(f"{self.config.base_url}/api/chat", payload)
292
+
293
+ def _memos_store(self, content: str, **kwargs: Any) -> dict[str, Any]:
294
+ """Store a memory via MemOS API."""
295
+ payload = {"content": content, **kwargs}
296
+ return self._http_post(f"{self.config.memos_url}/api/mem/store", payload)
297
+
298
+ def _memos_search(self, query: str, limit: int | None = None) -> list[dict[str, Any]]:
299
+ """Search memories via MemOS API."""
300
+ payload: dict[str, Any] = {"query": query}
301
+ if limit is not None:
302
+ payload["limit"] = limit
303
+ else:
304
+ payload["limit"] = self.config.max_context_memories
305
+ if self.config.memory_types:
306
+ payload["type"] = None # Search all types
307
+
308
+ result = self._http_post(f"{self.config.memos_url}/api/mem/search", payload)
309
+ if isinstance(result, list):
310
+ return result
311
+ return []
312
+
313
+ def _memos_forget(self, memory_id: str) -> bool:
314
+ """Delete a memory via MemOS API."""
315
+ try:
316
+ self._http_post(f"{self.config.memos_url}/api/mem/forget", {"id": memory_id})
317
+ return True
318
+ except Exception:
319
+ return False
320
+
321
+ # ------------------------------------------------------------------
322
+ # HTTP helpers (stdlib only, no external deps)
323
+ # ------------------------------------------------------------------
324
+
325
+ @staticmethod
326
+ def _validate_url(url: str) -> str:
327
+ """SSRF guard: require http(s) with a host, and reject requests to
328
+ link-local / cloud-metadata addresses after DNS resolution.
329
+
330
+ Loopback and private ranges are intentionally allowed — this is a
331
+ local-first product whose services live on the user's own machine.
332
+ """
333
+ parsed = urllib.parse.urlparse(url)
334
+ if parsed.scheme not in ("http", "https") or not parsed.hostname:
335
+ raise ValueError(f"Unsupported URL (must be http/https with a host): {url!r}")
336
+ try:
337
+ infos = socket.getaddrinfo(
338
+ parsed.hostname,
339
+ parsed.port or (443 if parsed.scheme == "https" else 80),
340
+ type=socket.SOCK_STREAM,
341
+ )
342
+ except socket.gaierror as exc:
343
+ raise ValueError(f"Cannot resolve host in URL: {url!r}") from exc
344
+ for info in infos:
345
+ ip = ipaddress.ip_address(info[4][0])
346
+ if ip.is_link_local:
347
+ raise ValueError(
348
+ f"Refusing request to link-local/metadata address {ip} (SSRF guard): {url!r}"
349
+ )
350
+ return url
351
+
352
+ @staticmethod
353
+ def _http_get(url: str) -> dict[str, Any]:
354
+ """Simple GET request."""
355
+ OllamaMemory._validate_url(url)
356
+ req = urllib.request.Request(url, method="GET")
357
+ with urllib.request.urlopen(req, timeout=5) as resp:
358
+ return json.loads(resp.read().decode())
359
+
360
+ @staticmethod
361
+ def _http_post(url: str, data: dict[str, Any]) -> dict[str, Any]:
362
+ """Simple POST request with JSON body."""
363
+ OllamaMemory._validate_url(url)
364
+ body = json.dumps(data).encode("utf-8")
365
+ req = urllib.request.Request(
366
+ url,
367
+ data=body,
368
+ headers={"Content-Type": "application/json"},
369
+ method="POST",
370
+ )
371
+ with urllib.request.urlopen(req, timeout=30) as resp:
372
+ return json.loads(resp.read().decode())
373
+
374
+ @staticmethod
375
+ def _http_post_stream(url: str, data: dict[str, Any]) -> list[dict[str, Any]]:
376
+ """POST with streamed NDJSON response."""
377
+ OllamaMemory._validate_url(url)
378
+ body = json.dumps(data).encode("utf-8")
379
+ req = urllib.request.Request(
380
+ url,
381
+ data=body,
382
+ headers={"Content-Type": "application/json"},
383
+ method="POST",
384
+ )
385
+ chunks: list[dict[str, Any]] = []
386
+ with urllib.request.urlopen(req, timeout=120) as resp:
387
+ for line in resp:
388
+ line = line.strip()
389
+ if line:
390
+ chunks.append(json.loads(line.decode()))
391
+ return chunks
392
+
393
+ def _assert_init(self) -> None:
394
+ """Assert the adapter has been initialised."""
395
+ if not self._initialized:
396
+ raise RuntimeError("OllamaMemory not initialised. Call `await chat.init()` first.")
File without changes