etchmem 0.3.0__tar.gz → 0.4.0__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.
- {etchmem-0.3.0 → etchmem-0.4.0}/PKG-INFO +52 -5
- {etchmem-0.3.0 → etchmem-0.4.0}/README.md +50 -3
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem/__init__.py +13 -3
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem/async_client.py +50 -7
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem/client.py +68 -6
- etchmem-0.4.0/etchmem/models.py +529 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem.egg-info/PKG-INFO +52 -5
- {etchmem-0.3.0 → etchmem-0.4.0}/pyproject.toml +2 -2
- etchmem-0.4.0/tests/test_sdk.py +401 -0
- etchmem-0.3.0/etchmem/models.py +0 -285
- etchmem-0.3.0/tests/test_sdk.py +0 -202
- {etchmem-0.3.0 → etchmem-0.4.0}/LICENSE +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem/errors.py +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem.egg-info/SOURCES.txt +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem.egg-info/dependency_links.txt +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem.egg-info/requires.txt +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/etchmem.egg-info/top_level.txt +0 -0
- {etchmem-0.3.0 → etchmem-0.4.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etchmem
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for etchmem-server 0.0.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python SDK for etchmem-server 0.0.10 — the Knowledge Consolidation Engine for AI agents
|
|
5
5
|
Author: Andrey Olishchuk
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Repository, https://github.com/vossmoos/etchmem-sdk-python
|
|
@@ -30,7 +30,9 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
**Python SDK for [etchmem-server](https://etchmem.io) — the Knowledge Consolidation Engine for AI agents.**
|
|
32
32
|
|
|
33
|
-
Compatible with **etchmem-server 0.0.
|
|
33
|
+
Compatible with **etchmem-server 0.0.10** (`aolishchuk/etchmem:0.0.10`). Fields added
|
|
34
|
+
after 0.0.4 (`details`, `evidence`, `event_at`, `values`, …) simply stay empty against
|
|
35
|
+
older servers.
|
|
34
36
|
|
|
35
37
|
Your agents' daily activity is full of hard-won facts — deals signed, decisions made, preferences learned. etchmem-server turns that stream of raw signals into a clean, typed, versioned knowledge base: one consolidated belief per fact, with confidence, provenance, conflict status, and full version history — queryable at any point in time.
|
|
36
38
|
|
|
@@ -65,7 +67,7 @@ for hit in mem.recall("what did Acme sign?", top_k=5):
|
|
|
65
67
|
print(f"[{hit.origin}] {hit.content} (confidence={hit.confidence})")
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
###
|
|
70
|
+
### Four ways to ask
|
|
69
71
|
|
|
70
72
|
Which one you want depends on what you already know. Reaching for the wrong one
|
|
71
73
|
is a quiet source of wrong answers.
|
|
@@ -73,6 +75,7 @@ is a quiet source of wrong answers.
|
|
|
73
75
|
| You know | Method | Returns |
|
|
74
76
|
|---|---|---|
|
|
75
77
|
| the subject | `know(ref)` | every belief about it — exhaustive, ordered by property |
|
|
78
|
+
| the kind of belief (scope, property, subject type) | `etches(...)` | every matching belief across subjects — exhaustive, paged |
|
|
76
79
|
| only words | `recall(query)` | beliefs ranked by wording, `top_k` |
|
|
77
80
|
| words, and want what they connect to | `associate(query)` | the above **plus** each subject's full facts **plus** connected nodes |
|
|
78
81
|
|
|
@@ -83,6 +86,12 @@ for e in facts.settled(min_confidence=0.6):
|
|
|
83
86
|
print(e.property, "=", e.current_value)
|
|
84
87
|
print(facts.contested, "open questions")
|
|
85
88
|
|
|
89
|
+
# etches — you know the kind of belief: every QA rule, across all subjects
|
|
90
|
+
for e in mem.iter_etches(scope="content-qa", property="qa_rejection",
|
|
91
|
+
min_confidence=0.8):
|
|
92
|
+
for d in e.details: # label + the author's full wording
|
|
93
|
+
print(e.entity_name, "·", d.value, "—", d.detail)
|
|
94
|
+
|
|
86
95
|
# recall — you only have words. entity_id chains straight into know()
|
|
87
96
|
for hit in mem.recall("bathroom leaks", top_k=5):
|
|
88
97
|
detail = mem.know(hit.entity_id)
|
|
@@ -124,10 +133,13 @@ The SDK mirrors the REST endpoints one-to-one:
|
|
|
124
133
|
| `remember(data, *, source, scope, extract_mode="deferred", metadata=None, occurred_at=None)` | `POST /remember` | `RememberResult` |
|
|
125
134
|
| `recall(query, *, scope=None, source=None, top_k=5, include_signals=True, as_of=None, as_of_basis="ingest")` | `POST /recall` | `RecallResponse` (iterable of `RecallResult`) |
|
|
126
135
|
| `know(ref, *, as_of=None, entity_type=None, as_of_basis="ingest")` | `GET /entity/{ref}/etches` | `Knowledge` (iterable of `Etch`) |
|
|
127
|
-
| `associate(query, *, scope=None, top_k=5, hops=1, as_of=None, min_score=0.15)` | `POST /associate` | `Association` |
|
|
136
|
+
| `associate(query, *, scope=None, top_k=5, hops=1, as_of=None, min_score=0.15, as_of_basis="ingest")` | `POST /associate` | `Association` |
|
|
137
|
+
| `etches(*, scope=None, property=None, entity_type=None, status=None, min_confidence=None, limit=500, offset=0)` | `GET /etches` | `EtchList` (one page of `Etch`; `.total`, `.has_more`) |
|
|
138
|
+
| `iter_etches(*, page_size=500, **filters)` | `GET /etches` (all pages) | iterator of `Etch` |
|
|
128
139
|
| `sleep()` | `POST /sleep` | `SleepResult` |
|
|
129
140
|
| `export()` | `POST /export` | `ExportResult` (list of `Etch`) |
|
|
130
141
|
| `history(etch_id)` | `GET /etch/{id}/history` | `History` (iterable of `EtchVersion`) |
|
|
142
|
+
| `dossier(etch_id)` | `GET /etch/{id}/dossier` | `Dossier` (etch + versions + claims + raw signals) |
|
|
131
143
|
| `stats()` | `GET /stats` | `Stats` |
|
|
132
144
|
| `health()` | `GET /health` | `Health` |
|
|
133
145
|
|
|
@@ -203,6 +215,41 @@ for v in mem.history(etch_id):
|
|
|
203
215
|
print(" fact true from", v.event_at, "— we learned it", v.created_at)
|
|
204
216
|
```
|
|
205
217
|
|
|
218
|
+
### retracted values
|
|
219
|
+
|
|
220
|
+
From server 0.0.10 a withdrawn fact ("we no longer need CSV export") is kept, not
|
|
221
|
+
deleted. `current_value` lists only what is still in force; `values` lists every value
|
|
222
|
+
ever asserted, heaviest first, with `active`, `weight`, `last_asserted_at` and
|
|
223
|
+
`retracted_at`:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
for e in mem.know("Acme"):
|
|
227
|
+
print(e.property, "=", e.current_value)
|
|
228
|
+
for v in e.retracted: # no longer valid, but once was
|
|
229
|
+
print(" withdrawn:", v.value, "at", v.retracted_at)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### evidence and dossier
|
|
233
|
+
|
|
234
|
+
`confidence` is built from who said it (source trust declared in the server's
|
|
235
|
+
`ext/*.yaml`) and how many independent sources agree. Every etch and recall hit
|
|
236
|
+
carries the `evidence` behind the number, so you can apply your own rule:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
for hit in mem.recall(paragraph, scope="content-qa", include_signals=False):
|
|
240
|
+
if hit.evidence and hit.evidence.max_trust >= 0.8:
|
|
241
|
+
print(hit.value, [s.source for s in hit.evidence.sources])
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`dossier(etch_id)` answers "why does the agent believe that?" in one call: the
|
|
245
|
+
etch, its versions, the claims that formed it (with corroboration counts) and the
|
|
246
|
+
raw signals behind them. Raw signals are withheld (`signals_omitted=True`) when
|
|
247
|
+
the server runs with claims anonymization.
|
|
248
|
+
|
|
249
|
+
`sleep()` also reports `subject_retries`, `relations_resolved` and the
|
|
250
|
+
`entities` resolution counters; `stats().sources_without_trust` lists deposit
|
|
251
|
+
sources nobody declared a trust for.
|
|
252
|
+
|
|
206
253
|
---
|
|
207
254
|
|
|
208
255
|
## Configuration
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Python SDK for [etchmem-server](https://etchmem.io) — the Knowledge Consolidation Engine for AI agents.**
|
|
4
4
|
|
|
5
|
-
Compatible with **etchmem-server 0.0.
|
|
5
|
+
Compatible with **etchmem-server 0.0.10** (`aolishchuk/etchmem:0.0.10`). Fields added
|
|
6
|
+
after 0.0.4 (`details`, `evidence`, `event_at`, `values`, …) simply stay empty against
|
|
7
|
+
older servers.
|
|
6
8
|
|
|
7
9
|
Your agents' daily activity is full of hard-won facts — deals signed, decisions made, preferences learned. etchmem-server turns that stream of raw signals into a clean, typed, versioned knowledge base: one consolidated belief per fact, with confidence, provenance, conflict status, and full version history — queryable at any point in time.
|
|
8
10
|
|
|
@@ -37,7 +39,7 @@ for hit in mem.recall("what did Acme sign?", top_k=5):
|
|
|
37
39
|
print(f"[{hit.origin}] {hit.content} (confidence={hit.confidence})")
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
###
|
|
42
|
+
### Four ways to ask
|
|
41
43
|
|
|
42
44
|
Which one you want depends on what you already know. Reaching for the wrong one
|
|
43
45
|
is a quiet source of wrong answers.
|
|
@@ -45,6 +47,7 @@ is a quiet source of wrong answers.
|
|
|
45
47
|
| You know | Method | Returns |
|
|
46
48
|
|---|---|---|
|
|
47
49
|
| the subject | `know(ref)` | every belief about it — exhaustive, ordered by property |
|
|
50
|
+
| the kind of belief (scope, property, subject type) | `etches(...)` | every matching belief across subjects — exhaustive, paged |
|
|
48
51
|
| only words | `recall(query)` | beliefs ranked by wording, `top_k` |
|
|
49
52
|
| words, and want what they connect to | `associate(query)` | the above **plus** each subject's full facts **plus** connected nodes |
|
|
50
53
|
|
|
@@ -55,6 +58,12 @@ for e in facts.settled(min_confidence=0.6):
|
|
|
55
58
|
print(e.property, "=", e.current_value)
|
|
56
59
|
print(facts.contested, "open questions")
|
|
57
60
|
|
|
61
|
+
# etches — you know the kind of belief: every QA rule, across all subjects
|
|
62
|
+
for e in mem.iter_etches(scope="content-qa", property="qa_rejection",
|
|
63
|
+
min_confidence=0.8):
|
|
64
|
+
for d in e.details: # label + the author's full wording
|
|
65
|
+
print(e.entity_name, "·", d.value, "—", d.detail)
|
|
66
|
+
|
|
58
67
|
# recall — you only have words. entity_id chains straight into know()
|
|
59
68
|
for hit in mem.recall("bathroom leaks", top_k=5):
|
|
60
69
|
detail = mem.know(hit.entity_id)
|
|
@@ -96,10 +105,13 @@ The SDK mirrors the REST endpoints one-to-one:
|
|
|
96
105
|
| `remember(data, *, source, scope, extract_mode="deferred", metadata=None, occurred_at=None)` | `POST /remember` | `RememberResult` |
|
|
97
106
|
| `recall(query, *, scope=None, source=None, top_k=5, include_signals=True, as_of=None, as_of_basis="ingest")` | `POST /recall` | `RecallResponse` (iterable of `RecallResult`) |
|
|
98
107
|
| `know(ref, *, as_of=None, entity_type=None, as_of_basis="ingest")` | `GET /entity/{ref}/etches` | `Knowledge` (iterable of `Etch`) |
|
|
99
|
-
| `associate(query, *, scope=None, top_k=5, hops=1, as_of=None, min_score=0.15)` | `POST /associate` | `Association` |
|
|
108
|
+
| `associate(query, *, scope=None, top_k=5, hops=1, as_of=None, min_score=0.15, as_of_basis="ingest")` | `POST /associate` | `Association` |
|
|
109
|
+
| `etches(*, scope=None, property=None, entity_type=None, status=None, min_confidence=None, limit=500, offset=0)` | `GET /etches` | `EtchList` (one page of `Etch`; `.total`, `.has_more`) |
|
|
110
|
+
| `iter_etches(*, page_size=500, **filters)` | `GET /etches` (all pages) | iterator of `Etch` |
|
|
100
111
|
| `sleep()` | `POST /sleep` | `SleepResult` |
|
|
101
112
|
| `export()` | `POST /export` | `ExportResult` (list of `Etch`) |
|
|
102
113
|
| `history(etch_id)` | `GET /etch/{id}/history` | `History` (iterable of `EtchVersion`) |
|
|
114
|
+
| `dossier(etch_id)` | `GET /etch/{id}/dossier` | `Dossier` (etch + versions + claims + raw signals) |
|
|
103
115
|
| `stats()` | `GET /stats` | `Stats` |
|
|
104
116
|
| `health()` | `GET /health` | `Health` |
|
|
105
117
|
|
|
@@ -175,6 +187,41 @@ for v in mem.history(etch_id):
|
|
|
175
187
|
print(" fact true from", v.event_at, "— we learned it", v.created_at)
|
|
176
188
|
```
|
|
177
189
|
|
|
190
|
+
### retracted values
|
|
191
|
+
|
|
192
|
+
From server 0.0.10 a withdrawn fact ("we no longer need CSV export") is kept, not
|
|
193
|
+
deleted. `current_value` lists only what is still in force; `values` lists every value
|
|
194
|
+
ever asserted, heaviest first, with `active`, `weight`, `last_asserted_at` and
|
|
195
|
+
`retracted_at`:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
for e in mem.know("Acme"):
|
|
199
|
+
print(e.property, "=", e.current_value)
|
|
200
|
+
for v in e.retracted: # no longer valid, but once was
|
|
201
|
+
print(" withdrawn:", v.value, "at", v.retracted_at)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### evidence and dossier
|
|
205
|
+
|
|
206
|
+
`confidence` is built from who said it (source trust declared in the server's
|
|
207
|
+
`ext/*.yaml`) and how many independent sources agree. Every etch and recall hit
|
|
208
|
+
carries the `evidence` behind the number, so you can apply your own rule:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
for hit in mem.recall(paragraph, scope="content-qa", include_signals=False):
|
|
212
|
+
if hit.evidence and hit.evidence.max_trust >= 0.8:
|
|
213
|
+
print(hit.value, [s.source for s in hit.evidence.sources])
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`dossier(etch_id)` answers "why does the agent believe that?" in one call: the
|
|
217
|
+
etch, its versions, the claims that formed it (with corroboration counts) and the
|
|
218
|
+
raw signals behind them. Raw signals are withheld (`signals_omitted=True`) when
|
|
219
|
+
the server runs with claims anonymization.
|
|
220
|
+
|
|
221
|
+
`sleep()` also reports `subject_retries`, `relations_resolved` and the
|
|
222
|
+
`entities` resolution counters; `stats().sources_without_trust` lists deposit
|
|
223
|
+
sources nobody declared a trust for.
|
|
224
|
+
|
|
178
225
|
---
|
|
179
226
|
|
|
180
227
|
## Configuration
|
|
@@ -24,8 +24,10 @@ from etchmem.errors import (
|
|
|
24
24
|
NotFoundError, ServerError,
|
|
25
25
|
)
|
|
26
26
|
from etchmem.models import (
|
|
27
|
-
Association,
|
|
28
|
-
|
|
27
|
+
Association, Claim, DetailItem, Dossier, EntityInfo, Etch, EtchList,
|
|
28
|
+
EtchVersion, Evidence, ExportResult, Health, History, Knowledge,
|
|
29
|
+
RecallResponse, RecallResult, RememberResult, Signal, SleepResult,
|
|
30
|
+
SourceEvidence, Stats, ValueWeight,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
__all__ = [
|
|
@@ -41,8 +43,16 @@ __all__ = [
|
|
|
41
43
|
"SleepResult",
|
|
42
44
|
"ExportResult",
|
|
43
45
|
"Etch",
|
|
46
|
+
"EtchList",
|
|
44
47
|
"EtchVersion",
|
|
45
48
|
"History",
|
|
49
|
+
"Dossier",
|
|
50
|
+
"Claim",
|
|
51
|
+
"Signal",
|
|
52
|
+
"DetailItem",
|
|
53
|
+
"Evidence",
|
|
54
|
+
"SourceEvidence",
|
|
55
|
+
"ValueWeight",
|
|
46
56
|
"Stats",
|
|
47
57
|
"Health",
|
|
48
58
|
# errors
|
|
@@ -54,4 +64,4 @@ __all__ = [
|
|
|
54
64
|
"ServerError",
|
|
55
65
|
]
|
|
56
66
|
|
|
57
|
-
__version__ = "0.
|
|
67
|
+
__version__ = "0.4.0"
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
+
from collections.abc import AsyncIterator, Sequence
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
|
|
9
|
-
from etchmem.client import
|
|
10
|
+
from etchmem.client import (
|
|
11
|
+
DEFAULT_BASE_URL, DEFAULT_TIMEOUT, _etch_filters, _ref_path,
|
|
12
|
+
)
|
|
10
13
|
from etchmem.errors import ConnectionError, error_for_status
|
|
11
14
|
from etchmem.models import (
|
|
12
|
-
|
|
13
|
-
SleepResult, Stats,
|
|
15
|
+
Association, Dossier, Etch, EtchList, ExportResult, Health, History,
|
|
16
|
+
Knowledge, RecallResponse, RememberResult, SleepResult, Stats,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -115,9 +118,6 @@ class AsyncEtchMem:
|
|
|
115
118
|
"as_of_basis": as_of_basis})
|
|
116
119
|
return RecallResponse.from_dict(payload)
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
121
|
async def associate(
|
|
122
122
|
self,
|
|
123
123
|
query: str,
|
|
@@ -172,9 +172,43 @@ class AsyncEtchMem:
|
|
|
172
172
|
if as_of_basis != "ingest":
|
|
173
173
|
params["as_of_basis"] = as_of_basis
|
|
174
174
|
payload = await self._request(
|
|
175
|
-
"GET", f"/entity/{ref}/etches", params=params or None)
|
|
175
|
+
"GET", f"/entity/{_ref_path(ref)}/etches", params=params or None)
|
|
176
176
|
return Knowledge.from_dict(payload)
|
|
177
177
|
|
|
178
|
+
async def etches(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
scope: str | None = None,
|
|
182
|
+
property: str | Sequence[str] | None = None,
|
|
183
|
+
entity_type: str | Sequence[str] | None = None,
|
|
184
|
+
status: str | None = None,
|
|
185
|
+
min_confidence: float | None = None,
|
|
186
|
+
limit: int = 500,
|
|
187
|
+
offset: int = 0,
|
|
188
|
+
) -> EtchList:
|
|
189
|
+
"""EVERY belief matching plain filters — exhaustive, not ranked.
|
|
190
|
+
|
|
191
|
+
Use when you know the *kind* of belief rather than the subject or the
|
|
192
|
+
words: "all qa_rejection rules in scope content-qa", "every contested
|
|
193
|
+
lifecycle_status". ``property`` and ``entity_type`` take one value or a
|
|
194
|
+
list. Returns one page (newest first); ``total`` counts every match, so
|
|
195
|
+
page with ``offset`` or use :meth:`iter_etches`.
|
|
196
|
+
"""
|
|
197
|
+
return EtchList.from_dict(await self._request(
|
|
198
|
+
"GET", "/etches", params=_etch_filters(
|
|
199
|
+
scope, property, entity_type, status, min_confidence, limit, offset)))
|
|
200
|
+
|
|
201
|
+
async def iter_etches(self, *, page_size: int = 500, **filters: Any) -> AsyncIterator[Etch]:
|
|
202
|
+
"""Every belief matching the filters of :meth:`etches`, across all pages."""
|
|
203
|
+
offset = 0
|
|
204
|
+
while True:
|
|
205
|
+
page = await self.etches(limit=page_size, offset=offset, **filters)
|
|
206
|
+
for etch in page.etches:
|
|
207
|
+
yield etch
|
|
208
|
+
if not page.has_more or not page.etches:
|
|
209
|
+
return
|
|
210
|
+
offset += page.count
|
|
211
|
+
|
|
178
212
|
async def sleep(self) -> SleepResult:
|
|
179
213
|
"""Run one consolidation tick now (batch → extract → fold)."""
|
|
180
214
|
return SleepResult.from_dict(await self._request("POST", "/sleep"))
|
|
@@ -188,6 +222,15 @@ class AsyncEtchMem:
|
|
|
188
222
|
return History.from_dict(
|
|
189
223
|
await self._request("GET", f"/etch/{etch_id}/history"))
|
|
190
224
|
|
|
225
|
+
async def dossier(self, etch_id: str) -> Dossier:
|
|
226
|
+
"""Full provenance of one belief: etch, versions, claims, raw signals.
|
|
227
|
+
|
|
228
|
+
The answer to "why does the agent believe that?". Raw signals are
|
|
229
|
+
withheld (``signals_omitted=True``) when the server anonymizes claims.
|
|
230
|
+
"""
|
|
231
|
+
return Dossier.from_dict(
|
|
232
|
+
await self._request("GET", f"/etch/{etch_id}/dossier"))
|
|
233
|
+
|
|
191
234
|
async def stats(self) -> Stats:
|
|
192
235
|
"""Queue depths and store counts."""
|
|
193
236
|
return Stats.from_dict(await self._request("GET", "/stats"))
|
|
@@ -2,20 +2,43 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
5
6
|
from typing import Any
|
|
7
|
+
from urllib.parse import quote
|
|
6
8
|
|
|
7
9
|
import httpx
|
|
8
10
|
|
|
9
11
|
from etchmem.errors import ConnectionError, error_for_status
|
|
10
12
|
from etchmem.models import (
|
|
11
|
-
|
|
12
|
-
SleepResult, Stats,
|
|
13
|
+
Association, Dossier, Etch, EtchList, ExportResult, Health, History,
|
|
14
|
+
Knowledge, RecallResponse, RememberResult, SleepResult, Stats,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
DEFAULT_BASE_URL = "http://localhost:8000"
|
|
16
18
|
DEFAULT_TIMEOUT = 30.0
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
def _etch_filters(scope: str | None, property: str | Sequence[str] | None,
|
|
22
|
+
entity_type: str | Sequence[str] | None, status: str | None,
|
|
23
|
+
min_confidence: float | None, limit: int,
|
|
24
|
+
offset: int) -> dict[str, Any]:
|
|
25
|
+
"""Query params for GET /etches; list filters become repeated params."""
|
|
26
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
27
|
+
for key, value in (("scope", scope), ("status", status),
|
|
28
|
+
("min_confidence", min_confidence)):
|
|
29
|
+
if value is not None:
|
|
30
|
+
params[key] = value
|
|
31
|
+
for key, value in (("property", property), ("entity_type", entity_type)):
|
|
32
|
+
if value:
|
|
33
|
+
params[key] = [value] if isinstance(value, str) else list(value)
|
|
34
|
+
return params
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ref_path(ref: str) -> str:
|
|
38
|
+
"""Percent-encode an entity ref for the URL path ("MSM 0808", "R&D #4")."""
|
|
39
|
+
return quote(ref, safe="")
|
|
40
|
+
|
|
41
|
+
|
|
19
42
|
class EtchMem:
|
|
20
43
|
"""Synchronous etchmem client.
|
|
21
44
|
|
|
@@ -139,9 +162,6 @@ class EtchMem:
|
|
|
139
162
|
"as_of_basis": as_of_basis})
|
|
140
163
|
return RecallResponse.from_dict(payload)
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
165
|
def associate(
|
|
146
166
|
self,
|
|
147
167
|
query: str,
|
|
@@ -196,9 +216,42 @@ class EtchMem:
|
|
|
196
216
|
if as_of_basis != "ingest":
|
|
197
217
|
params["as_of_basis"] = as_of_basis
|
|
198
218
|
payload = self._request(
|
|
199
|
-
"GET", f"/entity/{ref}/etches", params=params or None)
|
|
219
|
+
"GET", f"/entity/{_ref_path(ref)}/etches", params=params or None)
|
|
200
220
|
return Knowledge.from_dict(payload)
|
|
201
221
|
|
|
222
|
+
def etches(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
scope: str | None = None,
|
|
226
|
+
property: str | Sequence[str] | None = None,
|
|
227
|
+
entity_type: str | Sequence[str] | None = None,
|
|
228
|
+
status: str | None = None,
|
|
229
|
+
min_confidence: float | None = None,
|
|
230
|
+
limit: int = 500,
|
|
231
|
+
offset: int = 0,
|
|
232
|
+
) -> EtchList:
|
|
233
|
+
"""EVERY belief matching plain filters — exhaustive, not ranked.
|
|
234
|
+
|
|
235
|
+
Use when you know the *kind* of belief rather than the subject or the
|
|
236
|
+
words: "all qa_rejection rules in scope content-qa", "every contested
|
|
237
|
+
lifecycle_status". ``property`` and ``entity_type`` take one value or a
|
|
238
|
+
list. Returns one page (newest first); ``total`` counts every match, so
|
|
239
|
+
page with ``offset`` or use :meth:`iter_etches`.
|
|
240
|
+
"""
|
|
241
|
+
return EtchList.from_dict(self._request(
|
|
242
|
+
"GET", "/etches", params=_etch_filters(
|
|
243
|
+
scope, property, entity_type, status, min_confidence, limit, offset)))
|
|
244
|
+
|
|
245
|
+
def iter_etches(self, *, page_size: int = 500, **filters: Any) -> Iterator[Etch]:
|
|
246
|
+
"""Every belief matching the filters of :meth:`etches`, across all pages."""
|
|
247
|
+
offset = 0
|
|
248
|
+
while True:
|
|
249
|
+
page = self.etches(limit=page_size, offset=offset, **filters)
|
|
250
|
+
yield from page.etches
|
|
251
|
+
if not page.has_more or not page.etches:
|
|
252
|
+
return
|
|
253
|
+
offset += page.count
|
|
254
|
+
|
|
202
255
|
def sleep(self) -> SleepResult:
|
|
203
256
|
"""Run one consolidation tick now (batch → extract → fold)."""
|
|
204
257
|
return SleepResult.from_dict(self._request("POST", "/sleep"))
|
|
@@ -212,6 +265,15 @@ class EtchMem:
|
|
|
212
265
|
return History.from_dict(
|
|
213
266
|
self._request("GET", f"/etch/{etch_id}/history"))
|
|
214
267
|
|
|
268
|
+
def dossier(self, etch_id: str) -> Dossier:
|
|
269
|
+
"""Full provenance of one belief: etch, versions, claims, raw signals.
|
|
270
|
+
|
|
271
|
+
The answer to "why does the agent believe that?". Raw signals are
|
|
272
|
+
withheld (``signals_omitted=True``) when the server anonymizes claims.
|
|
273
|
+
"""
|
|
274
|
+
return Dossier.from_dict(
|
|
275
|
+
self._request("GET", f"/etch/{etch_id}/dossier"))
|
|
276
|
+
|
|
215
277
|
def stats(self) -> Stats:
|
|
216
278
|
"""Queue depths and store counts."""
|
|
217
279
|
return Stats.from_dict(self._request("GET", "/stats"))
|