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 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
@@ -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
- Etch, EtchVersion, ExportResult, Health, History, RecallResponse,
28
- RecallResult, RememberResult, SleepResult, Stats,
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.2.0"
67
+ __version__ = "0.4.0"