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/crewai.py
ADDED
|
@@ -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
|