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.
- mem_os_python-1.6.27.dist-info/METADATA +1032 -0
- mem_os_python-1.6.27.dist-info/RECORD +253 -0
- mem_os_python-1.6.27.dist-info/WHEEL +4 -0
- mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
- mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
- memos/__init__.py +16 -0
- memos/_js/adapters/anthropic.d.ts +279 -0
- memos/_js/adapters/anthropic.d.ts.map +1 -0
- memos/_js/adapters/anthropic.js +348 -0
- memos/_js/adapters/anthropic.js.map +1 -0
- memos/_js/adapters/base.d.ts +10 -0
- memos/_js/adapters/base.d.ts.map +1 -0
- memos/_js/adapters/base.js +2 -0
- memos/_js/adapters/base.js.map +1 -0
- memos/_js/adapters/openai.d.ts +264 -0
- memos/_js/adapters/openai.d.ts.map +1 -0
- memos/_js/adapters/openai.js +342 -0
- memos/_js/adapters/openai.js.map +1 -0
- memos/_js/apps/explorer.d.ts +244 -0
- memos/_js/apps/explorer.d.ts.map +1 -0
- memos/_js/apps/explorer.js +738 -0
- memos/_js/apps/explorer.js.map +1 -0
- memos/_js/citations.d.ts +65 -0
- memos/_js/citations.d.ts.map +1 -0
- memos/_js/citations.js +143 -0
- memos/_js/citations.js.map +1 -0
- memos/_js/cli-browse.d.ts +3 -0
- memos/_js/cli-browse.d.ts.map +1 -0
- memos/_js/cli-browse.js +129 -0
- memos/_js/cli-browse.js.map +1 -0
- memos/_js/cli-completion.d.ts +31 -0
- memos/_js/cli-completion.d.ts.map +1 -0
- memos/_js/cli-completion.js +340 -0
- memos/_js/cli-completion.js.map +1 -0
- memos/_js/cli-help.d.ts +18 -0
- memos/_js/cli-help.d.ts.map +1 -0
- memos/_js/cli-help.js +357 -0
- memos/_js/cli-help.js.map +1 -0
- memos/_js/cli.d.ts +28 -0
- memos/_js/cli.d.ts.map +1 -0
- memos/_js/cli.js +2880 -0
- memos/_js/cli.js.map +1 -0
- memos/_js/compression.d.ts +25 -0
- memos/_js/compression.d.ts.map +1 -0
- memos/_js/compression.js +319 -0
- memos/_js/compression.js.map +1 -0
- memos/_js/confidence-machine.d.ts +99 -0
- memos/_js/confidence-machine.d.ts.map +1 -0
- memos/_js/confidence-machine.js +222 -0
- memos/_js/confidence-machine.js.map +1 -0
- memos/_js/context-pack.d.ts +506 -0
- memos/_js/context-pack.d.ts.map +1 -0
- memos/_js/context-pack.js +1435 -0
- memos/_js/context-pack.js.map +1 -0
- memos/_js/contradictions.d.ts +160 -0
- memos/_js/contradictions.d.ts.map +1 -0
- memos/_js/contradictions.js +255 -0
- memos/_js/contradictions.js.map +1 -0
- memos/_js/dashboard.d.ts +30 -0
- memos/_js/dashboard.d.ts.map +1 -0
- memos/_js/dashboard.js +302 -0
- memos/_js/dashboard.js.map +1 -0
- memos/_js/ebbinghaus.d.ts +29 -0
- memos/_js/ebbinghaus.d.ts.map +1 -0
- memos/_js/ebbinghaus.js +32 -0
- memos/_js/ebbinghaus.js.map +1 -0
- memos/_js/embedding-queue.d.ts +190 -0
- memos/_js/embedding-queue.d.ts.map +1 -0
- memos/_js/embedding-queue.js +316 -0
- memos/_js/embedding-queue.js.map +1 -0
- memos/_js/embeddings.d.ts +187 -0
- memos/_js/embeddings.d.ts.map +1 -0
- memos/_js/embeddings.js +633 -0
- memos/_js/embeddings.js.map +1 -0
- memos/_js/entity-extraction.d.ts +54 -0
- memos/_js/entity-extraction.d.ts.map +1 -0
- memos/_js/entity-extraction.js +325 -0
- memos/_js/entity-extraction.js.map +1 -0
- memos/_js/entity-index.d.ts +93 -0
- memos/_js/entity-index.d.ts.map +1 -0
- memos/_js/entity-index.js +165 -0
- memos/_js/entity-index.js.map +1 -0
- memos/_js/event-memory.d.ts +147 -0
- memos/_js/event-memory.d.ts.map +1 -0
- memos/_js/event-memory.js +290 -0
- memos/_js/event-memory.js.map +1 -0
- memos/_js/external-import.d.ts +98 -0
- memos/_js/external-import.d.ts.map +1 -0
- memos/_js/external-import.js +669 -0
- memos/_js/external-import.js.map +1 -0
- memos/_js/fidelity.d.ts +367 -0
- memos/_js/fidelity.d.ts.map +1 -0
- memos/_js/fidelity.js +911 -0
- memos/_js/fidelity.js.map +1 -0
- memos/_js/graph-expansion.d.ts +119 -0
- memos/_js/graph-expansion.d.ts.map +1 -0
- memos/_js/graph-expansion.js +213 -0
- memos/_js/graph-expansion.js.map +1 -0
- memos/_js/graph-mermaid.d.ts +13 -0
- memos/_js/graph-mermaid.d.ts.map +1 -0
- memos/_js/graph-mermaid.js +32 -0
- memos/_js/graph-mermaid.js.map +1 -0
- memos/_js/graph.d.ts +170 -0
- memos/_js/graph.d.ts.map +1 -0
- memos/_js/graph.js +482 -0
- memos/_js/graph.js.map +1 -0
- memos/_js/harness.d.ts +63 -0
- memos/_js/harness.d.ts.map +1 -0
- memos/_js/harness.js +93 -0
- memos/_js/harness.js.map +1 -0
- memos/_js/importance.d.ts +118 -0
- memos/_js/importance.d.ts.map +1 -0
- memos/_js/importance.js +144 -0
- memos/_js/importance.js.map +1 -0
- memos/_js/index.d.ts +51 -0
- memos/_js/index.d.ts.map +1 -0
- memos/_js/index.js +53 -0
- memos/_js/index.js.map +1 -0
- memos/_js/mcp.d.ts +58 -0
- memos/_js/mcp.d.ts.map +1 -0
- memos/_js/mcp.js +1853 -0
- memos/_js/mcp.js.map +1 -0
- memos/_js/memory.d.ts +1762 -0
- memos/_js/memory.d.ts.map +1 -0
- memos/_js/memory.js +6283 -0
- memos/_js/memory.js.map +1 -0
- memos/_js/procedural.d.ts +74 -0
- memos/_js/procedural.d.ts.map +1 -0
- memos/_js/procedural.js +125 -0
- memos/_js/procedural.js.map +1 -0
- memos/_js/project-config.d.ts +68 -0
- memos/_js/project-config.d.ts.map +1 -0
- memos/_js/project-config.js +59 -0
- memos/_js/project-config.js.map +1 -0
- memos/_js/proof-of-forgetting.d.ts +57 -0
- memos/_js/proof-of-forgetting.d.ts.map +1 -0
- memos/_js/proof-of-forgetting.js +80 -0
- memos/_js/proof-of-forgetting.js.map +1 -0
- memos/_js/provenance.d.ts +122 -0
- memos/_js/provenance.d.ts.map +1 -0
- memos/_js/provenance.js +182 -0
- memos/_js/provenance.js.map +1 -0
- memos/_js/quarantine-commands.d.ts +23 -0
- memos/_js/quarantine-commands.d.ts.map +1 -0
- memos/_js/quarantine-commands.js +42 -0
- memos/_js/quarantine-commands.js.map +1 -0
- memos/_js/quarantine.d.ts +120 -0
- memos/_js/quarantine.d.ts.map +1 -0
- memos/_js/quarantine.js +873 -0
- memos/_js/quarantine.js.map +1 -0
- memos/_js/redact.d.ts +12 -0
- memos/_js/redact.d.ts.map +1 -0
- memos/_js/redact.js +26 -0
- memos/_js/redact.js.map +1 -0
- memos/_js/repl.d.ts +57 -0
- memos/_js/repl.d.ts.map +1 -0
- memos/_js/repl.js +295 -0
- memos/_js/repl.js.map +1 -0
- memos/_js/rerank-local.d.ts +61 -0
- memos/_js/rerank-local.d.ts.map +1 -0
- memos/_js/rerank-local.js +210 -0
- memos/_js/rerank-local.js.map +1 -0
- memos/_js/retain-filter.d.ts +98 -0
- memos/_js/retain-filter.d.ts.map +1 -0
- memos/_js/retain-filter.js +232 -0
- memos/_js/retain-filter.js.map +1 -0
- memos/_js/retrieval.d.ts +265 -0
- memos/_js/retrieval.d.ts.map +1 -0
- memos/_js/retrieval.js +1018 -0
- memos/_js/retrieval.js.map +1 -0
- memos/_js/revert.d.ts +77 -0
- memos/_js/revert.d.ts.map +1 -0
- memos/_js/revert.js +252 -0
- memos/_js/revert.js.map +1 -0
- memos/_js/scope.d.ts +30 -0
- memos/_js/scope.d.ts.map +1 -0
- memos/_js/scope.js +71 -0
- memos/_js/scope.js.map +1 -0
- memos/_js/semantic-screen.d.ts +152 -0
- memos/_js/semantic-screen.d.ts.map +1 -0
- memos/_js/semantic-screen.js +290 -0
- memos/_js/semantic-screen.js.map +1 -0
- memos/_js/session-hooks.d.ts +127 -0
- memos/_js/session-hooks.d.ts.map +1 -0
- memos/_js/session-hooks.js +482 -0
- memos/_js/session-hooks.js.map +1 -0
- memos/_js/storage/sqlite.d.ts +427 -0
- memos/_js/storage/sqlite.d.ts.map +1 -0
- memos/_js/storage/sqlite.js +2428 -0
- memos/_js/storage/sqlite.js.map +1 -0
- memos/_js/sync.d.ts +213 -0
- memos/_js/sync.d.ts.map +1 -0
- memos/_js/sync.js +389 -0
- memos/_js/sync.js.map +1 -0
- memos/_js/tamper-log.d.ts +98 -0
- memos/_js/tamper-log.d.ts.map +1 -0
- memos/_js/tamper-log.js +327 -0
- memos/_js/tamper-log.js.map +1 -0
- memos/_js/temporal.d.ts +92 -0
- memos/_js/temporal.d.ts.map +1 -0
- memos/_js/temporal.js +536 -0
- memos/_js/temporal.js.map +1 -0
- memos/_js/time-travel.d.ts +38 -0
- memos/_js/time-travel.d.ts.map +1 -0
- memos/_js/time-travel.js +46 -0
- memos/_js/time-travel.js.map +1 -0
- memos/_js/timestamp-format.d.ts +18 -0
- memos/_js/timestamp-format.d.ts.map +1 -0
- memos/_js/timestamp-format.js +52 -0
- memos/_js/timestamp-format.js.map +1 -0
- memos/_js/triple-extraction.d.ts +31 -0
- memos/_js/triple-extraction.d.ts.map +1 -0
- memos/_js/triple-extraction.js +116 -0
- memos/_js/triple-extraction.js.map +1 -0
- memos/_js/types.d.ts +2474 -0
- memos/_js/types.d.ts.map +1 -0
- memos/_js/types.js +20 -0
- memos/_js/types.js.map +1 -0
- memos/_js/updater.d.ts +95 -0
- memos/_js/updater.d.ts.map +1 -0
- memos/_js/updater.js +268 -0
- memos/_js/updater.js.map +1 -0
- memos/_js/version.d.ts +17 -0
- memos/_js/version.d.ts.map +1 -0
- memos/_js/version.js +34 -0
- memos/_js/version.js.map +1 -0
- memos/_js/vision/clip-query-cache.d.ts +58 -0
- memos/_js/vision/clip-query-cache.d.ts.map +1 -0
- memos/_js/vision/clip-query-cache.js +79 -0
- memos/_js/vision/clip-query-cache.js.map +1 -0
- memos/_js/vision/clip.d.ts +113 -0
- memos/_js/vision/clip.d.ts.map +1 -0
- memos/_js/vision/clip.js +224 -0
- memos/_js/vision/clip.js.map +1 -0
- memos/_js/vision/fake-clip.d.ts +55 -0
- memos/_js/vision/fake-clip.d.ts.map +1 -0
- memos/_js/vision/fake-clip.js +122 -0
- memos/_js/vision/fake-clip.js.map +1 -0
- memos/_js/vision/index.d.ts +15 -0
- memos/_js/vision/index.d.ts.map +1 -0
- memos/_js/vision/index.js +13 -0
- memos/_js/vision/index.js.map +1 -0
- memos/_js/vision/store.d.ts +137 -0
- memos/_js/vision/store.d.ts.map +1 -0
- memos/_js/vision/store.js +383 -0
- memos/_js/vision/store.js.map +1 -0
- memos/adapters/__init__.py +0 -0
- memos/adapters/crewai.py +385 -0
- memos/adapters/langchain.py +301 -0
- memos/adapters/ollama.py +396 -0
- memos/server/__init__.py +0 -0
- memos/server/main.py +547 -0
- memos/server/routes.py +360 -0
memos/adapters/ollama.py
ADDED
|
@@ -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.")
|
memos/server/__init__.py
ADDED
|
File without changes
|