echo-mem 0.5.3__tar.gz → 0.5.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.
- {echo_mem-0.5.3/src/echo_mem.egg-info → echo_mem-0.5.4}/PKG-INFO +81 -3
- {echo_mem-0.5.3 → echo_mem-0.5.4}/README.md +80 -2
- {echo_mem-0.5.3 → echo_mem-0.5.4}/pyproject.toml +1 -1
- {echo_mem-0.5.3 → echo_mem-0.5.4/src/echo_mem.egg-info}/PKG-INFO +81 -3
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/main.py +8 -1
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/bm25.py +18 -2
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/query_memory.py +179 -12
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/server.py +30 -28
- {echo_mem-0.5.3 → echo_mem-0.5.4}/LICENSE +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/LICENSE-APACHE-2.0 +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/setup.cfg +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_mem.egg-info/SOURCES.txt +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_mem.egg-info/dependency_links.txt +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_mem.egg-info/entry_points.txt +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_mem.egg-info/requires.txt +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_mem.egg-info/top_level.txt +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/audit/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/audit/get_audit_log.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/adopt.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/analyse.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/benchmark.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/calibrate.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/connect.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/dashboard_html.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/export.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/graph.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/health.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/hooks.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/infer_causal.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/initdb.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/install.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/judge.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/llm.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/merge.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/queue.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/quickstart.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/reattribute_cmd.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/recall.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/reconcile.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/reindex.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/session_start.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/skill.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/status.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/stop_gate.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/trial.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/unmerge.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/cli/why.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/client.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/eval/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/eval/external.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/eval/independent.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/eval/retrieval.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/eval/sweep.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/graph/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/graph/communities.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/config.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/db.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/logging.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/pool.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/infra/project.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/activity.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/bootstrap.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/capture.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/embeddings.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/neighbourhood.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/resolution.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/ingestion/write_episode.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/env.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0001_initial_schema.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0002_trial_observations.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0003_project_and_agent_provenance.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0004_bootstrap_state.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0005_recall_save_guards.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0006_project_analysis.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0007_backfill_unknown_agent.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0008_read_events.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0009_read_event_attribution.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0010_stop_gate_fired.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0011_backfill_absent_agent.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0012_read_event_agent.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0013_active_fact_indexes.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0014_session_activity.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0015_retracted_observations.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0016_one_node_per_name.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0017_writer_version.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0018_trial_runs_are_history.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0019_versions_expected_since.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0020_who_closed_a_queued_document.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0021_delivery_evidence.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0022_independent_evaluation.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0023_second_judge.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0024_node_by_id.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0025_lexical_statistics.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0026_late_causal_extraction.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/migrations/versions/0027_lexical_staleness_without_a_scan.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/causality.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/fusion.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/retrieval/salience.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/skill/SKILL.md +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/trial/__init__.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/trial/check.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/trial/observations.py +0 -0
- {echo_mem-0.5.3 → echo_mem-0.5.4}/src/echo_memory/trial/reads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echo-mem
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A long-horizon memory architecture for AI agents
|
|
5
5
|
License-Expression: BUSL-1.1
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -80,6 +80,27 @@ echo-memory install --global
|
|
|
80
80
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
81
81
|
> only the distribution name differs.
|
|
82
82
|
|
|
83
|
+
## Upgrading
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pipx upgrade echo-mem
|
|
87
|
+
echo-memory init-db # apply any new migrations
|
|
88
|
+
echo-memory reindex # rebuild what the new code derives
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
92
|
+
you just installed.
|
|
93
|
+
|
|
94
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
95
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
96
|
+
half understands.
|
|
97
|
+
|
|
98
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
99
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
100
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
101
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
102
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
103
|
+
|
|
83
104
|
## Usage
|
|
84
105
|
|
|
85
106
|
```bash
|
|
@@ -99,6 +120,8 @@ echo-memory infer-causal-hints # type the facts whose own sentence states a
|
|
|
99
120
|
echo-memory eval # retrieval quality against your own store
|
|
100
121
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
101
122
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
123
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
124
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
102
125
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
103
126
|
echo-memory benchmark # write, query and digest latency
|
|
104
127
|
```
|
|
@@ -108,7 +131,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
108
131
|
| Tool | What it does |
|
|
109
132
|
|---|---|
|
|
110
133
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
111
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
134
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
112
135
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
113
136
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
114
137
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -128,6 +151,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
128
151
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
129
152
|
used to claim it in the present tense.
|
|
130
153
|
|
|
154
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
155
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
156
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
157
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
158
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
159
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
160
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
161
|
+
business reaching a field callers read as relevance.
|
|
162
|
+
|
|
163
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
164
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
165
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
166
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
167
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
168
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
169
|
+
fusion a coin flip.
|
|
170
|
+
|
|
171
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
172
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
173
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
174
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
175
|
+
returns to `ts_rank` everywhere.
|
|
176
|
+
|
|
177
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
178
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
179
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
180
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
181
|
+
|
|
182
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
183
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
184
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
185
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
186
|
+
entity a fact is about is structural and exact.
|
|
187
|
+
|
|
188
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
189
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
190
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
191
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
192
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
193
|
+
about three entities is a question this model cannot express.
|
|
194
|
+
|
|
195
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
196
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
197
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
198
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
199
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
200
|
+
|
|
201
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
202
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
203
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
204
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
205
|
+
the reason this project refuses that idea.
|
|
206
|
+
|
|
131
207
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
132
208
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
133
209
|
reachable with its history.
|
|
@@ -190,6 +266,7 @@ difference nobody sized is not a result.
|
|
|
190
266
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
191
267
|
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
192
268
|
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
269
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
193
270
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
194
271
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
195
272
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -299,7 +376,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
299
376
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
300
377
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
301
378
|
|
|
302
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
379
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
380
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
303
381
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
304
382
|
|
|
305
383
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -57,6 +57,27 @@ echo-memory install --global
|
|
|
57
57
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
58
58
|
> only the distribution name differs.
|
|
59
59
|
|
|
60
|
+
## Upgrading
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx upgrade echo-mem
|
|
64
|
+
echo-memory init-db # apply any new migrations
|
|
65
|
+
echo-memory reindex # rebuild what the new code derives
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
69
|
+
you just installed.
|
|
70
|
+
|
|
71
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
72
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
73
|
+
half understands.
|
|
74
|
+
|
|
75
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
76
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
77
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
78
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
79
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
80
|
+
|
|
60
81
|
## Usage
|
|
61
82
|
|
|
62
83
|
```bash
|
|
@@ -76,6 +97,8 @@ echo-memory infer-causal-hints # type the facts whose own sentence states a
|
|
|
76
97
|
echo-memory eval # retrieval quality against your own store
|
|
77
98
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
78
99
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
100
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
101
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
79
102
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
80
103
|
echo-memory benchmark # write, query and digest latency
|
|
81
104
|
```
|
|
@@ -85,7 +108,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
85
108
|
| Tool | What it does |
|
|
86
109
|
|---|---|
|
|
87
110
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
88
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
111
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
89
112
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
90
113
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
91
114
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -105,6 +128,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
105
128
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
106
129
|
used to claim it in the present tense.
|
|
107
130
|
|
|
131
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
132
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
133
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
134
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
135
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
136
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
137
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
138
|
+
business reaching a field callers read as relevance.
|
|
139
|
+
|
|
140
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
141
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
142
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
143
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
144
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
145
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
146
|
+
fusion a coin flip.
|
|
147
|
+
|
|
148
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
149
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
150
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
151
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
152
|
+
returns to `ts_rank` everywhere.
|
|
153
|
+
|
|
154
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
155
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
156
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
157
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
158
|
+
|
|
159
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
160
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
161
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
162
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
163
|
+
entity a fact is about is structural and exact.
|
|
164
|
+
|
|
165
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
166
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
167
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
168
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
169
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
170
|
+
about three entities is a question this model cannot express.
|
|
171
|
+
|
|
172
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
173
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
174
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
175
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
176
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
177
|
+
|
|
178
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
179
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
180
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
181
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
182
|
+
the reason this project refuses that idea.
|
|
183
|
+
|
|
108
184
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
109
185
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
110
186
|
reachable with its history.
|
|
@@ -167,6 +243,7 @@ difference nobody sized is not a result.
|
|
|
167
243
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
168
244
|
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
169
245
|
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
246
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
170
247
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
171
248
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
172
249
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -276,7 +353,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
276
353
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
277
354
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
278
355
|
|
|
279
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
356
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
357
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
280
358
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
281
359
|
|
|
282
360
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# (`-m echo_memory.server`) and every existing install reference it, and
|
|
7
7
|
# renaming the module would break live configuration to fix a cosmetic mismatch.
|
|
8
8
|
name = "echo-mem"
|
|
9
|
-
version = "0.5.
|
|
9
|
+
version = "0.5.4"
|
|
10
10
|
description = "A long-horizon memory architecture for AI agents"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
# BUSL-1.1 is the SPDX identifier for the Business Source License 1.1, so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echo-mem
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A long-horizon memory architecture for AI agents
|
|
5
5
|
License-Expression: BUSL-1.1
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -80,6 +80,27 @@ echo-memory install --global
|
|
|
80
80
|
> hosted product. The import package and the CLI are both `echo_memory` / `echo-memory`;
|
|
81
81
|
> only the distribution name differs.
|
|
82
82
|
|
|
83
|
+
## Upgrading
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pipx upgrade echo-mem
|
|
87
|
+
echo-memory init-db # apply any new migrations
|
|
88
|
+
echo-memory reindex # rebuild what the new code derives
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both steps, in that order, and neither is optional on a store that predates the version
|
|
92
|
+
you just installed.
|
|
93
|
+
|
|
94
|
+
**Skipping `init-db` fails loudly.** A read against a schema older than the code raises an
|
|
95
|
+
undefined column, which is the good case: it stops rather than answering from something it
|
|
96
|
+
half understands.
|
|
97
|
+
|
|
98
|
+
**Skipping `reindex` fails silently, which is worse.** Derived data that the new code reads
|
|
99
|
+
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
|
|
100
|
+
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
|
|
101
|
+
your queries keep ranking by `ts_rank` while the setting reports as on. `reindex` rebuilds
|
|
102
|
+
the embeddings and those statistics together. `echo-memory health` tells you afterwards.
|
|
103
|
+
|
|
83
104
|
## Usage
|
|
84
105
|
|
|
85
106
|
```bash
|
|
@@ -99,6 +120,8 @@ echo-memory infer-causal-hints # type the facts whose own sentence states a
|
|
|
99
120
|
echo-memory eval # retrieval quality against your own store
|
|
100
121
|
echo-memory eval --context # what a recall costs against injecting everything
|
|
101
122
|
echo-memory eval --context --sweep # the same, as a curve across corpus size
|
|
123
|
+
echo-memory eval-external locomo <path> # LoCoMo, the corpus the published figures use
|
|
124
|
+
echo-memory eval-external longmemeval <path> # LongMemEval, the same
|
|
102
125
|
echo-memory calibrate # is entity resolution trustworthy on your data
|
|
103
126
|
echo-memory benchmark # write, query and digest latency
|
|
104
127
|
```
|
|
@@ -108,7 +131,7 @@ echo-memory benchmark # write, query and digest latency
|
|
|
108
131
|
| Tool | What it does |
|
|
109
132
|
|---|---|
|
|
110
133
|
| `write_episode` | Store entities and the facts connecting them. No model call. |
|
|
111
|
-
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. |
|
|
134
|
+
| `query_memory` | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes `as_of` to read the store as it stood at an instant, and `about` to return the facts recorded against one or two named entities. |
|
|
112
135
|
| `trace_cause` | Causal chains through the entities a subject matches, not a ranked list. |
|
|
113
136
|
| `record_recall_save` | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
|
|
114
137
|
| `get_audit_log` | Every change to memory, with a plain language reason. |
|
|
@@ -128,6 +151,59 @@ no summarisation, and retrieval today walks every active fact in the scope. It i
|
|
|
128
151
|
described in [`docs/designs/`](docs/designs/) and listed below as v1c, and this paragraph
|
|
129
152
|
used to claim it in the present tense.
|
|
130
153
|
|
|
154
|
+
**A ranked answer that says why each fact is in it.** Every returned fact carries
|
|
155
|
+
`score`, its cosine similarity to the query, `rank`, its position, and `matched`, the
|
|
156
|
+
channels that found it. `score` is deliberately not the fusion number: reciprocal rank
|
|
157
|
+
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
|
|
158
|
+
similarity means the same thing every time, which is what a caller thresholding on it
|
|
159
|
+
needs. It is computed for every fact returned, including the ones only full text search
|
|
160
|
+
found, because which channel retrieved a fact is an implementation detail and has no
|
|
161
|
+
business reaching a field callers read as relevance.
|
|
162
|
+
|
|
163
|
+
**The full text channel ranks by BM25.** Postgres's `ts_rank` counts term occurrences and
|
|
164
|
+
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
|
|
165
|
+
much as one in three; no saturation, so repetition scales linearly; no length
|
|
166
|
+
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
|
|
167
|
+
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
|
|
168
|
+
position and never the score underneath it, so a channel ordered by scan order handed the
|
|
169
|
+
fusion a coin flip.
|
|
170
|
+
|
|
171
|
+
BM25 is computed in SQL rather than installed, so no extension is added to your database.
|
|
172
|
+
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
|
|
173
|
+
the scope's lock, and by `echo-memory reindex`; a scope that has none falls back to
|
|
174
|
+
`ts_rank` rather than ranking on statistics it does not have. `ECHO_MEMORY_LEXICAL_BM25=0`
|
|
175
|
+
returns to `ts_rank` everywhere.
|
|
176
|
+
|
|
177
|
+
**Facts about an entity, which resemblance cannot express.** `query_memory` takes
|
|
178
|
+
`about`, one or two entity names, and returns only the facts recorded against them. One
|
|
179
|
+
name gives the edges incident to that node; two give the edges between them in either
|
|
180
|
+
direction, which is "both names in the same fact" without going near the fact text.
|
|
181
|
+
|
|
182
|
+
This is not a ranking improvement and it is not a substitute for one. A query about one
|
|
183
|
+
team returns a semantically close fact about a different team because the wrong fact
|
|
184
|
+
genuinely does resemble the question, and no amount of better ranking removes it.
|
|
185
|
+
Identity is not similarity, and a fact is an edge between two nodes, so asking which
|
|
186
|
+
entity a fact is about is structural and exact.
|
|
187
|
+
|
|
188
|
+
Name matching is exact and case insensitive, never a substring, because a short name is a
|
|
189
|
+
substring of longer unrelated words. A name nothing is recorded under returns no facts
|
|
190
|
+
rather than the nearest thing, and so does a pair with no fact joining them: that is real
|
|
191
|
+
absence and it means nobody recorded this, not here is something adjacent. More than two
|
|
192
|
+
names is refused rather than answered empty, because an edge has two endpoints and a fact
|
|
193
|
+
about three entities is a question this model cannot express.
|
|
194
|
+
|
|
195
|
+
**The history the store has always kept is readable.** Every fact has carried `t_valid`
|
|
196
|
+
and `t_invalid` since the first migration, and the read path had only ever asked whether a
|
|
197
|
+
fact is current, so the store paid to keep the whole record of what a scope believed and
|
|
198
|
+
could not answer the first question a post mortem asks. `query_memory` takes `as_of` in
|
|
199
|
+
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
|
|
200
|
+
|
|
201
|
+
Supersession is what makes that more than a curiosity. Writing the same source, target and
|
|
202
|
+
relation again does not edit the old fact, it ends it: the old edge takes a `t_invalid` and
|
|
203
|
+
stays queryable. Nothing is ever rewritten, so the history is real rather than
|
|
204
|
+
reconstructed, which is the property the memory reconsolidation literature gives up and
|
|
205
|
+
the reason this project refuses that idea.
|
|
206
|
+
|
|
131
207
|
**Provenance on every fact.** Who wrote it, which tool, which project, when, and which
|
|
132
208
|
reads returned it. A superseded fact is never deleted. It stops being drawn and stays
|
|
133
209
|
reachable with its history.
|
|
@@ -190,6 +266,7 @@ difference nobody sized is not a result.
|
|
|
190
266
|
| The same saving across 8x of corpus growth | 75.5% at 32 facts rising to **96.4% at 261**, hit@10 0.900 to 0.946 | `echo-memory eval --context --sweep` |
|
|
191
267
|
| LoCoMo retrieval, 1,982 questions, 5,882 turns | recall@10 **0.601**, hit@10 0.658, MRR 0.460, session@10 0.850 | `echo-memory eval-external locomo` |
|
|
192
268
|
| LongMemEval retrieval, 90 questions, 15 per type | session@10 **0.937**, recall@10 0.727, MRR 0.428 | `echo-memory eval-external longmemeval --per-type 15` |
|
|
269
|
+
| Lexical BM25 against `ts_rank` | **+0.0271** MRR on entity_pair [+0.0083, +0.0481] and on multihop [+0.0166, +0.0381], the other two shapes inside noise, none worse | `echo-memory eval --ablate`, the `without BM25` row with its sign reversed |
|
|
193
270
|
| Server side model calls per write | **0** | `echo-memory benchmark` |
|
|
194
271
|
| Write, query, digest latency (median) | 15ms, 8ms, 1ms | `echo-memory benchmark` |
|
|
195
272
|
| Entity resolution AUC | 0.666, 95% CI [0.421, 0.881] | `echo-memory calibrate` |
|
|
@@ -299,7 +376,8 @@ not a thing to withhold from the person whose data it is.
|
|
|
299
376
|
organisation wide shared graph, with no forced migration later. The novel work is the
|
|
300
377
|
memory structure and the read/write algorithm on top of it, not a new database engine.
|
|
301
378
|
|
|
302
|
-
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a
|
|
379
|
+
**Retrieval** Hybrid vector and full text search fused by reciprocal rank in v1a, with
|
|
380
|
+
the full text side ranked by BM25 computed in SQL over per scope corpus statistics.
|
|
303
381
|
Personalised PageRank via `networkx` lands in v1b for multi hop associative retrieval.
|
|
304
382
|
|
|
305
383
|
**Interface** [Model Context Protocol](https://modelcontextprotocol.io), so any compliant
|
|
@@ -825,7 +825,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
825
825
|
("vector only", {"vector_only": True}),
|
|
826
826
|
("lexical only", {"lexical_only": True}),
|
|
827
827
|
("+ graph hop", {"graph_hops": 1}),
|
|
828
|
-
|
|
828
|
+
# Subtractive, not additive, because BM25 is the default since
|
|
829
|
+
# 0.5.3. An ablation reads a difference against shipping, so
|
|
830
|
+
# "+ BM25" against a shipping baseline that already has BM25
|
|
831
|
+
# measured one configuration against itself and reported
|
|
832
|
+
# +0.0000 with a zero width interval on every shape - a row
|
|
833
|
+
# that looks like "no effect" and is really "no experiment".
|
|
834
|
+
# The others stay additive because they are still off.
|
|
835
|
+
("without BM25", {"lexical_bm25": False}),
|
|
829
836
|
("+ routed expansion", {"route_expansion": True}),
|
|
830
837
|
("+ salience", {"use_salience": True}),
|
|
831
838
|
]
|
|
@@ -164,7 +164,9 @@ def usable(conn, group_id: str) -> bool:
|
|
|
164
164
|
return not _grown_past_tolerance(row)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
def candidates(
|
|
167
|
+
def candidates(
|
|
168
|
+
conn, group_id: str, terms: list[str], limit: int, only: list[str] | None = None,
|
|
169
|
+
) -> list[tuple[str, float]]:
|
|
168
170
|
"""(edge_id, bm25 score), best first, for facts matching any query term.
|
|
169
171
|
|
|
170
172
|
Scored only over rows that matched, which is what makes the per-document
|
|
@@ -186,6 +188,19 @@ def candidates(conn, group_id: str, terms: list[str], limit: int) -> list[tuple[
|
|
|
186
188
|
"""
|
|
187
189
|
if not terms:
|
|
188
190
|
return []
|
|
191
|
+
# An explicit id restriction, for a read scoped to an entity. The filter
|
|
192
|
+
# sits inside the matched CTE rather than around the result, so a scoped
|
|
193
|
+
# read ranks only the facts in scope instead of ranking the whole scope and
|
|
194
|
+
# discarding most of it.
|
|
195
|
+
only_sql = "" if only is None else "AND f.id::text = ANY(%s)"
|
|
196
|
+
# Positional, so the order has to match where each %s lands in the SQL
|
|
197
|
+
# below: scope's group_id, the terms, weighted's group_id, matched's
|
|
198
|
+
# group_id, then this filter (it sits INSIDE matched, after that
|
|
199
|
+
# group_id), then the limit.
|
|
200
|
+
params: list = [group_id, list(terms), group_id, group_id]
|
|
201
|
+
if only is not None:
|
|
202
|
+
params.append([str(e) for e in only])
|
|
203
|
+
params.append(limit)
|
|
189
204
|
rows = conn.execute(
|
|
190
205
|
f"""
|
|
191
206
|
WITH scope AS (
|
|
@@ -222,6 +237,7 @@ def candidates(conn, group_id: str, terms: list[str], limit: int) -> list[tuple[
|
|
|
222
237
|
LATERAL unnest(to_tsvector('english', {_FACT_TEXT})) AS t
|
|
223
238
|
WHERE {_SCOPED} AND {_ACTIVE}
|
|
224
239
|
AND t.lexeme IN (SELECT lexeme FROM weighted)
|
|
240
|
+
{only_sql}
|
|
225
241
|
)
|
|
226
242
|
SELECT m.id::text,
|
|
227
243
|
sum(w.idf * (m.tf * ({K1} + 1))
|
|
@@ -236,7 +252,7 @@ def candidates(conn, group_id: str, terms: list[str], limit: int) -> list[tuple[
|
|
|
236
252
|
ORDER BY score DESC, m.id
|
|
237
253
|
LIMIT %s
|
|
238
254
|
""",
|
|
239
|
-
|
|
255
|
+
params,
|
|
240
256
|
).fetchall()
|
|
241
257
|
return [(str(edge_id), float(score)) for edge_id, score in rows]
|
|
242
258
|
|