muninn-python 0.1.0__tar.gz → 0.2.4__tar.gz
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.
- muninn_python-0.2.4/.gitignore +73 -0
- {muninn_python-0.1.0 → muninn_python-0.2.4}/PKG-INFO +1 -1
- {muninn_python-0.1.0 → muninn_python-0.2.4}/pyproject.toml +1 -1
- muninn_python-0.1.0/.gitignore +0 -17
- muninn_python-0.1.0/muninn/__init__.py +0 -57
- muninn_python-0.1.0/muninn/client.py +0 -499
- muninn_python-0.1.0/muninn/errors.py +0 -47
- muninn_python-0.1.0/muninn/langchain.py +0 -184
- muninn_python-0.1.0/muninn/sse.py +0 -122
- muninn_python-0.1.0/muninn/types.py +0 -132
- {muninn_python-0.1.0 → muninn_python-0.2.4}/README.md +0 -0
- {muninn_python-0.1.0 → muninn_python-0.2.4}/requirements.txt +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Compiled binaries
|
|
2
|
+
/muninn
|
|
3
|
+
/muninndb
|
|
4
|
+
/muninndb-server
|
|
5
|
+
/bench
|
|
6
|
+
/bench-test
|
|
7
|
+
/eval
|
|
8
|
+
/diag
|
|
9
|
+
/muninndb-test
|
|
10
|
+
|
|
11
|
+
# Embed assets — downloaded via `make fetch-assets`, not committed (too large)
|
|
12
|
+
internal/plugin/embed/assets/*.dylib
|
|
13
|
+
internal/plugin/embed/assets/*.so
|
|
14
|
+
internal/plugin/embed/assets/*.dll
|
|
15
|
+
internal/plugin/embed/assets/*.onnx
|
|
16
|
+
internal/plugin/embed/assets/tokenizer.json
|
|
17
|
+
|
|
18
|
+
# Binary artifacts — never commit
|
|
19
|
+
*.muninn
|
|
20
|
+
*.dylib
|
|
21
|
+
*.so
|
|
22
|
+
*.dll
|
|
23
|
+
*.onnx
|
|
24
|
+
*.exe
|
|
25
|
+
|
|
26
|
+
# Local data directories
|
|
27
|
+
muninndb-data/
|
|
28
|
+
**/muninndb-data/
|
|
29
|
+
/tmp/
|
|
30
|
+
|
|
31
|
+
# Dependencies — install locally, never commit
|
|
32
|
+
**/node_modules/
|
|
33
|
+
**/.venv/
|
|
34
|
+
**/venv/
|
|
35
|
+
**/__pycache__/
|
|
36
|
+
*.pyc
|
|
37
|
+
*.bak
|
|
38
|
+
|
|
39
|
+
# macOS
|
|
40
|
+
.DS_Store
|
|
41
|
+
|
|
42
|
+
# Go test cache
|
|
43
|
+
*.test
|
|
44
|
+
*.out
|
|
45
|
+
|
|
46
|
+
# Worktrees
|
|
47
|
+
.worktrees/
|
|
48
|
+
|
|
49
|
+
# Bible eval testdata (downloaded via scripts/eval-bible-setup.sh)
|
|
50
|
+
testdata/bible/kjv.json
|
|
51
|
+
testdata/bible/cross-refs.tsv
|
|
52
|
+
testdata/bible/cross-refs.csv
|
|
53
|
+
testdata/bible/*.muninn
|
|
54
|
+
|
|
55
|
+
# Planning and design docs — not committed
|
|
56
|
+
docs/plans/
|
|
57
|
+
|
|
58
|
+
# Evaluation tools and results — internal development only
|
|
59
|
+
cmd/eval*/
|
|
60
|
+
eval-results/
|
|
61
|
+
scripts/eval-*
|
|
62
|
+
/eval-bible
|
|
63
|
+
/eval-temporal
|
|
64
|
+
/eval-expert
|
|
65
|
+
/eval-paraphrase
|
|
66
|
+
/eval
|
|
67
|
+
sdk/node/node_modules/
|
|
68
|
+
sdk/node/dist/
|
|
69
|
+
sdk/python/dist/
|
|
70
|
+
sdk/python/*.egg-info/
|
|
71
|
+
sdk/muninndb/dist/
|
|
72
|
+
sdk/muninndb/*.egg-info/
|
|
73
|
+
sdk/php/vendor/
|
muninn_python-0.1.0/.gitignore
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""MuninnDB Python SDK - Async client for cognitive memory database."""
|
|
2
|
-
|
|
3
|
-
from .client import MuninnClient
|
|
4
|
-
from .errors import (
|
|
5
|
-
MuninnAuthError,
|
|
6
|
-
MuninnConflict,
|
|
7
|
-
MuninnConnectionError,
|
|
8
|
-
MuninnError,
|
|
9
|
-
MuninnNotFound,
|
|
10
|
-
MuninnServerError,
|
|
11
|
-
MuninnTimeoutError,
|
|
12
|
-
)
|
|
13
|
-
from .types import (
|
|
14
|
-
ActivateRequest,
|
|
15
|
-
ActivateResponse,
|
|
16
|
-
ActivationItem,
|
|
17
|
-
BriefSentence,
|
|
18
|
-
CoherenceResult,
|
|
19
|
-
Push,
|
|
20
|
-
ReadResponse,
|
|
21
|
-
StatResponse,
|
|
22
|
-
WriteRequest,
|
|
23
|
-
WriteResponse,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
__version__ = "0.1.0"
|
|
27
|
-
|
|
28
|
-
# LangChain integration — only imported if langchain-core is installed.
|
|
29
|
-
# Usage: from muninn.langchain import MuninnDBMemory
|
|
30
|
-
def __getattr__(name: str):
|
|
31
|
-
if name == "MuninnDBMemory":
|
|
32
|
-
from .langchain import MuninnDBMemory
|
|
33
|
-
return MuninnDBMemory
|
|
34
|
-
raise AttributeError(f"module 'muninn' has no attribute {name!r}")
|
|
35
|
-
|
|
36
|
-
__all__ = [
|
|
37
|
-
"MuninnClient",
|
|
38
|
-
"MuninnError",
|
|
39
|
-
"MuninnAuthError",
|
|
40
|
-
"MuninnConnectionError",
|
|
41
|
-
"MuninnNotFound",
|
|
42
|
-
"MuninnConflict",
|
|
43
|
-
"MuninnServerError",
|
|
44
|
-
"MuninnTimeoutError",
|
|
45
|
-
"WriteRequest",
|
|
46
|
-
"WriteResponse",
|
|
47
|
-
"ActivateRequest",
|
|
48
|
-
"ActivateResponse",
|
|
49
|
-
"ActivationItem",
|
|
50
|
-
"BriefSentence",
|
|
51
|
-
"ReadResponse",
|
|
52
|
-
"StatResponse",
|
|
53
|
-
"CoherenceResult",
|
|
54
|
-
"Push",
|
|
55
|
-
# Optional (requires langchain-core):
|
|
56
|
-
"MuninnDBMemory",
|
|
57
|
-
]
|
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
"""Async MuninnDB client."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import random
|
|
8
|
-
|
|
9
|
-
import httpx
|
|
10
|
-
|
|
11
|
-
from .errors import (
|
|
12
|
-
MuninnAuthError,
|
|
13
|
-
MuninnConnectionError,
|
|
14
|
-
MuninnConflict,
|
|
15
|
-
MuninnError,
|
|
16
|
-
MuninnNotFound,
|
|
17
|
-
MuninnServerError,
|
|
18
|
-
MuninnTimeoutError,
|
|
19
|
-
)
|
|
20
|
-
from .sse import SSEStream
|
|
21
|
-
from .types import (
|
|
22
|
-
ActivateResponse,
|
|
23
|
-
ActivationItem,
|
|
24
|
-
BriefSentence,
|
|
25
|
-
CoherenceResult,
|
|
26
|
-
ReadResponse,
|
|
27
|
-
StatResponse,
|
|
28
|
-
WriteResponse,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class MuninnClient:
|
|
33
|
-
"""Async client for MuninnDB REST API.
|
|
34
|
-
|
|
35
|
-
The client uses httpx for async HTTP and supports automatic retry with
|
|
36
|
-
exponential backoff for transient failures.
|
|
37
|
-
|
|
38
|
-
Usage:
|
|
39
|
-
async with MuninnClient("http://localhost:8476") as client:
|
|
40
|
-
eng_id = await client.write(
|
|
41
|
-
vault="default",
|
|
42
|
-
concept="memory concept",
|
|
43
|
-
content="memory content"
|
|
44
|
-
)
|
|
45
|
-
results = await client.activate(
|
|
46
|
-
vault="default",
|
|
47
|
-
context=["search query"]
|
|
48
|
-
)
|
|
49
|
-
async for push in client.subscribe(vault="default"):
|
|
50
|
-
print(f"New engram: {push.engram_id}")
|
|
51
|
-
break
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
base_url: Base URL of MuninnDB server (default: http://localhost:8476)
|
|
55
|
-
token: Optional Bearer token for authentication
|
|
56
|
-
timeout: Request timeout in seconds (default: 5.0)
|
|
57
|
-
max_retries: Maximum retry attempts for transient errors (default: 3)
|
|
58
|
-
retry_backoff: Initial backoff multiplier for retries (default: 0.5)
|
|
59
|
-
max_connections: Max concurrent connections (default: 20)
|
|
60
|
-
keepalive_connections: Max keepalive connections (default: 10)
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def __init__(
|
|
64
|
-
self,
|
|
65
|
-
base_url: str = "http://localhost:8476",
|
|
66
|
-
token: str | None = None,
|
|
67
|
-
timeout: float = 5.0,
|
|
68
|
-
max_retries: int = 3,
|
|
69
|
-
retry_backoff: float = 0.5,
|
|
70
|
-
max_connections: int = 20,
|
|
71
|
-
keepalive_connections: int = 10,
|
|
72
|
-
):
|
|
73
|
-
self._base_url = base_url.rstrip("/")
|
|
74
|
-
self._token = token
|
|
75
|
-
self._timeout = timeout
|
|
76
|
-
self._max_retries = max_retries
|
|
77
|
-
self._retry_backoff = retry_backoff
|
|
78
|
-
self._max_connections = max_connections
|
|
79
|
-
self._keepalive_connections = keepalive_connections
|
|
80
|
-
self._http: httpx.AsyncClient | None = None
|
|
81
|
-
|
|
82
|
-
async def __aenter__(self):
|
|
83
|
-
"""Enter async context."""
|
|
84
|
-
self._http = httpx.AsyncClient(
|
|
85
|
-
base_url=self._base_url,
|
|
86
|
-
timeout=self._timeout,
|
|
87
|
-
limits=httpx.Limits(
|
|
88
|
-
max_connections=self._max_connections,
|
|
89
|
-
max_keepalive_connections=self._keepalive_connections,
|
|
90
|
-
),
|
|
91
|
-
headers=self._default_headers(),
|
|
92
|
-
)
|
|
93
|
-
return self
|
|
94
|
-
|
|
95
|
-
async def __aexit__(self, *args):
|
|
96
|
-
"""Exit async context."""
|
|
97
|
-
if self._http:
|
|
98
|
-
await self._http.aclose()
|
|
99
|
-
|
|
100
|
-
async def write(
|
|
101
|
-
self,
|
|
102
|
-
vault: str = "default",
|
|
103
|
-
concept: str = "",
|
|
104
|
-
content: str = "",
|
|
105
|
-
tags: list[str] | None = None,
|
|
106
|
-
confidence: float = 0.9,
|
|
107
|
-
stability: float = 0.5,
|
|
108
|
-
) -> str:
|
|
109
|
-
"""Write an engram to the database.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
vault: Vault name (default: "default")
|
|
113
|
-
concept: Concept/title for this engram
|
|
114
|
-
content: Main content/body
|
|
115
|
-
tags: Optional list of tags for categorization
|
|
116
|
-
confidence: Confidence score 0-1 (default: 0.9)
|
|
117
|
-
stability: Stability score 0-1 (default: 0.5)
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
ULID string ID of the created engram
|
|
121
|
-
|
|
122
|
-
Raises:
|
|
123
|
-
MuninnError: If write fails
|
|
124
|
-
"""
|
|
125
|
-
body = {
|
|
126
|
-
"vault": vault,
|
|
127
|
-
"concept": concept,
|
|
128
|
-
"content": content,
|
|
129
|
-
"confidence": confidence,
|
|
130
|
-
"stability": stability,
|
|
131
|
-
}
|
|
132
|
-
if tags:
|
|
133
|
-
body["tags"] = tags
|
|
134
|
-
|
|
135
|
-
response = await self._request("POST", "/api/engrams", json=body)
|
|
136
|
-
return response.get("id", "")
|
|
137
|
-
|
|
138
|
-
async def activate(
|
|
139
|
-
self,
|
|
140
|
-
vault: str = "default",
|
|
141
|
-
context: list[str] | None = None,
|
|
142
|
-
max_results: int = 10,
|
|
143
|
-
threshold: float = 0.1,
|
|
144
|
-
max_hops: int = 0,
|
|
145
|
-
include_why: bool = False,
|
|
146
|
-
brief_mode: str = "auto",
|
|
147
|
-
) -> ActivateResponse:
|
|
148
|
-
"""Activate memory using semantic search and graph traversal.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
vault: Vault name (default: "default")
|
|
152
|
-
context: List of query terms/context
|
|
153
|
-
max_results: Max results to return (default: 10)
|
|
154
|
-
threshold: Min activation score threshold (default: 0.1)
|
|
155
|
-
max_hops: Max graph hops to traverse (default: 0)
|
|
156
|
-
include_why: Include reasoning/why field (default: False)
|
|
157
|
-
brief_mode: Brief extraction mode - "auto", "extractive", "abstractive" (default: "auto")
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
ActivateResponse with activations and optional brief
|
|
161
|
-
|
|
162
|
-
Raises:
|
|
163
|
-
MuninnError: If activation fails
|
|
164
|
-
"""
|
|
165
|
-
if context is None:
|
|
166
|
-
context = []
|
|
167
|
-
|
|
168
|
-
body = {
|
|
169
|
-
"vault": vault,
|
|
170
|
-
"context": context,
|
|
171
|
-
"max_results": max_results,
|
|
172
|
-
"threshold": threshold,
|
|
173
|
-
"max_hops": max_hops,
|
|
174
|
-
"include_why": include_why,
|
|
175
|
-
"brief_mode": brief_mode,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
response = await self._request("POST", "/api/activate", json=body)
|
|
179
|
-
|
|
180
|
-
# Support both snake_case (new) and PascalCase (old) field names for
|
|
181
|
-
# backward compatibility with servers that have not yet been updated.
|
|
182
|
-
raw_activations = response.get("activations") or response.get("Activations") or []
|
|
183
|
-
activations = [
|
|
184
|
-
ActivationItem(
|
|
185
|
-
id=item.get("id") or item.get("ID", ""),
|
|
186
|
-
concept=item.get("concept") or item.get("Concept", ""),
|
|
187
|
-
content=item.get("content") or item.get("Content", ""),
|
|
188
|
-
score=item.get("score") or item.get("Score", 0.0),
|
|
189
|
-
confidence=item.get("confidence") or item.get("Confidence", 0.0),
|
|
190
|
-
why=item.get("why") or item.get("Why"),
|
|
191
|
-
hop_path=item.get("hop_path") or item.get("HopPath"),
|
|
192
|
-
dormant=item.get("dormant") or item.get("Dormant", False),
|
|
193
|
-
)
|
|
194
|
-
for item in raw_activations
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
brief = None
|
|
198
|
-
raw_brief = response.get("brief") or response.get("Brief")
|
|
199
|
-
if raw_brief:
|
|
200
|
-
brief = [
|
|
201
|
-
BriefSentence(
|
|
202
|
-
engram_id=sent.get("engram_id") or sent.get("EngramID", ""),
|
|
203
|
-
text=sent.get("text") or sent.get("Text", ""),
|
|
204
|
-
score=sent.get("score") or sent.get("Score", 0.0),
|
|
205
|
-
)
|
|
206
|
-
for sent in raw_brief
|
|
207
|
-
]
|
|
208
|
-
|
|
209
|
-
return ActivateResponse(
|
|
210
|
-
query_id=response.get("query_id") or response.get("QueryID", ""),
|
|
211
|
-
total_found=response.get("total_found") or response.get("TotalFound", 0),
|
|
212
|
-
activations=activations,
|
|
213
|
-
latency_ms=response.get("latency_ms") or response.get("LatencyMs", 0.0),
|
|
214
|
-
brief=brief,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
async def read(self, id: str, vault: str = "default") -> ReadResponse:
|
|
218
|
-
"""Read a specific engram by ID.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
id: Engram ULID
|
|
222
|
-
vault: Vault name (default: "default")
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
ReadResponse with engram details
|
|
226
|
-
|
|
227
|
-
Raises:
|
|
228
|
-
MuninnNotFound: If engram doesn't exist
|
|
229
|
-
MuninnError: If read fails
|
|
230
|
-
"""
|
|
231
|
-
response = await self._request("GET", f"/api/engrams/{id}", params={"vault": vault})
|
|
232
|
-
|
|
233
|
-
coherence = response.get("coherence")
|
|
234
|
-
return ReadResponse(
|
|
235
|
-
id=response.get("id", ""),
|
|
236
|
-
concept=response.get("concept", ""),
|
|
237
|
-
content=response.get("content", ""),
|
|
238
|
-
confidence=response.get("confidence", 0.0),
|
|
239
|
-
relevance=response.get("relevance", 0.0),
|
|
240
|
-
stability=response.get("stability", 0.0),
|
|
241
|
-
access_count=response.get("access_count", 0),
|
|
242
|
-
tags=response.get("tags", []),
|
|
243
|
-
state=response.get("state", ""),
|
|
244
|
-
created_at=response.get("created_at", 0),
|
|
245
|
-
updated_at=response.get("updated_at", 0),
|
|
246
|
-
last_access=response.get("last_access"),
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
async def forget(self, id: str, vault: str = "default", hard: bool = False) -> bool:
|
|
250
|
-
"""Delete an engram (soft or hard delete).
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
id: Engram ULID
|
|
254
|
-
vault: Vault name (default: "default")
|
|
255
|
-
hard: If True, hard delete (cannot recover). If False, soft delete (default: False)
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
True if deletion successful
|
|
259
|
-
|
|
260
|
-
Raises:
|
|
261
|
-
MuninnNotFound: If engram doesn't exist
|
|
262
|
-
MuninnError: If deletion fails
|
|
263
|
-
"""
|
|
264
|
-
if hard:
|
|
265
|
-
await self._request(
|
|
266
|
-
"POST",
|
|
267
|
-
f"/api/engrams/{id}/forget",
|
|
268
|
-
params={"vault": vault, "hard": "true"},
|
|
269
|
-
)
|
|
270
|
-
else:
|
|
271
|
-
await self._request(
|
|
272
|
-
"DELETE",
|
|
273
|
-
f"/api/engrams/{id}",
|
|
274
|
-
params={"vault": vault},
|
|
275
|
-
)
|
|
276
|
-
return True
|
|
277
|
-
|
|
278
|
-
async def link(
|
|
279
|
-
self,
|
|
280
|
-
source_id: str,
|
|
281
|
-
target_id: str,
|
|
282
|
-
vault: str = "default",
|
|
283
|
-
rel_type: int = 5,
|
|
284
|
-
weight: float = 1.0,
|
|
285
|
-
) -> bool:
|
|
286
|
-
"""Create an association/link between two engrams.
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
source_id: Source engram ULID
|
|
290
|
-
target_id: Target engram ULID
|
|
291
|
-
vault: Vault name (default: "default")
|
|
292
|
-
rel_type: Relationship type code (default: 5)
|
|
293
|
-
weight: Link weight/strength (default: 1.0)
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
True if link created successfully
|
|
297
|
-
|
|
298
|
-
Raises:
|
|
299
|
-
MuninnError: If link creation fails
|
|
300
|
-
"""
|
|
301
|
-
body = {
|
|
302
|
-
"vault": vault,
|
|
303
|
-
"source_id": source_id,
|
|
304
|
-
"target_id": target_id,
|
|
305
|
-
"rel_type": rel_type,
|
|
306
|
-
"weight": weight,
|
|
307
|
-
}
|
|
308
|
-
await self._request("POST", "/api/link", json=body)
|
|
309
|
-
return True
|
|
310
|
-
|
|
311
|
-
async def stats(self) -> StatResponse:
|
|
312
|
-
"""Get database statistics including coherence scores.
|
|
313
|
-
|
|
314
|
-
Returns:
|
|
315
|
-
StatResponse with engram count, vault count, storage bytes, and coherence
|
|
316
|
-
|
|
317
|
-
Raises:
|
|
318
|
-
MuninnError: If stats request fails
|
|
319
|
-
"""
|
|
320
|
-
response = await self._request("GET", "/api/stats")
|
|
321
|
-
|
|
322
|
-
coherence = None
|
|
323
|
-
if response.get("coherence"):
|
|
324
|
-
coherence = {
|
|
325
|
-
vault_name: CoherenceResult(
|
|
326
|
-
score=data.get("score", 0.0),
|
|
327
|
-
orphan_ratio=data.get("orphan_ratio", 0.0),
|
|
328
|
-
contradiction_density=data.get("contradiction_density", 0.0),
|
|
329
|
-
duplication_pressure=data.get("duplication_pressure", 0.0),
|
|
330
|
-
decay_variance=data.get("decay_variance", 0.0),
|
|
331
|
-
total_engrams=data.get("total_engrams", 0),
|
|
332
|
-
)
|
|
333
|
-
for vault_name, data in response["coherence"].items()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return StatResponse(
|
|
337
|
-
engram_count=response.get("engram_count", 0),
|
|
338
|
-
vault_count=response.get("vault_count", 0),
|
|
339
|
-
storage_bytes=response.get("storage_bytes", 0),
|
|
340
|
-
coherence=coherence,
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
def subscribe(
|
|
344
|
-
self,
|
|
345
|
-
vault: str = "default",
|
|
346
|
-
push_on_write: bool = True,
|
|
347
|
-
threshold: float = 0.0,
|
|
348
|
-
) -> SSEStream:
|
|
349
|
-
"""Subscribe to vault events via Server-Sent Events (SSE).
|
|
350
|
-
|
|
351
|
-
This returns an async iterable that yields Push events when engrams are
|
|
352
|
-
written to the vault. The stream automatically reconnects on network errors.
|
|
353
|
-
|
|
354
|
-
Usage:
|
|
355
|
-
stream = client.subscribe(vault="default")
|
|
356
|
-
async for push in stream:
|
|
357
|
-
print(f"New engram: {push.engram_id}")
|
|
358
|
-
if condition:
|
|
359
|
-
await stream.close()
|
|
360
|
-
|
|
361
|
-
Args:
|
|
362
|
-
vault: Vault to subscribe to (default: "default")
|
|
363
|
-
push_on_write: Emit push events on new writes (default: True)
|
|
364
|
-
threshold: Min activation threshold for push events (default: 0.0)
|
|
365
|
-
|
|
366
|
-
Returns:
|
|
367
|
-
SSEStream async iterable
|
|
368
|
-
|
|
369
|
-
Raises:
|
|
370
|
-
MuninnError: If subscription fails
|
|
371
|
-
"""
|
|
372
|
-
params = {
|
|
373
|
-
"vault": vault,
|
|
374
|
-
"push_on_write": str(push_on_write).lower(),
|
|
375
|
-
}
|
|
376
|
-
if threshold:
|
|
377
|
-
params["threshold"] = str(threshold)
|
|
378
|
-
|
|
379
|
-
return SSEStream(self, "/api/subscribe", params)
|
|
380
|
-
|
|
381
|
-
async def health(self) -> bool:
|
|
382
|
-
"""Check if MuninnDB server is healthy.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
True if server responds with 200 OK
|
|
386
|
-
|
|
387
|
-
Raises:
|
|
388
|
-
MuninnError: If health check fails
|
|
389
|
-
"""
|
|
390
|
-
try:
|
|
391
|
-
response = await self._request("GET", "/health")
|
|
392
|
-
return response.get("status") == "ok"
|
|
393
|
-
except MuninnError:
|
|
394
|
-
return False
|
|
395
|
-
|
|
396
|
-
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
397
|
-
"""Make an HTTP request with automatic retry logic.
|
|
398
|
-
|
|
399
|
-
Retries on transient errors (502, 503, 504, connection/read errors).
|
|
400
|
-
Does not retry on 4xx errors. Uses exponential backoff with jitter.
|
|
401
|
-
|
|
402
|
-
Args:
|
|
403
|
-
method: HTTP method (GET, POST, DELETE, etc)
|
|
404
|
-
path: URL path relative to base_url
|
|
405
|
-
**kwargs: Additional arguments to pass to httpx
|
|
406
|
-
|
|
407
|
-
Returns:
|
|
408
|
-
Parsed JSON response as dict
|
|
409
|
-
|
|
410
|
-
Raises:
|
|
411
|
-
MuninnAuthError: 401 Unauthorized
|
|
412
|
-
MuninnNotFound: 404 Not Found
|
|
413
|
-
MuninnConflict: 409 Conflict
|
|
414
|
-
MuninnServerError: 5xx errors
|
|
415
|
-
MuninnTimeoutError: Request timeout
|
|
416
|
-
MuninnConnectionError: Connection error
|
|
417
|
-
MuninnError: Other HTTP errors
|
|
418
|
-
"""
|
|
419
|
-
if not self._http:
|
|
420
|
-
raise MuninnError("Client not initialized. Use 'async with' context manager.")
|
|
421
|
-
|
|
422
|
-
attempt = 0
|
|
423
|
-
while attempt <= self._max_retries:
|
|
424
|
-
try:
|
|
425
|
-
response = await self._http.request(method, path, **kwargs)
|
|
426
|
-
self._raise_for_status(response)
|
|
427
|
-
return response.json()
|
|
428
|
-
|
|
429
|
-
except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError) as e:
|
|
430
|
-
if attempt >= self._max_retries:
|
|
431
|
-
raise MuninnConnectionError(f"Connection failed: {str(e)}")
|
|
432
|
-
await self._backoff(attempt)
|
|
433
|
-
attempt += 1
|
|
434
|
-
|
|
435
|
-
except httpx.ReadTimeout as e:
|
|
436
|
-
if attempt >= self._max_retries:
|
|
437
|
-
raise MuninnTimeoutError(f"Request timeout: {str(e)}")
|
|
438
|
-
await self._backoff(attempt)
|
|
439
|
-
attempt += 1
|
|
440
|
-
|
|
441
|
-
except httpx.HTTPStatusError as e:
|
|
442
|
-
# Don't retry on 4xx (except certain ones), do retry on 5xx
|
|
443
|
-
if 500 <= e.response.status_code < 600:
|
|
444
|
-
if attempt >= self._max_retries:
|
|
445
|
-
self._raise_for_status(e.response)
|
|
446
|
-
await self._backoff(attempt)
|
|
447
|
-
attempt += 1
|
|
448
|
-
else:
|
|
449
|
-
self._raise_for_status(e.response)
|
|
450
|
-
|
|
451
|
-
except MuninnError:
|
|
452
|
-
raise
|
|
453
|
-
|
|
454
|
-
raise MuninnError("Max retries exceeded")
|
|
455
|
-
|
|
456
|
-
async def _backoff(self, attempt: int):
|
|
457
|
-
"""Wait with exponential backoff + jitter.
|
|
458
|
-
|
|
459
|
-
Args:
|
|
460
|
-
attempt: Attempt number (0-indexed)
|
|
461
|
-
"""
|
|
462
|
-
delay = self._retry_backoff * (2 ** attempt) + random.uniform(0, 0.1)
|
|
463
|
-
await asyncio.sleep(delay)
|
|
464
|
-
|
|
465
|
-
def _default_headers(self) -> dict:
|
|
466
|
-
"""Build default request headers."""
|
|
467
|
-
headers = {"Content-Type": "application/json"}
|
|
468
|
-
if self._token:
|
|
469
|
-
headers["Authorization"] = f"Bearer {self._token}"
|
|
470
|
-
return headers
|
|
471
|
-
|
|
472
|
-
def _raise_for_status(self, response: httpx.Response):
|
|
473
|
-
"""Convert httpx response to appropriate MuninnError.
|
|
474
|
-
|
|
475
|
-
Args:
|
|
476
|
-
response: httpx Response object
|
|
477
|
-
|
|
478
|
-
Raises:
|
|
479
|
-
Appropriate MuninnError subclass
|
|
480
|
-
"""
|
|
481
|
-
if response.status_code == 401:
|
|
482
|
-
raise MuninnAuthError(
|
|
483
|
-
"Authentication required. Provide token= parameter to MuninnClient.",
|
|
484
|
-
401,
|
|
485
|
-
)
|
|
486
|
-
elif response.status_code == 404:
|
|
487
|
-
raise MuninnNotFound(f"Not found: {response.text}", 404)
|
|
488
|
-
elif response.status_code == 409:
|
|
489
|
-
raise MuninnConflict(f"Conflict: {response.text}", 409)
|
|
490
|
-
elif 500 <= response.status_code < 600:
|
|
491
|
-
raise MuninnServerError(
|
|
492
|
-
f"Server error {response.status_code}: {response.text}",
|
|
493
|
-
response.status_code,
|
|
494
|
-
)
|
|
495
|
-
elif response.status_code >= 400:
|
|
496
|
-
raise MuninnError(
|
|
497
|
-
f"Client error {response.status_code}: {response.text}",
|
|
498
|
-
response.status_code,
|
|
499
|
-
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"""MuninnDB error types."""
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MuninnError(Exception):
|
|
7
|
-
"""Base exception for all MuninnDB errors."""
|
|
8
|
-
|
|
9
|
-
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
10
|
-
super().__init__(message)
|
|
11
|
-
self.status_code = status_code
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class MuninnConnectionError(MuninnError):
|
|
15
|
-
"""Connection-related errors (network, SSL, DNS)."""
|
|
16
|
-
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class MuninnAuthError(MuninnError):
|
|
21
|
-
"""Authentication failed (401 Unauthorized)."""
|
|
22
|
-
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class MuninnNotFound(MuninnError):
|
|
27
|
-
"""Resource not found (404 Not Found)."""
|
|
28
|
-
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class MuninnConflict(MuninnError):
|
|
33
|
-
"""Request conflict (409 Conflict)."""
|
|
34
|
-
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class MuninnServerError(MuninnError):
|
|
39
|
-
"""Server error (5xx)."""
|
|
40
|
-
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class MuninnTimeoutError(MuninnError):
|
|
45
|
-
"""Request timeout."""
|
|
46
|
-
|
|
47
|
-
pass
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
"""LangChain memory integration for MuninnDB.
|
|
2
|
-
|
|
3
|
-
Provides MuninnDBMemory, a drop-in replacement for any LangChain memory backend.
|
|
4
|
-
Unlike traditional backends (ConversationBufferMemory, etc.), MuninnDB applies
|
|
5
|
-
cognitive primitives to retrieved context: relevance decays over time, frequently
|
|
6
|
-
recalled memories strengthen, and associations form automatically from co-activation.
|
|
7
|
-
|
|
8
|
-
Install:
|
|
9
|
-
pip install muninn-python[langchain]
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
from muninn.langchain import MuninnDBMemory
|
|
13
|
-
from langchain.chains import ConversationChain
|
|
14
|
-
from langchain_anthropic import ChatAnthropic
|
|
15
|
-
|
|
16
|
-
memory = MuninnDBMemory(vault="my-agent")
|
|
17
|
-
chain = ConversationChain(llm=ChatAnthropic(model="claude-haiku-4-5-20251001"), memory=memory)
|
|
18
|
-
chain.predict(input="What did we discuss about the payment service?")
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import asyncio
|
|
24
|
-
import concurrent.futures
|
|
25
|
-
import textwrap
|
|
26
|
-
from typing import Any, Dict, List, Optional
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
from langchain_core.memory import BaseMemory
|
|
30
|
-
except ImportError:
|
|
31
|
-
try:
|
|
32
|
-
from langchain.memory import BaseMemory # type: ignore[no-redef]
|
|
33
|
-
except ImportError as e:
|
|
34
|
-
raise ImportError(
|
|
35
|
-
"LangChain is required for MuninnDBMemory. "
|
|
36
|
-
"Install it with: pip install muninn-python[langchain]"
|
|
37
|
-
) from e
|
|
38
|
-
|
|
39
|
-
from .client import MuninnClient
|
|
40
|
-
from .types import ActivationItem
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _run_sync(coro: Any) -> Any:
|
|
44
|
-
"""Run an async coroutine synchronously.
|
|
45
|
-
|
|
46
|
-
Handles both contexts where there is no event loop (plain scripts, pytest)
|
|
47
|
-
and contexts where one is already running (FastAPI, Jupyter, async test runners).
|
|
48
|
-
"""
|
|
49
|
-
try:
|
|
50
|
-
asyncio.get_running_loop()
|
|
51
|
-
# Already inside an event loop — run in a fresh thread with its own loop.
|
|
52
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
53
|
-
return pool.submit(asyncio.run, coro).result()
|
|
54
|
-
except RuntimeError:
|
|
55
|
-
# No running event loop — safe to call asyncio.run() directly.
|
|
56
|
-
return asyncio.run(coro)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class MuninnDBMemory(BaseMemory):
|
|
60
|
-
"""LangChain memory backend powered by MuninnDB.
|
|
61
|
-
|
|
62
|
-
Each conversation turn is stored as a single engram (human input + AI output).
|
|
63
|
-
On every load, MuninnDB activates the most relevant memories for the current
|
|
64
|
-
input using semantic similarity, Hebbian association weights, and decay curves —
|
|
65
|
-
returning only what is genuinely relevant right now, not a raw chat buffer.
|
|
66
|
-
|
|
67
|
-
Attributes:
|
|
68
|
-
base_url: MuninnDB server URL (default: http://localhost:8475)
|
|
69
|
-
token: Optional Bearer token if MCP auth is enabled
|
|
70
|
-
vault: Vault name to store memories in (default: "default")
|
|
71
|
-
max_results: Max memories to surface per activation (default: 10)
|
|
72
|
-
memory_key: Key injected into chain inputs (default: "history")
|
|
73
|
-
input_key: Input dict key holding the human message. Auto-detected
|
|
74
|
-
if None (looks for "input", "question", "human_input", etc.)
|
|
75
|
-
human_prefix: Prefix for human turns in stored engrams (default: "Human")
|
|
76
|
-
ai_prefix: Prefix for AI turns in stored engrams (default: "AI")
|
|
77
|
-
return_docs: If True, return ActivationItem objects instead of a string.
|
|
78
|
-
Useful when you want to inspect scores or metadata.
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
base_url: str = "http://localhost:8475"
|
|
82
|
-
token: Optional[str] = None
|
|
83
|
-
vault: str = "default"
|
|
84
|
-
max_results: int = 10
|
|
85
|
-
memory_key: str = "history"
|
|
86
|
-
input_key: Optional[str] = None
|
|
87
|
-
human_prefix: str = "Human"
|
|
88
|
-
ai_prefix: str = "AI"
|
|
89
|
-
return_docs: bool = False
|
|
90
|
-
|
|
91
|
-
class Config:
|
|
92
|
-
arbitrary_types_allowed = True
|
|
93
|
-
|
|
94
|
-
# ── Public LangChain interface ───────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def memory_variables(self) -> List[str]:
|
|
98
|
-
return [self.memory_key]
|
|
99
|
-
|
|
100
|
-
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
-
"""Retrieve relevant memories for the current input (synchronous)."""
|
|
102
|
-
return _run_sync(self.aload_memory_variables(inputs))
|
|
103
|
-
|
|
104
|
-
async def aload_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
105
|
-
"""Retrieve relevant memories for the current input (async)."""
|
|
106
|
-
query = self._extract_input(inputs)
|
|
107
|
-
if not query:
|
|
108
|
-
return {self.memory_key: [] if self.return_docs else ""}
|
|
109
|
-
|
|
110
|
-
async with MuninnClient(self.base_url, token=self.token) as client:
|
|
111
|
-
result = await client.activate(
|
|
112
|
-
vault=self.vault,
|
|
113
|
-
context=[query],
|
|
114
|
-
max_results=self.max_results,
|
|
115
|
-
threshold=0.05,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if self.return_docs:
|
|
119
|
-
return {self.memory_key: result.activations}
|
|
120
|
-
|
|
121
|
-
return {self.memory_key: self._format_activations(result.activations)}
|
|
122
|
-
|
|
123
|
-
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
124
|
-
"""Store the current conversation turn (synchronous)."""
|
|
125
|
-
_run_sync(self.asave_context(inputs, outputs))
|
|
126
|
-
|
|
127
|
-
async def asave_context(
|
|
128
|
-
self, inputs: Dict[str, Any], outputs: Dict[str, Any]
|
|
129
|
-
) -> None:
|
|
130
|
-
"""Store the current conversation turn (async)."""
|
|
131
|
-
human_input = self._extract_input(inputs) or ""
|
|
132
|
-
ai_output = self._extract_output(outputs) or ""
|
|
133
|
-
|
|
134
|
-
# Concept = first 60 chars of the human turn (readable in the Web UI).
|
|
135
|
-
concept = (human_input[:57] + "...") if len(human_input) > 60 else human_input
|
|
136
|
-
content = f"{self.human_prefix}: {human_input}\n{self.ai_prefix}: {ai_output}"
|
|
137
|
-
|
|
138
|
-
async with MuninnClient(self.base_url, token=self.token) as client:
|
|
139
|
-
await client.write(vault=self.vault, concept=concept, content=content)
|
|
140
|
-
|
|
141
|
-
def clear(self) -> None:
|
|
142
|
-
"""No-op: MuninnDB uses natural decay rather than explicit truncation.
|
|
143
|
-
|
|
144
|
-
Memories fade on their own as they stop being recalled. If you need
|
|
145
|
-
a hard reset, create a new vault or use a different vault name per session.
|
|
146
|
-
"""
|
|
147
|
-
|
|
148
|
-
# ── Internal helpers ────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
def _extract_input(self, inputs: Dict[str, Any]) -> Optional[str]:
|
|
151
|
-
"""Extract the human message from the chain's input dict."""
|
|
152
|
-
if self.input_key:
|
|
153
|
-
return str(inputs.get(self.input_key, ""))
|
|
154
|
-
for key in ("input", "question", "human_input", "query", "message", "text"):
|
|
155
|
-
if key in inputs:
|
|
156
|
-
return str(inputs[key])
|
|
157
|
-
# Fall back to first string value.
|
|
158
|
-
for v in inputs.values():
|
|
159
|
-
if isinstance(v, str):
|
|
160
|
-
return v
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
def _extract_output(self, outputs: Dict[str, Any]) -> Optional[str]:
|
|
164
|
-
"""Extract the AI response from the chain's output dict."""
|
|
165
|
-
for key in ("output", "response", "answer", "text", "result", "generation"):
|
|
166
|
-
if key in outputs:
|
|
167
|
-
return str(outputs[key])
|
|
168
|
-
for v in outputs.values():
|
|
169
|
-
if isinstance(v, str):
|
|
170
|
-
return v
|
|
171
|
-
return None
|
|
172
|
-
|
|
173
|
-
def _format_activations(self, activations: List[ActivationItem]) -> str:
|
|
174
|
-
"""Format activated memories as a context string for the LLM prompt."""
|
|
175
|
-
if not activations:
|
|
176
|
-
return ""
|
|
177
|
-
|
|
178
|
-
lines = ["[Relevant memory context from MuninnDB]"]
|
|
179
|
-
for item in activations:
|
|
180
|
-
# Wrap long content so it's readable in prompts.
|
|
181
|
-
wrapped = textwrap.fill(item.content, width=120, subsequent_indent=" ")
|
|
182
|
-
lines.append(f"- {wrapped}")
|
|
183
|
-
lines.append("[End of memory context]")
|
|
184
|
-
return "\n".join(lines)
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""Async SSE (Server-Sent Events) stream implementation with auto-reconnect."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
|
|
8
|
-
from .errors import MuninnConnectionError
|
|
9
|
-
from .types import Push
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class SSEStream:
|
|
13
|
-
"""Async SSE stream with automatic reconnection and Last-Event-ID tracking.
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
stream = client.subscribe(vault="default")
|
|
17
|
-
async for push in stream:
|
|
18
|
-
print(push.engram_id)
|
|
19
|
-
if should_stop:
|
|
20
|
-
await stream.close()
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, client_ref, url: str, params: dict):
|
|
24
|
-
self._client = client_ref
|
|
25
|
-
self._url = url
|
|
26
|
-
self._params = params
|
|
27
|
-
self._last_event_id: str | None = None
|
|
28
|
-
self._closed = False
|
|
29
|
-
|
|
30
|
-
async def close(self):
|
|
31
|
-
"""Close the SSE stream."""
|
|
32
|
-
self._closed = True
|
|
33
|
-
|
|
34
|
-
def __aiter__(self):
|
|
35
|
-
"""Iterate over SSE events."""
|
|
36
|
-
return self._stream()
|
|
37
|
-
|
|
38
|
-
async def _stream(self):
|
|
39
|
-
"""Internal stream loop with automatic reconnection."""
|
|
40
|
-
backoff = 0.5
|
|
41
|
-
while not self._closed:
|
|
42
|
-
try:
|
|
43
|
-
headers = {"Accept": "text/event-stream"}
|
|
44
|
-
if self._last_event_id:
|
|
45
|
-
headers["Last-Event-ID"] = self._last_event_id
|
|
46
|
-
if self._client._token:
|
|
47
|
-
headers["Authorization"] = f"Bearer {self._client._token}"
|
|
48
|
-
|
|
49
|
-
async with self._client._http.stream(
|
|
50
|
-
"GET",
|
|
51
|
-
self._url,
|
|
52
|
-
params=self._params,
|
|
53
|
-
headers=headers,
|
|
54
|
-
# connect + write have bounded timeouts; read=None allows
|
|
55
|
-
# indefinite streaming without spurious disconnects.
|
|
56
|
-
timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=5.0),
|
|
57
|
-
) as resp:
|
|
58
|
-
if resp.status_code != 200:
|
|
59
|
-
raise httpx.HTTPStatusError(
|
|
60
|
-
f"SSE stream failed with {resp.status_code}",
|
|
61
|
-
request=resp.request,
|
|
62
|
-
response=resp,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
backoff = 0.5 # reset on successful connect
|
|
66
|
-
event_type = "message"
|
|
67
|
-
data_lines = []
|
|
68
|
-
|
|
69
|
-
async for line in resp.aiter_lines():
|
|
70
|
-
if self._closed:
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
line = line.strip()
|
|
74
|
-
|
|
75
|
-
if line.startswith("event:"):
|
|
76
|
-
event_type = line[6:].strip()
|
|
77
|
-
elif line.startswith("data:"):
|
|
78
|
-
data_lines.append(line[5:].strip())
|
|
79
|
-
elif line.startswith("id:"):
|
|
80
|
-
self._last_event_id = line[3:].strip()
|
|
81
|
-
elif line == "":
|
|
82
|
-
# Empty line marks end of event
|
|
83
|
-
if data_lines:
|
|
84
|
-
data_str = "\n".join(data_lines)
|
|
85
|
-
try:
|
|
86
|
-
data = json.loads(data_str)
|
|
87
|
-
if event_type == "push":
|
|
88
|
-
yield Push(
|
|
89
|
-
subscription_id=data.get("subscription_id", ""),
|
|
90
|
-
trigger=data.get("trigger", ""),
|
|
91
|
-
push_number=data.get("push_number", 0),
|
|
92
|
-
engram_id=data.get("engram_id"),
|
|
93
|
-
at=data.get("at"),
|
|
94
|
-
)
|
|
95
|
-
# ignore subscribed and other event types
|
|
96
|
-
except json.JSONDecodeError:
|
|
97
|
-
pass
|
|
98
|
-
|
|
99
|
-
event_type = "message"
|
|
100
|
-
data_lines = []
|
|
101
|
-
|
|
102
|
-
except httpx.HTTPStatusError as e:
|
|
103
|
-
# Fatal status codes: don't retry — surface immediately.
|
|
104
|
-
if e.response.status_code in (401, 403, 404):
|
|
105
|
-
raise MuninnConnectionError(
|
|
106
|
-
f"SSE stream failed with {e.response.status_code}: "
|
|
107
|
-
f"{e.response.text}"
|
|
108
|
-
) from e
|
|
109
|
-
# 5xx and other server errors: backoff and retry.
|
|
110
|
-
if self._closed:
|
|
111
|
-
return
|
|
112
|
-
await asyncio.sleep(min(backoff, 30.0))
|
|
113
|
-
backoff = min(backoff * 2, 30.0)
|
|
114
|
-
except (
|
|
115
|
-
httpx.ConnectError,
|
|
116
|
-
httpx.ReadError,
|
|
117
|
-
httpx.RemoteProtocolError,
|
|
118
|
-
):
|
|
119
|
-
if self._closed:
|
|
120
|
-
return
|
|
121
|
-
await asyncio.sleep(min(backoff, 30.0))
|
|
122
|
-
backoff = min(backoff * 2, 30.0)
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"""MuninnDB type definitions."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Any, Dict, List, Optional
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class WriteRequest:
|
|
11
|
-
"""Request to write an engram."""
|
|
12
|
-
|
|
13
|
-
vault: str
|
|
14
|
-
concept: str
|
|
15
|
-
content: str
|
|
16
|
-
tags: Optional[List[str]] = None
|
|
17
|
-
confidence: float = 0.9
|
|
18
|
-
stability: float = 0.5
|
|
19
|
-
embedding: Optional[List[float]] = None
|
|
20
|
-
associations: Optional[Dict[str, Any]] = None
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class WriteResponse:
|
|
25
|
-
"""Response from writing an engram."""
|
|
26
|
-
|
|
27
|
-
id: str
|
|
28
|
-
created_at: int
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class ActivateRequest:
|
|
33
|
-
"""Request to activate memory."""
|
|
34
|
-
|
|
35
|
-
vault: str
|
|
36
|
-
context: List[str]
|
|
37
|
-
max_results: int = 10
|
|
38
|
-
threshold: float = 0.1
|
|
39
|
-
max_hops: int = 0
|
|
40
|
-
include_why: bool = False
|
|
41
|
-
brief_mode: str = "auto"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclass
|
|
45
|
-
class ActivationItem:
|
|
46
|
-
"""A single activated memory item."""
|
|
47
|
-
|
|
48
|
-
id: str
|
|
49
|
-
concept: str
|
|
50
|
-
content: str
|
|
51
|
-
score: float
|
|
52
|
-
confidence: float
|
|
53
|
-
score_components: Optional[Dict[str, float]] = None
|
|
54
|
-
why: Optional[str] = None
|
|
55
|
-
hop_path: Optional[List[str]] = None
|
|
56
|
-
dormant: bool = False
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass
|
|
60
|
-
class BriefSentence:
|
|
61
|
-
"""A sentence extracted by brief mode."""
|
|
62
|
-
|
|
63
|
-
engram_id: str
|
|
64
|
-
text: str
|
|
65
|
-
score: float
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@dataclass
|
|
69
|
-
class ActivateResponse:
|
|
70
|
-
"""Response from activating memory."""
|
|
71
|
-
|
|
72
|
-
query_id: str
|
|
73
|
-
total_found: int
|
|
74
|
-
activations: List[ActivationItem]
|
|
75
|
-
latency_ms: float = 0.0
|
|
76
|
-
brief: Optional[List[BriefSentence]] = None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@dataclass
|
|
80
|
-
class ReadResponse:
|
|
81
|
-
"""Response from reading an engram."""
|
|
82
|
-
|
|
83
|
-
id: str
|
|
84
|
-
concept: str
|
|
85
|
-
content: str
|
|
86
|
-
confidence: float
|
|
87
|
-
relevance: float
|
|
88
|
-
stability: float
|
|
89
|
-
access_count: int
|
|
90
|
-
tags: List[str]
|
|
91
|
-
state: str
|
|
92
|
-
created_at: int
|
|
93
|
-
updated_at: int
|
|
94
|
-
last_access: Optional[int] = None
|
|
95
|
-
coherence: Optional[Dict[str, "CoherenceResult"]] = None
|
|
96
|
-
summary: Optional[str] = None
|
|
97
|
-
key_points: Optional[List[str]] = None
|
|
98
|
-
memory_type: Optional[int] = None
|
|
99
|
-
classification: Optional[int] = None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@dataclass
|
|
103
|
-
class CoherenceResult:
|
|
104
|
-
"""Coherence metrics for a vault."""
|
|
105
|
-
|
|
106
|
-
score: float
|
|
107
|
-
orphan_ratio: float
|
|
108
|
-
contradiction_density: float
|
|
109
|
-
duplication_pressure: float
|
|
110
|
-
decay_variance: float
|
|
111
|
-
total_engrams: int
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@dataclass
|
|
115
|
-
class StatResponse:
|
|
116
|
-
"""Response from stats endpoint."""
|
|
117
|
-
|
|
118
|
-
engram_count: int
|
|
119
|
-
vault_count: int
|
|
120
|
-
storage_bytes: int
|
|
121
|
-
coherence: dict[str, CoherenceResult] | None = None
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@dataclass
|
|
125
|
-
class Push:
|
|
126
|
-
"""SSE push event from subscription."""
|
|
127
|
-
|
|
128
|
-
subscription_id: str
|
|
129
|
-
trigger: str
|
|
130
|
-
push_number: int
|
|
131
|
-
engram_id: Optional[str] = None
|
|
132
|
-
at: Optional[int] = None
|
|
File without changes
|
|
File without changes
|