etchmem 0.2.1__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.4.0/PKG-INFO +294 -0
- etchmem-0.4.0/README.md +266 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem/__init__.py +16 -3
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem/async_client.py +115 -7
- etchmem-0.4.0/etchmem/client.py +283 -0
- etchmem-0.4.0/etchmem/models.py +529 -0
- etchmem-0.4.0/etchmem.egg-info/PKG-INFO +294 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/pyproject.toml +2 -2
- etchmem-0.4.0/tests/test_sdk.py +401 -0
- etchmem-0.2.1/PKG-INFO +0 -165
- etchmem-0.2.1/README.md +0 -137
- etchmem-0.2.1/etchmem/client.py +0 -150
- etchmem-0.2.1/etchmem/models.py +0 -211
- etchmem-0.2.1/etchmem.egg-info/PKG-INFO +0 -165
- etchmem-0.2.1/tests/test_sdk.py +0 -202
- {etchmem-0.2.1 → etchmem-0.4.0}/LICENSE +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem/errors.py +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem.egg-info/SOURCES.txt +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem.egg-info/dependency_links.txt +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem.egg-info/requires.txt +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/etchmem.egg-info/top_level.txt +0 -0
- {etchmem-0.2.1 → etchmem-0.4.0}/setup.cfg +0 -0
etchmem-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: etchmem
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python SDK for etchmem-server 0.0.10 — the Knowledge Consolidation Engine for AI agents
|
|
5
|
+
Author: Andrey Olishchuk
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/vossmoos/etchmem-sdk-python
|
|
8
|
+
Keywords: llm,agent,memory,knowledge,consolidation,rag,sdk,rest,client,etchmem
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# etchmem
|
|
30
|
+
|
|
31
|
+
**Python SDK for [etchmem-server](https://etchmem.io) — the Knowledge Consolidation Engine for AI agents.**
|
|
32
|
+
|
|
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.
|
|
36
|
+
|
|
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.
|
|
38
|
+
|
|
39
|
+
This package is the REST client. One dependency (`httpx`), sync and async, fully typed responses.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install etchmem
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **Note on MCP:** etchmem-server also exposes an MCP interface at `/mcp` with the same operations as tools. MCP clients (Claude, agent frameworks, IDEs) connect to it directly over the MCP protocol — no SDK needed. This package is for REST integrations only.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
Run the server (see the etchmem-server repo — `docker compose up`), then:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from etchmem import EtchMem
|
|
55
|
+
|
|
56
|
+
mem = EtchMem("http://localhost:8000") # default; also reads ETCHMEM_BASE_URL
|
|
57
|
+
|
|
58
|
+
# Deposit a raw signal — no pre-structuring needed
|
|
59
|
+
mem.remember(
|
|
60
|
+
"Acme signed the Q3 renewal at $40k. Maria approved the discount.",
|
|
61
|
+
source="agent-33",
|
|
62
|
+
scope="sales",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Semantic recall over consolidated beliefs
|
|
66
|
+
for hit in mem.recall("what did Acme sign?", top_k=5):
|
|
67
|
+
print(f"[{hit.origin}] {hit.content} (confidence={hit.confidence})")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Four ways to ask
|
|
71
|
+
|
|
72
|
+
Which one you want depends on what you already know. Reaching for the wrong one
|
|
73
|
+
is a quiet source of wrong answers.
|
|
74
|
+
|
|
75
|
+
| You know | Method | Returns |
|
|
76
|
+
|---|---|---|
|
|
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 |
|
|
79
|
+
| only words | `recall(query)` | beliefs ranked by wording, `top_k` |
|
|
80
|
+
| words, and want what they connect to | `associate(query)` | the above **plus** each subject's full facts **plus** connected nodes |
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# know — you have the subject
|
|
84
|
+
facts = mem.know("MSM-0808") # id or surface name; variants resolve
|
|
85
|
+
for e in facts.settled(min_confidence=0.6):
|
|
86
|
+
print(e.property, "=", e.current_value)
|
|
87
|
+
print(facts.contested, "open questions")
|
|
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
|
+
|
|
95
|
+
# recall — you only have words. entity_id chains straight into know()
|
|
96
|
+
for hit in mem.recall("bathroom leaks", top_k=5):
|
|
97
|
+
detail = mem.know(hit.entity_id)
|
|
98
|
+
|
|
99
|
+
# associate — words, and what they connect to
|
|
100
|
+
assoc = mem.associate("bathroom leaks")
|
|
101
|
+
for node in assoc.nodes:
|
|
102
|
+
print(node["hops"], node["entity"]["name"], node["reached_via"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`recall` is `ORDER BY similarity LIMIT top_k` with **no threshold** — it returns
|
|
106
|
+
rows whatever their score, so an empty result means an empty scope rather than a
|
|
107
|
+
failed search, and a returned row is not by itself evidence of a match. It also
|
|
108
|
+
matches on the etch *narrative*, so an exact article number is compared by vector
|
|
109
|
+
similarity, not string equality. When you know the subject, use `know`.
|
|
110
|
+
|
|
111
|
+
`associate` follows declared relations one hop in both directions, so a fault
|
|
112
|
+
reached by wording leads to the part that fixes it — which shares no word with
|
|
113
|
+
the query.
|
|
114
|
+
|
|
115
|
+
### Async
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from etchmem import AsyncEtchMem
|
|
119
|
+
|
|
120
|
+
async with AsyncEtchMem("http://localhost:8000") as mem:
|
|
121
|
+
await mem.remember("...", source="agent-33", scope="sales")
|
|
122
|
+
hits = await mem.recall("what did Acme sign?")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## API surface
|
|
128
|
+
|
|
129
|
+
The SDK mirrors the REST endpoints one-to-one:
|
|
130
|
+
|
|
131
|
+
| Method | Endpoint | Returns |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `remember(data, *, source, scope, extract_mode="deferred", metadata=None, occurred_at=None)` | `POST /remember` | `RememberResult` |
|
|
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`) |
|
|
135
|
+
| `know(ref, *, as_of=None, entity_type=None, as_of_basis="ingest")` | `GET /entity/{ref}/etches` | `Knowledge` (iterable of `Etch`) |
|
|
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` |
|
|
139
|
+
| `sleep()` | `POST /sleep` | `SleepResult` |
|
|
140
|
+
| `export()` | `POST /export` | `ExportResult` (list of `Etch`) |
|
|
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) |
|
|
143
|
+
| `stats()` | `GET /stats` | `Stats` |
|
|
144
|
+
| `health()` | `GET /health` | `Health` |
|
|
145
|
+
|
|
146
|
+
All responses are plain dataclasses — see `etchmem/models.py`.
|
|
147
|
+
|
|
148
|
+
### remember
|
|
149
|
+
|
|
150
|
+
`extract_mode="immediate"` marks the signal urgent (claims extracted on the next worker tick); `"deferred"` (default) batches it cheaply. Deposits are idempotent — identical content returns `stored=False`.
|
|
151
|
+
|
|
152
|
+
`occurred_at` (ISO-8601 or epoch seconds) declares when the content actually
|
|
153
|
+
*happened*, as distinct from when you sent it. It defaults to ingest time, and
|
|
154
|
+
**you must set it when loading historical records** — otherwise every signal in
|
|
155
|
+
a backfill carries today's date, the server's recency policy has nothing to
|
|
156
|
+
order by, and a 2009 answer competes with a 2022 answer as an equal.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
res = mem.remember(
|
|
160
|
+
"Modul MSM-0808: NT-2405 is the correct supply up to revision C.",
|
|
161
|
+
source="ticket-resolved", scope="innoxel",
|
|
162
|
+
occurred_at="2014-03-17",
|
|
163
|
+
)
|
|
164
|
+
assert res.occurred_at_declared # the parsed value is echoed back — check it
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### know
|
|
168
|
+
|
|
169
|
+
Every belief about one subject, exhaustive and ordered by property. `ref` is an
|
|
170
|
+
entity id (`product_msm_0808`) or a surface name (`MSM-0808`, `MSM 0808`),
|
|
171
|
+
resolved the way ingestion resolved it, so callers never need the internal id
|
|
172
|
+
format. `Knowledge.settled(min_confidence)` filters to what is safe to state as
|
|
173
|
+
fact; `.contested` counts the beliefs the engine could not settle, which belong
|
|
174
|
+
in an internal note rather than a customer-facing answer.
|
|
175
|
+
|
|
176
|
+
### time travel, and which clock
|
|
177
|
+
|
|
178
|
+
Pass `as_of` (ISO-8601) to `recall`, `know` or `associate`. `as_of_basis` picks
|
|
179
|
+
the clock:
|
|
180
|
+
|
|
181
|
+
| Basis | Question it answers |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `"ingest"` (default) | What did we **believe** then — what would this system have told a customer on that date. The question an audit or a dispute asks. |
|
|
184
|
+
| `"event"` | What was **true** then, by the dates on the underlying facts. |
|
|
185
|
+
|
|
186
|
+
They differ whenever a source reports late — a promotion on the 2nd that reached
|
|
187
|
+
you on the 20th is invisible to ingest-basis on the 10th, visible to event-basis:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
mem.know("john@doe.com", as_of="2026-09-10") # believed
|
|
191
|
+
mem.know("john@doe.com", as_of="2026-09-10", as_of_basis="event") # was true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Event-basis is only as good as your `occurred_at` discipline: signals deposited
|
|
195
|
+
without it fall back to ingest time, and both bases then give the same answer.
|
|
196
|
+
|
|
197
|
+
### sleep
|
|
198
|
+
|
|
199
|
+
The server consolidates on its own cadence. `mem.sleep()` forces one pipeline tick now (batch → extract claims → fold into etches) — useful in tests and demos:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
tick = mem.sleep()
|
|
203
|
+
print(tick.etches_formed, tick.etches_updated, tick.contested)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### history
|
|
207
|
+
|
|
208
|
+
A version is written only when a belief actually **changes**, so six daily
|
|
209
|
+
enrichment runs with one real change give you two versions, not six. Each
|
|
210
|
+
carries both clocks:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
for v in mem.history(etch_id):
|
|
214
|
+
print(v.version, v.current_value, v.status, v.confidence)
|
|
215
|
+
print(" fact true from", v.event_at, "— we learned it", v.created_at)
|
|
216
|
+
```
|
|
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
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Configuration
|
|
256
|
+
|
|
257
|
+
| Parameter | Env var | Default |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `base_url` | `ETCHMEM_BASE_URL` | `http://localhost:8000` |
|
|
260
|
+
| `api_key` | `ETCHMEM_API_KEY` | none (sent as `Authorization: Bearer …` when set) |
|
|
261
|
+
| `timeout` | — | 30 s |
|
|
262
|
+
|
|
263
|
+
You can also inject your own `httpx.Client` / `httpx.AsyncClient` via the `client` parameter (custom TLS, proxies, retries).
|
|
264
|
+
|
|
265
|
+
## Errors
|
|
266
|
+
|
|
267
|
+
All SDK errors derive from `EtchMemError`:
|
|
268
|
+
|
|
269
|
+
- `ConnectionError` — server unreachable (network / timeout)
|
|
270
|
+
- `APIError` — non-2xx response (`.status_code`, `.detail`), with subclasses `NotFoundError` (404), `AuthenticationError` (401/403), `ServerError` (5xx)
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from etchmem import EtchMem, NotFoundError
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
mem.history("no-such-etch")
|
|
277
|
+
except NotFoundError as e:
|
|
278
|
+
print(e.detail)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Development
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
pip install -e ".[dev]"
|
|
287
|
+
python -m pytest tests/
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Releasing to PyPI: see [PIP-DEPLOY.md](PIP-DEPLOY.md).
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT
|
etchmem-0.4.0/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# etchmem
|
|
2
|
+
|
|
3
|
+
**Python SDK for [etchmem-server](https://etchmem.io) — the Knowledge Consolidation Engine for AI agents.**
|
|
4
|
+
|
|
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.
|
|
8
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
This package is the REST client. One dependency (`httpx`), sync and async, fully typed responses.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install etchmem
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Note on MCP:** etchmem-server also exposes an MCP interface at `/mcp` with the same operations as tools. MCP clients (Claude, agent frameworks, IDEs) connect to it directly over the MCP protocol — no SDK needed. This package is for REST integrations only.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
Run the server (see the etchmem-server repo — `docker compose up`), then:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from etchmem import EtchMem
|
|
27
|
+
|
|
28
|
+
mem = EtchMem("http://localhost:8000") # default; also reads ETCHMEM_BASE_URL
|
|
29
|
+
|
|
30
|
+
# Deposit a raw signal — no pre-structuring needed
|
|
31
|
+
mem.remember(
|
|
32
|
+
"Acme signed the Q3 renewal at $40k. Maria approved the discount.",
|
|
33
|
+
source="agent-33",
|
|
34
|
+
scope="sales",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Semantic recall over consolidated beliefs
|
|
38
|
+
for hit in mem.recall("what did Acme sign?", top_k=5):
|
|
39
|
+
print(f"[{hit.origin}] {hit.content} (confidence={hit.confidence})")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Four ways to ask
|
|
43
|
+
|
|
44
|
+
Which one you want depends on what you already know. Reaching for the wrong one
|
|
45
|
+
is a quiet source of wrong answers.
|
|
46
|
+
|
|
47
|
+
| You know | Method | Returns |
|
|
48
|
+
|---|---|---|
|
|
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 |
|
|
51
|
+
| only words | `recall(query)` | beliefs ranked by wording, `top_k` |
|
|
52
|
+
| words, and want what they connect to | `associate(query)` | the above **plus** each subject's full facts **plus** connected nodes |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# know — you have the subject
|
|
56
|
+
facts = mem.know("MSM-0808") # id or surface name; variants resolve
|
|
57
|
+
for e in facts.settled(min_confidence=0.6):
|
|
58
|
+
print(e.property, "=", e.current_value)
|
|
59
|
+
print(facts.contested, "open questions")
|
|
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
|
+
|
|
67
|
+
# recall — you only have words. entity_id chains straight into know()
|
|
68
|
+
for hit in mem.recall("bathroom leaks", top_k=5):
|
|
69
|
+
detail = mem.know(hit.entity_id)
|
|
70
|
+
|
|
71
|
+
# associate — words, and what they connect to
|
|
72
|
+
assoc = mem.associate("bathroom leaks")
|
|
73
|
+
for node in assoc.nodes:
|
|
74
|
+
print(node["hops"], node["entity"]["name"], node["reached_via"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`recall` is `ORDER BY similarity LIMIT top_k` with **no threshold** — it returns
|
|
78
|
+
rows whatever their score, so an empty result means an empty scope rather than a
|
|
79
|
+
failed search, and a returned row is not by itself evidence of a match. It also
|
|
80
|
+
matches on the etch *narrative*, so an exact article number is compared by vector
|
|
81
|
+
similarity, not string equality. When you know the subject, use `know`.
|
|
82
|
+
|
|
83
|
+
`associate` follows declared relations one hop in both directions, so a fault
|
|
84
|
+
reached by wording leads to the part that fixes it — which shares no word with
|
|
85
|
+
the query.
|
|
86
|
+
|
|
87
|
+
### Async
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from etchmem import AsyncEtchMem
|
|
91
|
+
|
|
92
|
+
async with AsyncEtchMem("http://localhost:8000") as mem:
|
|
93
|
+
await mem.remember("...", source="agent-33", scope="sales")
|
|
94
|
+
hits = await mem.recall("what did Acme sign?")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## API surface
|
|
100
|
+
|
|
101
|
+
The SDK mirrors the REST endpoints one-to-one:
|
|
102
|
+
|
|
103
|
+
| Method | Endpoint | Returns |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `remember(data, *, source, scope, extract_mode="deferred", metadata=None, occurred_at=None)` | `POST /remember` | `RememberResult` |
|
|
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`) |
|
|
107
|
+
| `know(ref, *, as_of=None, entity_type=None, as_of_basis="ingest")` | `GET /entity/{ref}/etches` | `Knowledge` (iterable of `Etch`) |
|
|
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` |
|
|
111
|
+
| `sleep()` | `POST /sleep` | `SleepResult` |
|
|
112
|
+
| `export()` | `POST /export` | `ExportResult` (list of `Etch`) |
|
|
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) |
|
|
115
|
+
| `stats()` | `GET /stats` | `Stats` |
|
|
116
|
+
| `health()` | `GET /health` | `Health` |
|
|
117
|
+
|
|
118
|
+
All responses are plain dataclasses — see `etchmem/models.py`.
|
|
119
|
+
|
|
120
|
+
### remember
|
|
121
|
+
|
|
122
|
+
`extract_mode="immediate"` marks the signal urgent (claims extracted on the next worker tick); `"deferred"` (default) batches it cheaply. Deposits are idempotent — identical content returns `stored=False`.
|
|
123
|
+
|
|
124
|
+
`occurred_at` (ISO-8601 or epoch seconds) declares when the content actually
|
|
125
|
+
*happened*, as distinct from when you sent it. It defaults to ingest time, and
|
|
126
|
+
**you must set it when loading historical records** — otherwise every signal in
|
|
127
|
+
a backfill carries today's date, the server's recency policy has nothing to
|
|
128
|
+
order by, and a 2009 answer competes with a 2022 answer as an equal.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
res = mem.remember(
|
|
132
|
+
"Modul MSM-0808: NT-2405 is the correct supply up to revision C.",
|
|
133
|
+
source="ticket-resolved", scope="innoxel",
|
|
134
|
+
occurred_at="2014-03-17",
|
|
135
|
+
)
|
|
136
|
+
assert res.occurred_at_declared # the parsed value is echoed back — check it
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### know
|
|
140
|
+
|
|
141
|
+
Every belief about one subject, exhaustive and ordered by property. `ref` is an
|
|
142
|
+
entity id (`product_msm_0808`) or a surface name (`MSM-0808`, `MSM 0808`),
|
|
143
|
+
resolved the way ingestion resolved it, so callers never need the internal id
|
|
144
|
+
format. `Knowledge.settled(min_confidence)` filters to what is safe to state as
|
|
145
|
+
fact; `.contested` counts the beliefs the engine could not settle, which belong
|
|
146
|
+
in an internal note rather than a customer-facing answer.
|
|
147
|
+
|
|
148
|
+
### time travel, and which clock
|
|
149
|
+
|
|
150
|
+
Pass `as_of` (ISO-8601) to `recall`, `know` or `associate`. `as_of_basis` picks
|
|
151
|
+
the clock:
|
|
152
|
+
|
|
153
|
+
| Basis | Question it answers |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `"ingest"` (default) | What did we **believe** then — what would this system have told a customer on that date. The question an audit or a dispute asks. |
|
|
156
|
+
| `"event"` | What was **true** then, by the dates on the underlying facts. |
|
|
157
|
+
|
|
158
|
+
They differ whenever a source reports late — a promotion on the 2nd that reached
|
|
159
|
+
you on the 20th is invisible to ingest-basis on the 10th, visible to event-basis:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
mem.know("john@doe.com", as_of="2026-09-10") # believed
|
|
163
|
+
mem.know("john@doe.com", as_of="2026-09-10", as_of_basis="event") # was true
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Event-basis is only as good as your `occurred_at` discipline: signals deposited
|
|
167
|
+
without it fall back to ingest time, and both bases then give the same answer.
|
|
168
|
+
|
|
169
|
+
### sleep
|
|
170
|
+
|
|
171
|
+
The server consolidates on its own cadence. `mem.sleep()` forces one pipeline tick now (batch → extract claims → fold into etches) — useful in tests and demos:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
tick = mem.sleep()
|
|
175
|
+
print(tick.etches_formed, tick.etches_updated, tick.contested)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### history
|
|
179
|
+
|
|
180
|
+
A version is written only when a belief actually **changes**, so six daily
|
|
181
|
+
enrichment runs with one real change give you two versions, not six. Each
|
|
182
|
+
carries both clocks:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
for v in mem.history(etch_id):
|
|
186
|
+
print(v.version, v.current_value, v.status, v.confidence)
|
|
187
|
+
print(" fact true from", v.event_at, "— we learned it", v.created_at)
|
|
188
|
+
```
|
|
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
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Configuration
|
|
228
|
+
|
|
229
|
+
| Parameter | Env var | Default |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `base_url` | `ETCHMEM_BASE_URL` | `http://localhost:8000` |
|
|
232
|
+
| `api_key` | `ETCHMEM_API_KEY` | none (sent as `Authorization: Bearer …` when set) |
|
|
233
|
+
| `timeout` | — | 30 s |
|
|
234
|
+
|
|
235
|
+
You can also inject your own `httpx.Client` / `httpx.AsyncClient` via the `client` parameter (custom TLS, proxies, retries).
|
|
236
|
+
|
|
237
|
+
## Errors
|
|
238
|
+
|
|
239
|
+
All SDK errors derive from `EtchMemError`:
|
|
240
|
+
|
|
241
|
+
- `ConnectionError` — server unreachable (network / timeout)
|
|
242
|
+
- `APIError` — non-2xx response (`.status_code`, `.detail`), with subclasses `NotFoundError` (404), `AuthenticationError` (401/403), `ServerError` (5xx)
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from etchmem import EtchMem, NotFoundError
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
mem.history("no-such-etch")
|
|
249
|
+
except NotFoundError as e:
|
|
250
|
+
print(e.detail)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
pip install -e ".[dev]"
|
|
259
|
+
python -m pytest tests/
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Releasing to PyPI: see [PIP-DEPLOY.md](PIP-DEPLOY.md).
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
@@ -24,8 +24,10 @@ from etchmem.errors import (
|
|
|
24
24
|
NotFoundError, ServerError,
|
|
25
25
|
)
|
|
26
26
|
from etchmem.models import (
|
|
27
|
-
|
|
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__ = [
|
|
@@ -35,11 +37,22 @@ __all__ = [
|
|
|
35
37
|
"RememberResult",
|
|
36
38
|
"RecallResponse",
|
|
37
39
|
"RecallResult",
|
|
40
|
+
"Association",
|
|
41
|
+
"EntityInfo",
|
|
42
|
+
"Knowledge",
|
|
38
43
|
"SleepResult",
|
|
39
44
|
"ExportResult",
|
|
40
45
|
"Etch",
|
|
46
|
+
"EtchList",
|
|
41
47
|
"EtchVersion",
|
|
42
48
|
"History",
|
|
49
|
+
"Dossier",
|
|
50
|
+
"Claim",
|
|
51
|
+
"Signal",
|
|
52
|
+
"DetailItem",
|
|
53
|
+
"Evidence",
|
|
54
|
+
"SourceEvidence",
|
|
55
|
+
"ValueWeight",
|
|
43
56
|
"Stats",
|
|
44
57
|
"Health",
|
|
45
58
|
# errors
|
|
@@ -51,4 +64,4 @@ __all__ = [
|
|
|
51
64
|
"ServerError",
|
|
52
65
|
]
|
|
53
66
|
|
|
54
|
-
__version__ = "0.
|
|
67
|
+
__version__ = "0.4.0"
|