refindery 0.1.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.
- refindery-0.1.0/PKG-INFO +233 -0
- refindery-0.1.0/README.md +172 -0
- refindery-0.1.0/pyproject.toml +153 -0
- refindery-0.1.0/src/refindery/__init__.py +3 -0
- refindery-0.1.0/src/refindery/__main__.py +6 -0
- refindery-0.1.0/src/refindery/adapters/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/chunking/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/chunking/chonkie_chunker.py +73 -0
- refindery-0.1.0/src/refindery/adapters/clock.py +11 -0
- refindery-0.1.0/src/refindery/adapters/cluster/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/cluster/engine.py +53 -0
- refindery-0.1.0/src/refindery/adapters/cluster/worker.py +208 -0
- refindery-0.1.0/src/refindery/adapters/embedding/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/embedding/catsu_embedder.py +84 -0
- refindery-0.1.0/src/refindery/adapters/embedding/surface_forms.py +53 -0
- refindery-0.1.0/src/refindery/adapters/extraction/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/extraction/http_fetcher.py +56 -0
- refindery-0.1.0/src/refindery/adapters/extraction/pdf_pypdf.py +32 -0
- refindery-0.1.0/src/refindery/adapters/extraction/pulpie_html.py +82 -0
- refindery-0.1.0/src/refindery/adapters/extractors/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/extractors/chain.py +48 -0
- refindery-0.1.0/src/refindery/adapters/extractors/gazetteer.py +66 -0
- refindery-0.1.0/src/refindery/adapters/extractors/gliner_spacy.py +97 -0
- refindery-0.1.0/src/refindery/adapters/extractors/llm.py +86 -0
- refindery-0.1.0/src/refindery/adapters/extractors/spacy_ner.py +69 -0
- refindery-0.1.0/src/refindery/adapters/llm/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/llm/openai_compat.py +70 -0
- refindery-0.1.0/src/refindery/adapters/metadata/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/0001_initial.sql +75 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/0002_forget.sql +12 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/0003_entities_clusters.sql +94 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/0004_backfills.sql +13 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/0005_jobs_page_id.sql +7 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrations/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/metadata/migrator.py +77 -0
- refindery-0.1.0/src/refindery/adapters/metadata/sqlite_store.py +1531 -0
- refindery-0.1.0/src/refindery/adapters/observability/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/observability/duckdb_sink.py +137 -0
- refindery-0.1.0/src/refindery/adapters/observability/logging.py +64 -0
- refindery-0.1.0/src/refindery/adapters/observability/metrics.py +76 -0
- refindery-0.1.0/src/refindery/adapters/observability/otel.py +58 -0
- refindery-0.1.0/src/refindery/adapters/observability/query_log.py +109 -0
- refindery-0.1.0/src/refindery/adapters/observability/query_log_reader.py +102 -0
- refindery-0.1.0/src/refindery/adapters/queue/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/queue/huey_queue.py +243 -0
- refindery-0.1.0/src/refindery/adapters/reranking/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/reranking/api.py +55 -0
- refindery-0.1.0/src/refindery/adapters/vector/__init__.py +1 -0
- refindery-0.1.0/src/refindery/adapters/vector/hybrid.py +44 -0
- refindery-0.1.0/src/refindery/adapters/vector/lancedb_store.py +326 -0
- refindery-0.1.0/src/refindery/adapters/vector/names.py +16 -0
- refindery-0.1.0/src/refindery/adapters/vector/qdrant_store.py +325 -0
- refindery-0.1.0/src/refindery/adapters/vector/sparse.py +45 -0
- refindery-0.1.0/src/refindery/api/__init__.py +1 -0
- refindery-0.1.0/src/refindery/api/app.py +65 -0
- refindery-0.1.0/src/refindery/api/auth.py +88 -0
- refindery-0.1.0/src/refindery/api/deps.py +10 -0
- refindery-0.1.0/src/refindery/api/mcp.py +59 -0
- refindery-0.1.0/src/refindery/api/routes/__init__.py +1 -0
- refindery-0.1.0/src/refindery/api/routes/clusters.py +110 -0
- refindery-0.1.0/src/refindery/api/routes/compare.py +79 -0
- refindery-0.1.0/src/refindery/api/routes/entities.py +99 -0
- refindery-0.1.0/src/refindery/api/routes/forget.py +89 -0
- refindery-0.1.0/src/refindery/api/routes/health.py +42 -0
- refindery-0.1.0/src/refindery/api/routes/jobs.py +74 -0
- refindery-0.1.0/src/refindery/api/routes/models.py +166 -0
- refindery-0.1.0/src/refindery/api/routes/pages.py +185 -0
- refindery-0.1.0/src/refindery/api/routes/search.py +205 -0
- refindery-0.1.0/src/refindery/api/schemas.py +478 -0
- refindery-0.1.0/src/refindery/application/__init__.py +1 -0
- refindery-0.1.0/src/refindery/application/container.py +503 -0
- refindery-0.1.0/src/refindery/application/ports/__init__.py +1 -0
- refindery-0.1.0/src/refindery/application/ports/chunker.py +19 -0
- refindery-0.1.0/src/refindery/application/ports/clock.py +12 -0
- refindery-0.1.0/src/refindery/application/ports/cluster_engine.py +42 -0
- refindery-0.1.0/src/refindery/application/ports/content_extractor.py +60 -0
- refindery-0.1.0/src/refindery/application/ports/embedder.py +37 -0
- refindery-0.1.0/src/refindery/application/ports/entity_extractor.py +17 -0
- refindery-0.1.0/src/refindery/application/ports/job_queue.py +43 -0
- refindery-0.1.0/src/refindery/application/ports/metadata_store.py +466 -0
- refindery-0.1.0/src/refindery/application/ports/query_log.py +72 -0
- refindery-0.1.0/src/refindery/application/ports/query_log_reader.py +66 -0
- refindery-0.1.0/src/refindery/application/ports/reranker.py +41 -0
- refindery-0.1.0/src/refindery/application/ports/surface_embedder.py +23 -0
- refindery-0.1.0/src/refindery/application/ports/vector_store.py +147 -0
- refindery-0.1.0/src/refindery/application/services/__init__.py +1 -0
- refindery-0.1.0/src/refindery/application/services/backfill.py +210 -0
- refindery-0.1.0/src/refindery/application/services/canonicalization.py +205 -0
- refindery-0.1.0/src/refindery/application/services/cluster_triggers.py +54 -0
- refindery-0.1.0/src/refindery/application/services/clustering_run.py +400 -0
- refindery-0.1.0/src/refindery/application/services/compare_service.py +299 -0
- refindery-0.1.0/src/refindery/application/services/entity_ingest.py +74 -0
- refindery-0.1.0/src/refindery/application/services/eval_service.py +360 -0
- refindery-0.1.0/src/refindery/application/services/extraction_router.py +38 -0
- refindery-0.1.0/src/refindery/application/services/feedback_service.py +28 -0
- refindery-0.1.0/src/refindery/application/services/forget_service.py +166 -0
- refindery-0.1.0/src/refindery/application/services/indexed_pages.py +27 -0
- refindery-0.1.0/src/refindery/application/services/indexing.py +245 -0
- refindery-0.1.0/src/refindery/application/services/ingest.py +150 -0
- refindery-0.1.0/src/refindery/application/services/model_registry.py +113 -0
- refindery-0.1.0/src/refindery/application/services/search_service.py +497 -0
- refindery-0.1.0/src/refindery/application/services/similarity_service.py +196 -0
- refindery-0.1.0/src/refindery/application/timing.py +31 -0
- refindery-0.1.0/src/refindery/cli.py +256 -0
- refindery-0.1.0/src/refindery/config.py +302 -0
- refindery-0.1.0/src/refindery/domain/__init__.py +5 -0
- refindery-0.1.0/src/refindery/domain/canonical_url.py +100 -0
- refindery-0.1.0/src/refindery/domain/clustering.py +220 -0
- refindery-0.1.0/src/refindery/domain/content_hash.py +8 -0
- refindery-0.1.0/src/refindery/domain/ctfidf.py +59 -0
- refindery-0.1.0/src/refindery/domain/entities.py +84 -0
- refindery-0.1.0/src/refindery/domain/errors.py +119 -0
- refindery-0.1.0/src/refindery/domain/ids.py +58 -0
- refindery-0.1.0/src/refindery/domain/models.py +250 -0
- refindery-0.1.0/src/refindery/domain/ranking_metrics.py +149 -0
- refindery-0.1.0/src/refindery/domain/retrieval.py +147 -0
- refindery-0.1.0/src/refindery/domain/rollup.py +43 -0
refindery-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: refindery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local, single-machine retrieval engine over the web pages you read.
|
|
5
|
+
Keywords: retrieval,search,rag,embeddings,mcp,browser-history
|
|
6
|
+
Author: Harold Martin
|
|
7
|
+
Author-email: Harold Martin <harold.martin@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
14
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
15
|
+
Requires-Dist: catsu>=0.1.8
|
|
16
|
+
Requires-Dist: chonkie>=1.7.0
|
|
17
|
+
Requires-Dist: duckdb>=1.5.4
|
|
18
|
+
Requires-Dist: fastapi>=0.139.0
|
|
19
|
+
Requires-Dist: fastapi-mcp>=0.4.0
|
|
20
|
+
Requires-Dist: fastembed>=0.8.0
|
|
21
|
+
Requires-Dist: httpx>=0.28.1
|
|
22
|
+
Requires-Dist: huey>=3.1.1
|
|
23
|
+
Requires-Dist: inflect>=7.5.0
|
|
24
|
+
Requires-Dist: lancedb>=0.34.0
|
|
25
|
+
Requires-Dist: model2vec>=0.8.2
|
|
26
|
+
Requires-Dist: numpy>=2.2
|
|
27
|
+
Requires-Dist: opentelemetry-api>=1.43.0
|
|
28
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.43.0
|
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.64b0
|
|
30
|
+
Requires-Dist: opentelemetry-sdk>=1.43.0
|
|
31
|
+
Requires-Dist: prometheus-client>=0.25.0
|
|
32
|
+
Requires-Dist: pyarrow>=24.0.0
|
|
33
|
+
Requires-Dist: pydantic>=2.13.4
|
|
34
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
35
|
+
Requires-Dist: pypdf>=6.14.2
|
|
36
|
+
Requires-Dist: qdrant-client>=1.18.0
|
|
37
|
+
Requires-Dist: rerankers>=0.10.0
|
|
38
|
+
Requires-Dist: scikit-learn>=1.9.0
|
|
39
|
+
Requires-Dist: scipy>=1.18.0
|
|
40
|
+
Requires-Dist: spacy>=3.8.3,<4
|
|
41
|
+
Requires-Dist: transformers>=5.13.0
|
|
42
|
+
Requires-Dist: umap-learn>=0.5.12
|
|
43
|
+
Requires-Dist: uuid6>=2025.0.1
|
|
44
|
+
Requires-Dist: uvicorn>=0.51.0
|
|
45
|
+
Requires-Dist: gliner>=0.2.24 ; extra == 'gliner'
|
|
46
|
+
Requires-Dist: gliner-spacy>=0.0.11 ; extra == 'gliner'
|
|
47
|
+
Requires-Dist: onnxruntime>=1.27.0 ; extra == 'gliner'
|
|
48
|
+
Requires-Dist: torch>=2.13.0 ; extra == 'html'
|
|
49
|
+
Requires-Dist: igraph>=1.0.0 ; extra == 'leiden'
|
|
50
|
+
Requires-Dist: leidenalg>=0.12.0 ; extra == 'leiden'
|
|
51
|
+
Requires-Dist: en-core-web-sm ; extra == 'ner'
|
|
52
|
+
Requires-Python: >=3.13
|
|
53
|
+
Project-URL: Homepage, https://github.com/hbmartin/refindery
|
|
54
|
+
Project-URL: Repository, https://github.com/hbmartin/refindery
|
|
55
|
+
Project-URL: Changelog, https://github.com/hbmartin/refindery/releases
|
|
56
|
+
Provides-Extra: gliner
|
|
57
|
+
Provides-Extra: html
|
|
58
|
+
Provides-Extra: leiden
|
|
59
|
+
Provides-Extra: ner
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
|
|
62
|
+
# Refindery
|
|
63
|
+
|
|
64
|
+
A local, single-machine retrieval engine over the web pages you read.
|
|
65
|
+
|
|
66
|
+
Upstream capture systems (browser extensions, history readers) extract
|
|
67
|
+
main-body text and POST it here. Refindery chunks, embeds, indexes, clusters,
|
|
68
|
+
and extracts entities from that text, then serves hybrid retrieval over it via
|
|
69
|
+
a local HTTP API and an MCP server.
|
|
70
|
+
|
|
71
|
+
**This is a retrieval engine, not a Q&A system.** It returns ranked, grounded
|
|
72
|
+
passages with provenance. Synthesis is the caller's job — typically an LLM
|
|
73
|
+
agent (e.g. Claude via MCP) that treats Refindery as a tool. No generation
|
|
74
|
+
appears on the query path.
|
|
75
|
+
|
|
76
|
+
## Jobs to be done
|
|
77
|
+
|
|
78
|
+
1. **Refind** — "I read something about X, take me back to it."
|
|
79
|
+
2. **Synthesize** — "What have I learned about Y?" (agent-mediated; Refindery supplies the passages)
|
|
80
|
+
3. **Resurface** — "What have I been reading a lot about?" (clusters, similarity)
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
Hexagonal / ports-and-adapters. A single non-blocking asyncio process hosts
|
|
85
|
+
the FastAPI app, the MCP server, and the job queue consumer; CPU-bound work
|
|
86
|
+
runs in a process pool.
|
|
87
|
+
|
|
88
|
+
| Port | Default adapter | Alternatives |
|
|
89
|
+
| ----------------- | ------------------------------ | ---------------------------------- |
|
|
90
|
+
| `VectorStore` | Qdrant (Docker) | LanceDB (in-process, zero daemon) |
|
|
91
|
+
| `MetadataStore` | SQLite (WAL) | Postgres (v2) |
|
|
92
|
+
| `Embedder` | Voyage (via catsu) | Cohere, OpenAI, local |
|
|
93
|
+
| `Reranker` | Cohere / Voyage (via rerankers)| local cross-encoders |
|
|
94
|
+
| `EntityExtractor` | spaCy + gazetteer | GLiNER (extra), LLM |
|
|
95
|
+
| `ClusterEngine` | UMAP + HDBSCAN | KMeans, Leiden (extra) |
|
|
96
|
+
|
|
97
|
+
Observability: OpenTelemetry traces (off by default), structured JSON logs,
|
|
98
|
+
Prometheus `/metrics`, and a DuckDB append-only query log — the substrate for
|
|
99
|
+
offline retrieval evals: `refindery eval score` computes nDCG/MRR/recall from
|
|
100
|
+
logged queries + `/v1/feedback` labels, and `refindery eval replay` diffs two
|
|
101
|
+
configurations (models or rerank on/off) over the same golden set.
|
|
102
|
+
|
|
103
|
+
## Quickstart
|
|
104
|
+
|
|
105
|
+
### Minimal profile (no Docker)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv sync --extra ner
|
|
109
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
110
|
+
export REFINDERY_VECTOR_STORE=lancedb
|
|
111
|
+
export VOYAGE_API_KEY=... # or configure another embedding provider
|
|
112
|
+
python -m refindery
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Docker profile (Qdrant, the default store)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uv sync --extra ner
|
|
119
|
+
docker compose up -d qdrant
|
|
120
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
121
|
+
python -m refindery
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Fully containerized
|
|
125
|
+
|
|
126
|
+
The multi-stage `Dockerfile` builds a slim image with the `ner` extra
|
|
127
|
+
(no torch/gliner; add extras to the sync lines if you need them). Data
|
|
128
|
+
lives on the `refindery_data` volume, model caches on `refindery_models`.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
132
|
+
export VOYAGE_API_KEY=...
|
|
133
|
+
docker compose up -d --build
|
|
134
|
+
curl -s http://127.0.0.1:8000/healthz
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Ingest and search
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
curl -s -X POST http://127.0.0.1:8000/v1/pages \
|
|
141
|
+
-H "Authorization: Bearer $REFINDERY_AUTH_TOKEN" \
|
|
142
|
+
-H "Content-Type: application/json" \
|
|
143
|
+
-d '{"url": "https://example.com/article", "title": "An Article",
|
|
144
|
+
"body_extracted": "Plain text main body content...",
|
|
145
|
+
"fetched_at": "2026-07-08T10:00:00Z", "source": "extension"}'
|
|
146
|
+
|
|
147
|
+
curl -s -X POST http://127.0.0.1:8000/v1/search \
|
|
148
|
+
-H "Authorization: Bearer $REFINDERY_AUTH_TOKEN" \
|
|
149
|
+
-H "Content-Type: application/json" \
|
|
150
|
+
-d '{"query": "main body content"}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### MCP
|
|
154
|
+
|
|
155
|
+
The MCP server is served over streamable HTTP at `/mcp` (same bearer token):
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
claude mcp add --transport http refindery http://127.0.0.1:8000/mcp \
|
|
159
|
+
--header "Authorization: Bearer $REFINDERY_AUTH_TOKEN"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Read-only tools are exposed by default (`search`, `get_page`, `similar_to`,
|
|
163
|
+
`list_clusters`, …). Mutating tools (`add_page`, `forget`) are opt-in via
|
|
164
|
+
`REFINDERY_MCP__ENABLE_MUTATING_TOOLS=true`. That flag controls visibility
|
|
165
|
+
only — authorization comes from the token's scopes on every transport.
|
|
166
|
+
|
|
167
|
+
### Auth tokens and scopes
|
|
168
|
+
|
|
169
|
+
`REFINDERY_AUTH_TOKEN` is a single full-access token. To hand each capture
|
|
170
|
+
source or agent its own revocable token, configure named tokens with `read`
|
|
171
|
+
or `write` scopes (write implies read; both forms can coexist):
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
export REFINDERY_AUTH_TOKENS='[
|
|
175
|
+
{"name": "chrome-capture", "token": "...", "scopes": ["write"]},
|
|
176
|
+
{"name": "agent", "token": "...", "scopes": ["read"]}
|
|
177
|
+
]'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Read-scoped tokens can search, browse, compare, and record feedback;
|
|
181
|
+
mutating endpoints (`add_page`, `forget`, model management, …) return 403
|
|
182
|
+
without the `write` scope.
|
|
183
|
+
|
|
184
|
+
## HTTP API
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
POST /v1/pages ingest (body_extracted XOR body_html; neither → fetch)
|
|
188
|
+
GET /v1/pages/{id} full body_text + metadata
|
|
189
|
+
GET /v1/pages/{id}/status queued|indexing|indexed|failed|dead
|
|
190
|
+
GET /v1/pages/{id}/similar ?mediation=vector|cluster|entity&k=
|
|
191
|
+
GET /v1/pages/{id}/entities
|
|
192
|
+
POST /v1/search hybrid dense+sparse, RRF, rerank, filters
|
|
193
|
+
POST /v1/compare A/B embedding models (Jaccard@k, RBO, Kendall's τ)
|
|
194
|
+
GET /v1/clusters ?include_tombstoned=false
|
|
195
|
+
GET /v1/clusters/{id}
|
|
196
|
+
POST /v1/clusters/recompute
|
|
197
|
+
GET /v1/entities/{id_or_form}
|
|
198
|
+
POST /v1/forget purge + blacklist atomically
|
|
199
|
+
GET /v1/blacklist
|
|
200
|
+
DELETE /v1/blacklist/{id}
|
|
201
|
+
POST /v1/models register embedding model
|
|
202
|
+
POST /v1/models/{id}/backfill dry-run estimate, then confirm
|
|
203
|
+
POST /v1/models/{id}/activate
|
|
204
|
+
DELETE /v1/models/{id}
|
|
205
|
+
POST /v1/feedback { query_id, page_id, relevant }
|
|
206
|
+
GET /healthz /readyz /metrics
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Bearer token is always required, even on loopback. The server binds to
|
|
210
|
+
`127.0.0.1` by default.
|
|
211
|
+
|
|
212
|
+
## Optional extras
|
|
213
|
+
|
|
214
|
+
| Extra | Enables | Pulls in |
|
|
215
|
+
| ----------- | ---------------------------------------------- | ------------------- |
|
|
216
|
+
| `html` | `body_html` / fetched-HTML extraction (pulpie) | torch (~2 GB) |
|
|
217
|
+
| `gliner` | GLiNER zero-shot NER | gliner, onnxruntime |
|
|
218
|
+
| `ner` | spaCy NER model | en_core_web_sm |
|
|
219
|
+
| `leiden` | Leiden clustering | igraph, leidenalg |
|
|
220
|
+
|
|
221
|
+
Entity extraction is required at startup. The default extractor chain needs
|
|
222
|
+
the `ner` extra unless you configure a healthy gazetteer, GLiNER, or LLM
|
|
223
|
+
extractor.
|
|
224
|
+
|
|
225
|
+
See [Architecture](docs/architecture.md) for the ports/adapters map, the
|
|
226
|
+
ingest→index→cluster data flow, and the search pipeline. See
|
|
227
|
+
[Operations](docs/operations.md) for alpha reset commands, query-log
|
|
228
|
+
purging, job lease behavior, vector-store caveats, and accepted risks.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). TL;DR: `uv sync --all-groups --extra ner`,
|
|
233
|
+
then `uv run ruff format . && uv run ruff check . && uv run pytest && uv run ty check && uv run pyrefly check`.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Refindery
|
|
2
|
+
|
|
3
|
+
A local, single-machine retrieval engine over the web pages you read.
|
|
4
|
+
|
|
5
|
+
Upstream capture systems (browser extensions, history readers) extract
|
|
6
|
+
main-body text and POST it here. Refindery chunks, embeds, indexes, clusters,
|
|
7
|
+
and extracts entities from that text, then serves hybrid retrieval over it via
|
|
8
|
+
a local HTTP API and an MCP server.
|
|
9
|
+
|
|
10
|
+
**This is a retrieval engine, not a Q&A system.** It returns ranked, grounded
|
|
11
|
+
passages with provenance. Synthesis is the caller's job — typically an LLM
|
|
12
|
+
agent (e.g. Claude via MCP) that treats Refindery as a tool. No generation
|
|
13
|
+
appears on the query path.
|
|
14
|
+
|
|
15
|
+
## Jobs to be done
|
|
16
|
+
|
|
17
|
+
1. **Refind** — "I read something about X, take me back to it."
|
|
18
|
+
2. **Synthesize** — "What have I learned about Y?" (agent-mediated; Refindery supplies the passages)
|
|
19
|
+
3. **Resurface** — "What have I been reading a lot about?" (clusters, similarity)
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
Hexagonal / ports-and-adapters. A single non-blocking asyncio process hosts
|
|
24
|
+
the FastAPI app, the MCP server, and the job queue consumer; CPU-bound work
|
|
25
|
+
runs in a process pool.
|
|
26
|
+
|
|
27
|
+
| Port | Default adapter | Alternatives |
|
|
28
|
+
| ----------------- | ------------------------------ | ---------------------------------- |
|
|
29
|
+
| `VectorStore` | Qdrant (Docker) | LanceDB (in-process, zero daemon) |
|
|
30
|
+
| `MetadataStore` | SQLite (WAL) | Postgres (v2) |
|
|
31
|
+
| `Embedder` | Voyage (via catsu) | Cohere, OpenAI, local |
|
|
32
|
+
| `Reranker` | Cohere / Voyage (via rerankers)| local cross-encoders |
|
|
33
|
+
| `EntityExtractor` | spaCy + gazetteer | GLiNER (extra), LLM |
|
|
34
|
+
| `ClusterEngine` | UMAP + HDBSCAN | KMeans, Leiden (extra) |
|
|
35
|
+
|
|
36
|
+
Observability: OpenTelemetry traces (off by default), structured JSON logs,
|
|
37
|
+
Prometheus `/metrics`, and a DuckDB append-only query log — the substrate for
|
|
38
|
+
offline retrieval evals: `refindery eval score` computes nDCG/MRR/recall from
|
|
39
|
+
logged queries + `/v1/feedback` labels, and `refindery eval replay` diffs two
|
|
40
|
+
configurations (models or rerank on/off) over the same golden set.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
### Minimal profile (no Docker)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync --extra ner
|
|
48
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
49
|
+
export REFINDERY_VECTOR_STORE=lancedb
|
|
50
|
+
export VOYAGE_API_KEY=... # or configure another embedding provider
|
|
51
|
+
python -m refindery
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Docker profile (Qdrant, the default store)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv sync --extra ner
|
|
58
|
+
docker compose up -d qdrant
|
|
59
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
60
|
+
python -m refindery
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Fully containerized
|
|
64
|
+
|
|
65
|
+
The multi-stage `Dockerfile` builds a slim image with the `ner` extra
|
|
66
|
+
(no torch/gliner; add extras to the sync lines if you need them). Data
|
|
67
|
+
lives on the `refindery_data` volume, model caches on `refindery_models`.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export REFINDERY_AUTH_TOKEN="$(openssl rand -hex 24)"
|
|
71
|
+
export VOYAGE_API_KEY=...
|
|
72
|
+
docker compose up -d --build
|
|
73
|
+
curl -s http://127.0.0.1:8000/healthz
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Ingest and search
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -s -X POST http://127.0.0.1:8000/v1/pages \
|
|
80
|
+
-H "Authorization: Bearer $REFINDERY_AUTH_TOKEN" \
|
|
81
|
+
-H "Content-Type: application/json" \
|
|
82
|
+
-d '{"url": "https://example.com/article", "title": "An Article",
|
|
83
|
+
"body_extracted": "Plain text main body content...",
|
|
84
|
+
"fetched_at": "2026-07-08T10:00:00Z", "source": "extension"}'
|
|
85
|
+
|
|
86
|
+
curl -s -X POST http://127.0.0.1:8000/v1/search \
|
|
87
|
+
-H "Authorization: Bearer $REFINDERY_AUTH_TOKEN" \
|
|
88
|
+
-H "Content-Type: application/json" \
|
|
89
|
+
-d '{"query": "main body content"}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### MCP
|
|
93
|
+
|
|
94
|
+
The MCP server is served over streamable HTTP at `/mcp` (same bearer token):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add --transport http refindery http://127.0.0.1:8000/mcp \
|
|
98
|
+
--header "Authorization: Bearer $REFINDERY_AUTH_TOKEN"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Read-only tools are exposed by default (`search`, `get_page`, `similar_to`,
|
|
102
|
+
`list_clusters`, …). Mutating tools (`add_page`, `forget`) are opt-in via
|
|
103
|
+
`REFINDERY_MCP__ENABLE_MUTATING_TOOLS=true`. That flag controls visibility
|
|
104
|
+
only — authorization comes from the token's scopes on every transport.
|
|
105
|
+
|
|
106
|
+
### Auth tokens and scopes
|
|
107
|
+
|
|
108
|
+
`REFINDERY_AUTH_TOKEN` is a single full-access token. To hand each capture
|
|
109
|
+
source or agent its own revocable token, configure named tokens with `read`
|
|
110
|
+
or `write` scopes (write implies read; both forms can coexist):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export REFINDERY_AUTH_TOKENS='[
|
|
114
|
+
{"name": "chrome-capture", "token": "...", "scopes": ["write"]},
|
|
115
|
+
{"name": "agent", "token": "...", "scopes": ["read"]}
|
|
116
|
+
]'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Read-scoped tokens can search, browse, compare, and record feedback;
|
|
120
|
+
mutating endpoints (`add_page`, `forget`, model management, …) return 403
|
|
121
|
+
without the `write` scope.
|
|
122
|
+
|
|
123
|
+
## HTTP API
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
POST /v1/pages ingest (body_extracted XOR body_html; neither → fetch)
|
|
127
|
+
GET /v1/pages/{id} full body_text + metadata
|
|
128
|
+
GET /v1/pages/{id}/status queued|indexing|indexed|failed|dead
|
|
129
|
+
GET /v1/pages/{id}/similar ?mediation=vector|cluster|entity&k=
|
|
130
|
+
GET /v1/pages/{id}/entities
|
|
131
|
+
POST /v1/search hybrid dense+sparse, RRF, rerank, filters
|
|
132
|
+
POST /v1/compare A/B embedding models (Jaccard@k, RBO, Kendall's τ)
|
|
133
|
+
GET /v1/clusters ?include_tombstoned=false
|
|
134
|
+
GET /v1/clusters/{id}
|
|
135
|
+
POST /v1/clusters/recompute
|
|
136
|
+
GET /v1/entities/{id_or_form}
|
|
137
|
+
POST /v1/forget purge + blacklist atomically
|
|
138
|
+
GET /v1/blacklist
|
|
139
|
+
DELETE /v1/blacklist/{id}
|
|
140
|
+
POST /v1/models register embedding model
|
|
141
|
+
POST /v1/models/{id}/backfill dry-run estimate, then confirm
|
|
142
|
+
POST /v1/models/{id}/activate
|
|
143
|
+
DELETE /v1/models/{id}
|
|
144
|
+
POST /v1/feedback { query_id, page_id, relevant }
|
|
145
|
+
GET /healthz /readyz /metrics
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Bearer token is always required, even on loopback. The server binds to
|
|
149
|
+
`127.0.0.1` by default.
|
|
150
|
+
|
|
151
|
+
## Optional extras
|
|
152
|
+
|
|
153
|
+
| Extra | Enables | Pulls in |
|
|
154
|
+
| ----------- | ---------------------------------------------- | ------------------- |
|
|
155
|
+
| `html` | `body_html` / fetched-HTML extraction (pulpie) | torch (~2 GB) |
|
|
156
|
+
| `gliner` | GLiNER zero-shot NER | gliner, onnxruntime |
|
|
157
|
+
| `ner` | spaCy NER model | en_core_web_sm |
|
|
158
|
+
| `leiden` | Leiden clustering | igraph, leidenalg |
|
|
159
|
+
|
|
160
|
+
Entity extraction is required at startup. The default extractor chain needs
|
|
161
|
+
the `ner` extra unless you configure a healthy gazetteer, GLiNER, or LLM
|
|
162
|
+
extractor.
|
|
163
|
+
|
|
164
|
+
See [Architecture](docs/architecture.md) for the ports/adapters map, the
|
|
165
|
+
ingest→index→cluster data flow, and the search pipeline. See
|
|
166
|
+
[Operations](docs/operations.md) for alpha reset commands, query-log
|
|
167
|
+
purging, job lease behavior, vector-store caveats, and accepted risks.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). TL;DR: `uv sync --all-groups --extra ner`,
|
|
172
|
+
then `uv run ruff format . && uv run ruff check . && uv run pytest && uv run ty check && uv run pyrefly check`.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "refindery"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A local, single-machine retrieval engine over the web pages you read."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Harold Martin", email = "harold.martin@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["retrieval", "search", "rag", "embeddings", "mcp", "browser-history"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Framework :: FastAPI",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Text Processing :: Indexing",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"aiosqlite>=0.22.1",
|
|
21
|
+
"catsu>=0.1.8",
|
|
22
|
+
"chonkie>=1.7.0",
|
|
23
|
+
"duckdb>=1.5.4",
|
|
24
|
+
"fastapi>=0.139.0",
|
|
25
|
+
"fastapi-mcp>=0.4.0",
|
|
26
|
+
"fastembed>=0.8.0",
|
|
27
|
+
"httpx>=0.28.1",
|
|
28
|
+
"huey>=3.1.1",
|
|
29
|
+
"inflect>=7.5.0",
|
|
30
|
+
"lancedb>=0.34.0",
|
|
31
|
+
"model2vec>=0.8.2",
|
|
32
|
+
"numpy>=2.2",
|
|
33
|
+
"opentelemetry-api>=1.43.0",
|
|
34
|
+
"opentelemetry-exporter-otlp-proto-http>=1.43.0",
|
|
35
|
+
"opentelemetry-instrumentation-fastapi>=0.64b0",
|
|
36
|
+
"opentelemetry-sdk>=1.43.0",
|
|
37
|
+
"prometheus-client>=0.25.0",
|
|
38
|
+
"pyarrow>=24.0.0",
|
|
39
|
+
"pydantic>=2.13.4",
|
|
40
|
+
"pydantic-settings>=2.14.2",
|
|
41
|
+
"pypdf>=6.14.2",
|
|
42
|
+
"qdrant-client>=1.18.0",
|
|
43
|
+
"rerankers>=0.10.0",
|
|
44
|
+
"scikit-learn>=1.9.0",
|
|
45
|
+
"scipy>=1.18.0",
|
|
46
|
+
"spacy>=3.8.3,<4",
|
|
47
|
+
"transformers>=5.13.0",
|
|
48
|
+
"umap-learn>=0.5.12",
|
|
49
|
+
"uuid6>=2025.0.1",
|
|
50
|
+
"uvicorn>=0.51.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
refindery = "refindery.cli:main"
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://github.com/hbmartin/refindery"
|
|
58
|
+
Repository = "https://github.com/hbmartin/refindery"
|
|
59
|
+
Changelog = "https://github.com/hbmartin/refindery/releases"
|
|
60
|
+
|
|
61
|
+
[project.optional-dependencies]
|
|
62
|
+
html = [
|
|
63
|
+
"torch>=2.13.0",
|
|
64
|
+
]
|
|
65
|
+
gliner = [
|
|
66
|
+
"gliner>=0.2.24",
|
|
67
|
+
"gliner-spacy>=0.0.11",
|
|
68
|
+
"onnxruntime>=1.27.0",
|
|
69
|
+
]
|
|
70
|
+
leiden = [
|
|
71
|
+
"igraph>=1.0.0",
|
|
72
|
+
"leidenalg>=0.12.0",
|
|
73
|
+
]
|
|
74
|
+
ner = [
|
|
75
|
+
"en-core-web-sm",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[dependency-groups]
|
|
79
|
+
dev = [
|
|
80
|
+
"deptry>=0.24.0",
|
|
81
|
+
"lizard>=1.23.0",
|
|
82
|
+
"pyrefly>=1.1.1",
|
|
83
|
+
"pyroma>=5.0",
|
|
84
|
+
"pytest>=9.1.1",
|
|
85
|
+
"pytest-asyncio>=1.0.0",
|
|
86
|
+
"pytest-cov>=7.1.0",
|
|
87
|
+
"pytest-httpx>=0.36.2",
|
|
88
|
+
"ruff>=0.15.20",
|
|
89
|
+
"testcontainers>=4.14.2",
|
|
90
|
+
"ty>=0.0.57",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.uv]
|
|
94
|
+
# numba is not imported directly; the floor keeps the resolver from walking
|
|
95
|
+
# back to ancient sdists instead of downgrading numpy (numba needs numpy<2.5).
|
|
96
|
+
constraint-dependencies = ["numba>=0.61"]
|
|
97
|
+
|
|
98
|
+
[tool.uv.sources]
|
|
99
|
+
en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" }
|
|
100
|
+
|
|
101
|
+
[tool.deptry.per_rule_ignores]
|
|
102
|
+
DEP002 = [
|
|
103
|
+
# Optional extras, imported lazily (deptry cannot see conditional imports):
|
|
104
|
+
"torch", "gliner", "gliner-spacy", "onnxruntime", "en-core-web-sm",
|
|
105
|
+
"igraph", "leidenalg",
|
|
106
|
+
# Lazy imports inside functions/adapters:
|
|
107
|
+
"scikit-learn", "umap-learn", "spacy", "model2vec",
|
|
108
|
+
"opentelemetry-exporter-otlp-proto-http",
|
|
109
|
+
"opentelemetry-instrumentation-fastapi",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[tool.pytest.ini_options]
|
|
113
|
+
testpaths = ["tests"]
|
|
114
|
+
asyncio_mode = "auto"
|
|
115
|
+
markers = [
|
|
116
|
+
"qdrant: requires a running Qdrant instance (QDRANT_URL or Docker)",
|
|
117
|
+
"slow: slow tests (model downloads, JIT warmup)",
|
|
118
|
+
"external: hits external APIs; requires credentials",
|
|
119
|
+
]
|
|
120
|
+
addopts = "-m 'not external'"
|
|
121
|
+
|
|
122
|
+
[tool.coverage.run]
|
|
123
|
+
source = ["src/refindery"]
|
|
124
|
+
|
|
125
|
+
[tool.coverage.report]
|
|
126
|
+
# Floor set just below measured coverage (85% local with qdrant tests
|
|
127
|
+
# skipped) to stop silent rot; applies to any full --cov run, local or CI.
|
|
128
|
+
fail_under = 84
|
|
129
|
+
|
|
130
|
+
[tool.pyrefly]
|
|
131
|
+
search-path = ["src", "."]
|
|
132
|
+
|
|
133
|
+
[tool.ruff]
|
|
134
|
+
# Exclude a variety of commonly ignored directories.
|
|
135
|
+
exclude = [".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "setup.py", "site-packages", "venv"]
|
|
136
|
+
|
|
137
|
+
line-length = 88
|
|
138
|
+
indent-width = 4
|
|
139
|
+
target-version = "py313"
|
|
140
|
+
|
|
141
|
+
[tool.ruff.lint]
|
|
142
|
+
select = ["ALL"]
|
|
143
|
+
ignore = ["COM812", "D105", "D107", "D203", "D212", "D213", "EM101", "PLR2004", "T201"]
|
|
144
|
+
|
|
145
|
+
[tool.ruff.lint.pylint]
|
|
146
|
+
max-args = 10
|
|
147
|
+
|
|
148
|
+
[tool.ruff.lint.per-file-ignores]
|
|
149
|
+
"tests/**/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ARG001", "ARG005", "D101", "D102", "D103", "D104", "DTZ003", "ERA001", "F403", "F405", "FBT002", "G004", "N802", "PLC0415", "PLR2004", "PLW1510", "PT011", "PT013", "PTH110", "PTH118", "PTH119", "PTH122", "PTH123", "PTH207", "S101", "S102", "S311", "S603", "S607", "SIM115"]
|
|
150
|
+
|
|
151
|
+
[build-system]
|
|
152
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
153
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters: concrete implementations of the application ports."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Canonical chunking adapter (chonkie)."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Sentence-aware chunking via chonkie with a hard token ceiling.
|
|
2
|
+
|
|
3
|
+
Chunking is canonical and model-independent: one chunking, every embedding
|
|
4
|
+
model embeds the same spans. The cl100k tokenizer is the canonical counter.
|
|
5
|
+
chonkie targets ``chunk_size`` but can exceed it on pathological input, so a
|
|
6
|
+
post-pass re-splits anything above the hard max (which model registration
|
|
7
|
+
guarantees is within every registered model's budget).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from chonkie import SentenceChunker, TokenChunker
|
|
11
|
+
|
|
12
|
+
from refindery.domain.ids import PageId, new_chunk_id
|
|
13
|
+
from refindery.domain.models import Chunk
|
|
14
|
+
|
|
15
|
+
_TOKENIZER = "cl100k_base"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChonkieChunker:
|
|
19
|
+
"""Chunker port implementation over chonkie's SentenceChunker."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
target_tokens: int = 448,
|
|
25
|
+
overlap_tokens: int = 64,
|
|
26
|
+
hard_max_tokens: int = 512,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._hard_max = hard_max_tokens
|
|
29
|
+
self._sentence = SentenceChunker(
|
|
30
|
+
tokenizer=_TOKENIZER,
|
|
31
|
+
chunk_size=target_tokens,
|
|
32
|
+
chunk_overlap=overlap_tokens,
|
|
33
|
+
)
|
|
34
|
+
self._splitter = TokenChunker(
|
|
35
|
+
tokenizer=_TOKENIZER,
|
|
36
|
+
chunk_size=hard_max_tokens,
|
|
37
|
+
chunk_overlap=0,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def chunk(self, *, page_id: PageId, text: str) -> list[Chunk]:
|
|
41
|
+
"""Split ``text`` into ordered chunks with char offsets and token counts."""
|
|
42
|
+
if not text.strip():
|
|
43
|
+
return []
|
|
44
|
+
spans: list[tuple[str, int, int, int]] = []
|
|
45
|
+
for piece in self._sentence.chunk(text):
|
|
46
|
+
if piece.token_count <= self._hard_max:
|
|
47
|
+
spans.append(
|
|
48
|
+
(piece.text, piece.start_index, piece.end_index, piece.token_count)
|
|
49
|
+
)
|
|
50
|
+
continue
|
|
51
|
+
spans.extend(
|
|
52
|
+
(
|
|
53
|
+
sub.text,
|
|
54
|
+
piece.start_index + sub.start_index,
|
|
55
|
+
piece.start_index + sub.end_index,
|
|
56
|
+
sub.token_count,
|
|
57
|
+
)
|
|
58
|
+
for sub in self._splitter.chunk(piece.text)
|
|
59
|
+
)
|
|
60
|
+
return [
|
|
61
|
+
Chunk(
|
|
62
|
+
id=new_chunk_id(),
|
|
63
|
+
page_id=page_id,
|
|
64
|
+
ordinal=ordinal,
|
|
65
|
+
text=chunk_text,
|
|
66
|
+
token_count=token_count,
|
|
67
|
+
char_start=char_start,
|
|
68
|
+
char_end=char_end,
|
|
69
|
+
)
|
|
70
|
+
for ordinal, (chunk_text, char_start, char_end, token_count) in enumerate(
|
|
71
|
+
spans
|
|
72
|
+
)
|
|
73
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Clustering engine adapters (UMAP/PCA + HDBSCAN/KMeans in a process pool)."""
|