jmfts 0.2.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.
- jmfts-0.2.0/LICENSE +21 -0
- jmfts-0.2.0/PKG-INFO +297 -0
- jmfts-0.2.0/README.md +235 -0
- jmfts-0.2.0/jmfts.egg-info/PKG-INFO +297 -0
- jmfts-0.2.0/jmfts.egg-info/SOURCES.txt +204 -0
- jmfts-0.2.0/jmfts.egg-info/dependency_links.txt +1 -0
- jmfts-0.2.0/jmfts.egg-info/entry_points.txt +5 -0
- jmfts-0.2.0/jmfts.egg-info/requires.txt +39 -0
- jmfts-0.2.0/jmfts.egg-info/top_level.txt +2 -0
- jmfts-0.2.0/jmfts_batch/README.md +161 -0
- jmfts-0.2.0/jmfts_batch/__init__.py +37 -0
- jmfts-0.2.0/jmfts_batch/cli.py +270 -0
- jmfts-0.2.0/jmfts_batch/provider.py +168 -0
- jmfts-0.2.0/jmfts_batch/providers/__init__.py +5 -0
- jmfts-0.2.0/jmfts_batch/providers/anthropic_api.py +200 -0
- jmfts-0.2.0/jmfts_batch/providers/mock.py +291 -0
- jmfts-0.2.0/jmfts_batch/providers/openai_api.py +215 -0
- jmfts-0.2.0/jmfts_batch/summarize.py +182 -0
- jmfts-0.2.0/jmfts_batch/worker.py +510 -0
- jmfts-0.2.0/jmfts_core/__init__.py +3 -0
- jmfts-0.2.0/jmfts_core/access.py +302 -0
- jmfts-0.2.0/jmfts_core/arxiv_fetch.py +117 -0
- jmfts-0.2.0/jmfts_core/chunking.py +363 -0
- jmfts-0.2.0/jmfts_core/citation_tasks.py +263 -0
- jmfts-0.2.0/jmfts_core/client.py +139 -0
- jmfts-0.2.0/jmfts_core/config.py +350 -0
- jmfts-0.2.0/jmfts_core/conversation_ingest.py +674 -0
- jmfts-0.2.0/jmfts_core/database.py +95 -0
- jmfts-0.2.0/jmfts_core/db_setup.py +139 -0
- jmfts-0.2.0/jmfts_core/embed_tasks.py +86 -0
- jmfts-0.2.0/jmfts_core/embedder.py +342 -0
- jmfts-0.2.0/jmfts_core/embedding.py +612 -0
- jmfts-0.2.0/jmfts_core/explain_wire.py +70 -0
- jmfts-0.2.0/jmfts_core/fact_extraction.py +560 -0
- jmfts-0.2.0/jmfts_core/graph_analysis.py +894 -0
- jmfts-0.2.0/jmfts_core/ingest_options.py +342 -0
- jmfts-0.2.0/jmfts_core/ingest_tasks.py +1232 -0
- jmfts-0.2.0/jmfts_core/ingest_worker.py +492 -0
- jmfts-0.2.0/jmfts_core/lint.py +328 -0
- jmfts-0.2.0/jmfts_core/llm_utils.py +22 -0
- jmfts-0.2.0/jmfts_core/models/__init__.py +34 -0
- jmfts-0.2.0/jmfts_core/models/document.py +216 -0
- jmfts-0.2.0/jmfts_core/models/document_blob.py +72 -0
- jmfts-0.2.0/jmfts_core/models/principal.py +76 -0
- jmfts-0.2.0/jmfts_core/models/search_context.py +47 -0
- jmfts-0.2.0/jmfts_core/models/search_index.py +101 -0
- jmfts-0.2.0/jmfts_core/models/task_queue.py +253 -0
- jmfts-0.2.0/jmfts_core/models/token_embedding.py +63 -0
- jmfts-0.2.0/jmfts_core/models/triple.py +161 -0
- jmfts-0.2.0/jmfts_core/models/usetype_presentation.py +46 -0
- jmfts-0.2.0/jmfts_core/office/__init__.py +104 -0
- jmfts-0.2.0/jmfts_core/office/extract.py +390 -0
- jmfts-0.2.0/jmfts_core/pdf_extraction.py +629 -0
- jmfts-0.2.0/jmfts_core/pipeline.py +1292 -0
- jmfts-0.2.0/jmfts_core/principal_context.py +56 -0
- jmfts-0.2.0/jmfts_core/probe.py +1209 -0
- jmfts-0.2.0/jmfts_core/query_router.py +236 -0
- jmfts-0.2.0/jmfts_core/registry.py +110 -0
- jmfts-0.2.0/jmfts_core/repositories/__init__.py +13 -0
- jmfts-0.2.0/jmfts_core/repositories/blob.py +269 -0
- jmfts-0.2.0/jmfts_core/repositories/document.py +1094 -0
- jmfts-0.2.0/jmfts_core/repositories/search.py +1375 -0
- jmfts-0.2.0/jmfts_core/repositories/search_context.py +77 -0
- jmfts-0.2.0/jmfts_core/repositories/task_queue.py +927 -0
- jmfts-0.2.0/jmfts_core/repositories/triple.py +367 -0
- jmfts-0.2.0/jmfts_core/repositories/usetype_presentation.py +124 -0
- jmfts-0.2.0/jmfts_core/repositories/view.py +271 -0
- jmfts-0.2.0/jmfts_core/reranker.py +161 -0
- jmfts-0.2.0/jmfts_core/rest/__init__.py +1 -0
- jmfts-0.2.0/jmfts_core/rest/auth.py +227 -0
- jmfts-0.2.0/jmfts_core/rest/main.py +406 -0
- jmfts-0.2.0/jmfts_core/rest/routers/__init__.py +1 -0
- jmfts-0.2.0/jmfts_core/rest/routers/conversations.py +12 -0
- jmfts-0.2.0/jmfts_core/rest/routers/documents.py +15 -0
- jmfts-0.2.0/jmfts_core/rest/routers/graph.py +13 -0
- jmfts-0.2.0/jmfts_core/rest/routers/indexes.py +14 -0
- jmfts-0.2.0/jmfts_core/rest/routers/runner.py +147 -0
- jmfts-0.2.0/jmfts_core/rest/routers/search.py +15 -0
- jmfts-0.2.0/jmfts_core/rest/routers/search_contexts.py +14 -0
- jmfts-0.2.0/jmfts_core/rest/routers/templates.py +18 -0
- jmfts-0.2.0/jmfts_core/rest/routers/triples.py +14 -0
- jmfts-0.2.0/jmfts_core/rest/routers/usetype_presentations.py +15 -0
- jmfts-0.2.0/jmfts_core/rest/routers/view.py +15 -0
- jmfts-0.2.0/jmfts_core/rest/schemas.py +305 -0
- jmfts-0.2.0/jmfts_core/rest/wiring.py +282 -0
- jmfts-0.2.0/jmfts_core/rollup_tasks.py +778 -0
- jmfts-0.2.0/jmfts_core/segmentation.py +144 -0
- jmfts-0.2.0/jmfts_core/services/__init__.py +36 -0
- jmfts-0.2.0/jmfts_core/services/access_service.py +263 -0
- jmfts-0.2.0/jmfts_core/services/conversation_service.py +163 -0
- jmfts-0.2.0/jmfts_core/services/document_service.py +1050 -0
- jmfts-0.2.0/jmfts_core/services/graph_service.py +407 -0
- jmfts-0.2.0/jmfts_core/services/index_service.py +219 -0
- jmfts-0.2.0/jmfts_core/services/ingest_service.py +810 -0
- jmfts-0.2.0/jmfts_core/services/search_context_service.py +131 -0
- jmfts-0.2.0/jmfts_core/services/search_service.py +663 -0
- jmfts-0.2.0/jmfts_core/services/template_service.py +320 -0
- jmfts-0.2.0/jmfts_core/services/triple_service.py +441 -0
- jmfts-0.2.0/jmfts_core/services/usetype_presentation_service.py +149 -0
- jmfts-0.2.0/jmfts_core/services/view_service.py +244 -0
- jmfts-0.2.0/jmfts_core/settling.py +421 -0
- jmfts-0.2.0/jmfts_core/sql/__init__.py +76 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/002_temporal_triples.sql +33 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/003_purge_ghost_predicates.sql +47 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/004_document_position.sql +34 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/005_document_event_time.sql +46 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/006_access_control.sql +65 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/007_term_postings_doc_index.sql +32 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/008_document_settled_lifecycle.sql +131 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/009_document_blobs.sql +85 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/010_task_queue.sql +146 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/011_task_queue_heartbeat.sql +58 -0
- jmfts-0.2.0/jmfts_core/sql/migrations/012_task_queue_batched.sql +78 -0
- jmfts-0.2.0/jmfts_core/sql/schema.sql +592 -0
- jmfts-0.2.0/jmfts_core/structural_splitting.py +306 -0
- jmfts-0.2.0/jmfts_core/structure_tasks.py +996 -0
- jmfts-0.2.0/jmfts_core/summarization.py +588 -0
- jmfts-0.2.0/jmfts_core/synthesis.py +97 -0
- jmfts-0.2.0/jmfts_core/task_errors.py +130 -0
- jmfts-0.2.0/jmfts_core/task_routing.py +131 -0
- jmfts-0.2.0/jmfts_core/token_selection.py +551 -0
- jmfts-0.2.0/jmfts_core/unit_of_work.py +95 -0
- jmfts-0.2.0/jmfts_core/url_fetch.py +116 -0
- jmfts-0.2.0/jmfts_core/view_renderer.py +154 -0
- jmfts-0.2.0/jmfts_core/worker.py +330 -0
- jmfts-0.2.0/pyproject.toml +302 -0
- jmfts-0.2.0/setup.cfg +4 -0
- jmfts-0.2.0/tests/test_access_control.py +251 -0
- jmfts-0.2.0/tests/test_access_edges.py +138 -0
- jmfts-0.2.0/tests/test_access_management.py +206 -0
- jmfts-0.2.0/tests/test_access_write_gate.py +183 -0
- jmfts-0.2.0/tests/test_api_auth.py +133 -0
- jmfts-0.2.0/tests/test_api_parity.py +302 -0
- jmfts-0.2.0/tests/test_batch_worker.py +736 -0
- jmfts-0.2.0/tests/test_bm25_index_lock.py +50 -0
- jmfts-0.2.0/tests/test_bulk_ingest_errors.py +348 -0
- jmfts-0.2.0/tests/test_chunking.py +425 -0
- jmfts-0.2.0/tests/test_citation_task.py +401 -0
- jmfts-0.2.0/tests/test_client_codegen.py +227 -0
- jmfts-0.2.0/tests/test_client_roundtrip.py +140 -0
- jmfts-0.2.0/tests/test_conversation_ingest.py +557 -0
- jmfts-0.2.0/tests/test_coreference.py +113 -0
- jmfts-0.2.0/tests/test_dedup_create.py +90 -0
- jmfts-0.2.0/tests/test_delete_link.py +87 -0
- jmfts-0.2.0/tests/test_doc_response_coverage.py +67 -0
- jmfts-0.2.0/tests/test_document_lifecycle.py +427 -0
- jmfts-0.2.0/tests/test_document_position.py +169 -0
- jmfts-0.2.0/tests/test_embed_task.py +335 -0
- jmfts-0.2.0/tests/test_ensonet_openai_compat.py +628 -0
- jmfts-0.2.0/tests/test_equal_weight_rrf.py +76 -0
- jmfts-0.2.0/tests/test_fact_extraction.py +485 -0
- jmfts-0.2.0/tests/test_file_upload.py +1260 -0
- jmfts-0.2.0/tests/test_graph_analysis.py +162 -0
- jmfts-0.2.0/tests/test_graph_neighbors.py +116 -0
- jmfts-0.2.0/tests/test_hybrid_rerank.py +149 -0
- jmfts-0.2.0/tests/test_importance_from_salience.py +64 -0
- jmfts-0.2.0/tests/test_ingest_analyze.py +551 -0
- jmfts-0.2.0/tests/test_ingest_attempts.py +491 -0
- jmfts-0.2.0/tests/test_ingest_explain.py +567 -0
- jmfts-0.2.0/tests/test_ingest_options.py +476 -0
- jmfts-0.2.0/tests/test_ingest_scheduling_races.py +509 -0
- jmfts-0.2.0/tests/test_ingest_text.py +421 -0
- jmfts-0.2.0/tests/test_ingest_worker.py +1320 -0
- jmfts-0.2.0/tests/test_known_defects.py +346 -0
- jmfts-0.2.0/tests/test_lint.py +53 -0
- jmfts-0.2.0/tests/test_local_client.py +141 -0
- jmfts-0.2.0/tests/test_maxsim_rerank_wiring.py +97 -0
- jmfts-0.2.0/tests/test_no_host_addresses.py +140 -0
- jmfts-0.2.0/tests/test_office_extraction.py +384 -0
- jmfts-0.2.0/tests/test_office_packaging.py +209 -0
- jmfts-0.2.0/tests/test_openapi_docs.py +193 -0
- jmfts-0.2.0/tests/test_packaging.py +266 -0
- jmfts-0.2.0/tests/test_pdf_extraction.py +649 -0
- jmfts-0.2.0/tests/test_pipeline.py +536 -0
- jmfts-0.2.0/tests/test_pipeline_idempotent.py +36 -0
- jmfts-0.2.0/tests/test_probe_office.py +884 -0
- jmfts-0.2.0/tests/test_query_router.py +97 -0
- jmfts-0.2.0/tests/test_raptor.py +686 -0
- jmfts-0.2.0/tests/test_readme_links.py +186 -0
- jmfts-0.2.0/tests/test_recall_as_of.py +118 -0
- jmfts-0.2.0/tests/test_regression_api.py +596 -0
- jmfts-0.2.0/tests/test_regression_ingestion.py +407 -0
- jmfts-0.2.0/tests/test_regression_knowledge_graph.py +500 -0
- jmfts-0.2.0/tests/test_regression_search.py +496 -0
- jmfts-0.2.0/tests/test_reparent_verb.py +97 -0
- jmfts-0.2.0/tests/test_reranker.py +480 -0
- jmfts-0.2.0/tests/test_rollup_pelt.py +564 -0
- jmfts-0.2.0/tests/test_runner_auth.py +184 -0
- jmfts-0.2.0/tests/test_runner_embed.py +277 -0
- jmfts-0.2.0/tests/test_search_contexts.py +167 -0
- jmfts-0.2.0/tests/test_search_regression.py +825 -0
- jmfts-0.2.0/tests/test_segment_ingest.py +224 -0
- jmfts-0.2.0/tests/test_segmentation.py +249 -0
- jmfts-0.2.0/tests/test_settling.py +590 -0
- jmfts-0.2.0/tests/test_structural_splitting.py +320 -0
- jmfts-0.2.0/tests/test_structure_tasks.py +391 -0
- jmfts-0.2.0/tests/test_supersede_race.py +110 -0
- jmfts-0.2.0/tests/test_synthesis.py +342 -0
- jmfts-0.2.0/tests/test_task_queue.py +1020 -0
- jmfts-0.2.0/tests/test_templates.py +311 -0
- jmfts-0.2.0/tests/test_thin_worker.py +558 -0
- jmfts-0.2.0/tests/test_thread_safety.py +123 -0
- jmfts-0.2.0/tests/test_unit_of_work.py +169 -0
- jmfts-0.2.0/tests/test_upsert_triple_verb.py +109 -0
- jmfts-0.2.0/tests/test_view_renderer.py +160 -0
- jmfts-0.2.0/tests/test_write_races.py +165 -0
jmfts-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fight Fire with Fire Robotics, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jmfts-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jmfts
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: John McCardle's Fusion Tree Search - A focused retrieval appliance with matryoshka embeddings and late interaction
|
|
5
|
+
Author: Fight Fire with Fire Robotics, LLC
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jmccardle/jmfts
|
|
8
|
+
Project-URL: Repository, https://github.com/jmccardle/jmfts
|
|
9
|
+
Project-URL: Issues, https://github.com/jmccardle/jmfts/issues
|
|
10
|
+
Keywords: retrieval,embeddings,colbert,bm25,pgvector,rag
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: jmfts-client==0.2.0
|
|
27
|
+
Requires-Dist: fastapi>=0.109.0
|
|
28
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
30
|
+
Requires-Dist: sqlalchemy>=2.0.25
|
|
31
|
+
Requires-Dist: psycopg2-binary>=2.9.9
|
|
32
|
+
Requires-Dist: pgvector>=0.2.4
|
|
33
|
+
Requires-Dist: pydantic>=2.5.0
|
|
34
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
35
|
+
Requires-Dist: transformers>=4.40
|
|
36
|
+
Requires-Dist: numpy>=1.26.0
|
|
37
|
+
Requires-Dist: httpx>=0.26.0
|
|
38
|
+
Requires-Dist: ruptures>=1.1.8
|
|
39
|
+
Requires-Dist: python-igraph>=0.11.0
|
|
40
|
+
Requires-Dist: leidenalg>=0.10.0
|
|
41
|
+
Requires-Dist: markdown-it-py>=3.0.0
|
|
42
|
+
Requires-Dist: pymupdf>=1.24
|
|
43
|
+
Requires-Dist: markdownify>=0.11
|
|
44
|
+
Requires-Dist: olefile>=0.47
|
|
45
|
+
Provides-Extra: embed
|
|
46
|
+
Requires-Dist: torch>=2.1.0; extra == "embed"
|
|
47
|
+
Requires-Dist: sentence-transformers>=2.2.2; extra == "embed"
|
|
48
|
+
Provides-Extra: office
|
|
49
|
+
Requires-Dist: python-docx>=1.1.0; extra == "office"
|
|
50
|
+
Requires-Dist: python-pptx>=0.6.23; extra == "office"
|
|
51
|
+
Requires-Dist: openpyxl>=3.1.0; extra == "office"
|
|
52
|
+
Provides-Extra: convert
|
|
53
|
+
Requires-Dist: unoserver>=3.0; extra == "convert"
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: jmfts[embed]; extra == "dev"
|
|
56
|
+
Requires-Dist: jmfts[office]; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
58
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
59
|
+
Requires-Dist: black>=24.1.0; extra == "dev"
|
|
60
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
61
|
+
Dynamic: license-file
|
|
62
|
+
|
|
63
|
+
# JMFTS
|
|
64
|
+
|
|
65
|
+
JMFTS (John McCardle's Fusion Tree Search) is a retrieval appliance: a
|
|
66
|
+
research-focused PostgreSQL + pgvector service combining matryoshka
|
|
67
|
+
embeddings, ColBERT-style late interaction (MaxSim), and BM25 into hybrid
|
|
68
|
+
search over a tree-structured document store.
|
|
69
|
+
|
|
70
|
+
Documents form a tree (`parent_id` + a materialized `path`), carry typed
|
|
71
|
+
cross-references to each other, and can hold temporal subject-predicate-object
|
|
72
|
+
triples with supersession instead of deletion. Five retrieval methods sit on
|
|
73
|
+
top: `vector`, `bm25`, `fulltext`, `maxsim`, and `hybrid` (a weighted/RRF
|
|
74
|
+
combination), plus an `auto` router. Ingestion pipelines (`markdown`,
|
|
75
|
+
`conversation`, `raw`, `transcript`, `wiki:url`, `wiki:arxiv`, `wiki:pdf`) are
|
|
76
|
+
idempotent on `(content_hash, parent_id)`.
|
|
77
|
+
|
|
78
|
+
Uploaded files are identified by their bytes, not their extension, and each
|
|
79
|
+
format is read into markdown before anything indexes it. Today that is `.pdf`,
|
|
80
|
+
plain text and markdown, HTML, and — with the `office` extra — `.docx` and
|
|
81
|
+
`.pptx`. An `.xlsx` is identified and described but not yet read. What a given
|
|
82
|
+
file would do is a question you can ask before uploading it: `POST /ingest/explain`
|
|
83
|
+
returns the plan, task by task, with the reason for every row that will not run.
|
|
84
|
+
|
|
85
|
+
## Status
|
|
86
|
+
|
|
87
|
+
Alpha. The retrieval and ingestion paths are exercised by a large test suite and
|
|
88
|
+
by benchmark runs; the interfaces are still moving.
|
|
89
|
+
|
|
90
|
+
**Measured, nDCG@10 on four standard BEIR datasets:**
|
|
91
|
+
|
|
92
|
+
| dataset | vector | bm25 | hybrid |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| SciFact | 0.6769 | 0.6595 | **0.7090** |
|
|
95
|
+
| NFCorpus | 0.3077 | 0.3043 | **0.3255** |
|
|
96
|
+
| FiQA | **0.3908** | 0.2363 | 0.3992 |
|
|
97
|
+
| TREC-COVID | **0.841** | 0.580 | 0.837 |
|
|
98
|
+
| *average* | 0.554 | 0.445 | **0.568** |
|
|
99
|
+
|
|
100
|
+
Hybrid wins three of the four; vector alone wins TREC-COVID, where our BM25
|
|
101
|
+
(0.580) sits below the canonical Anserini BEIR baseline of roughly 0.656.
|
|
102
|
+
Per-dataset weight tuning moves SciFact and NFCorpus by under a point. The full
|
|
103
|
+
breakdown, the caveats, and the comparisons against published ColBERTv2 and
|
|
104
|
+
SPLADE++ numbers are recorded in the development repository, not in this tree.
|
|
105
|
+
|
|
106
|
+
**One thing worth knowing before you rely on it.** `?rerank=true` is unmeasured.
|
|
107
|
+
The cross-encoder second stage loads a standard `CrossEncoder`
|
|
108
|
+
(`JMFTS_RERANKER_MODEL`, default `cross-encoder/ms-marco-MiniLM-L-6-v2`), and it
|
|
109
|
+
surfaces load and scoring errors rather than silently falling back to the
|
|
110
|
+
first-stage ranking. But that default was chosen for size and CPU viability, not
|
|
111
|
+
for measured retrieval quality — `vector→crossenc@100` has never been swept
|
|
112
|
+
against `vector→maxsim@200`. Treat reranked ordering as unvalidated. The
|
|
113
|
+
`maxsim` rerank method reads token embeddings you already stored and loads no
|
|
114
|
+
model at all.
|
|
115
|
+
|
|
116
|
+
## Install and run
|
|
117
|
+
|
|
118
|
+
You need PostgreSQL 14 or newer with the `pgvector` extension available. JMFTS
|
|
119
|
+
does not install or manage it.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install jmfts # base: no torch, cannot embed by itself
|
|
123
|
+
pip install 'jmfts[embed]' # + the model stack, for a single appliance
|
|
124
|
+
jmfts-init-db # create the database and load the schema
|
|
125
|
+
jmfts-server # serve on 0.0.0.0:8100
|
|
126
|
+
jmfts-server --port 9000 # ...or override one setting for this run
|
|
127
|
+
jmfts-server --help # what the JMFTS_* defaults currently resolve to
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For a working PostgreSQL and API in one command, the compose file brings up
|
|
131
|
+
`pgvector/pgvector:pg16` alongside the API:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker compose up
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
From a checkout, for development:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pip install -e ./jmfts-client # the client distribution; jmfts depends on it
|
|
141
|
+
pip install -e ".[dev]" # editable, with pytest/black/ruff; implies [embed]
|
|
142
|
+
uvicorn jmfts_core.rest.main:app --host 0.0.0.0 --port 8100 --reload
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This tree builds two distributions, and the first line is not optional. `jmfts` declares
|
|
146
|
+
`jmfts-client==0.2.0`, which is not on PyPI yet, so the second line alone fails to
|
|
147
|
+
resolve it.
|
|
148
|
+
|
|
149
|
+
### Reading and driving the API
|
|
150
|
+
|
|
151
|
+
`/docs` is Swagger UI over the live route table — 100 operations, grouped by tag, with the
|
|
152
|
+
request and response schemas. `/redoc` is the same document laid out for reading, and
|
|
153
|
+
`/openapi.json` is the document itself.
|
|
154
|
+
|
|
155
|
+
**A generated client already exists — do not write your own against `/openapi.json`.**
|
|
156
|
+
`pip install jmfts-client` gives you every one of those operations as a Python method, with
|
|
157
|
+
the request and response models the appliance itself validates against:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from jmfts_client import RemoteJmftsClient
|
|
161
|
+
from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
|
|
162
|
+
|
|
163
|
+
with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
|
|
164
|
+
jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
|
|
165
|
+
hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It carries `httpx` and `pydantic` and nothing else, so calling an appliance does not mean
|
|
169
|
+
installing one. Nobody writes those methods: a service method marked `@expose` becomes a
|
|
170
|
+
REST route, an entry in this OpenAPI document, a method on the in-process
|
|
171
|
+
`LocalJmftsClient`, and a method there — four views of one definition, with a test holding
|
|
172
|
+
each of them to it. See `jmfts-client/README.md`.
|
|
173
|
+
|
|
174
|
+
Press **Authorize** and paste `JMFTS_API_TOKEN` before trying an operation; the page names
|
|
175
|
+
which of the two credentials each one takes, since `/runner/*` uses `JMFTS_RUNNER_KEY`
|
|
176
|
+
instead and `GET /health` needs neither. All three pages answer without a token — a browser
|
|
177
|
+
navigating to a page cannot send an `Authorization` header, so gating them would close the
|
|
178
|
+
page rather than protect it. They expose the interface; reaching anything they describe
|
|
179
|
+
still costs a token.
|
|
180
|
+
|
|
181
|
+
Both pages load Swagger UI and ReDoc from `cdn.jsdelivr.net`, which is FastAPI's default. A
|
|
182
|
+
host with no route to the internet renders a blank page and must read `/openapi.json`
|
|
183
|
+
directly, or be given locally served copies of those assets.
|
|
184
|
+
|
|
185
|
+
### The office readers are an extra
|
|
186
|
+
|
|
187
|
+
**`pip install jmfts` reads PDF, text, markdown and HTML. `.docx` and `.pptx` need
|
|
188
|
+
`[office]`:**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install 'jmfts[office]' # python-docx, python-pptx, openpyxl
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The split is the same one the model stack draws, for the same reason. A base install
|
|
195
|
+
already *identifies* an office file and reports what it declares — how many slides, which
|
|
196
|
+
sheets, whether it carries macros or an unplaced member — because that runs on `zipfile`
|
|
197
|
+
alone and `probe` must always run. Opening one to get the text out is what needs the
|
|
198
|
+
readers. So a storage-side worker that never ingests office files is correctly installed
|
|
199
|
+
and correctly has no `python-docx`, and asking one to read a `.docx` raises
|
|
200
|
+
`OfficeStackNotInstalled`, which names the extra rather than reading as a broken
|
|
201
|
+
environment.
|
|
202
|
+
|
|
203
|
+
### The model is an extra
|
|
204
|
+
|
|
205
|
+
**`pip install jmfts` does not install torch.** Base JMFTS is storage, retrieval, the
|
|
206
|
+
tree, BM25, the queue and the whole ingest pipeline; the only step that needs an
|
|
207
|
+
accelerator is producing vectors, and that step can be somebody else's. Measured:
|
|
208
|
+
584 MB installed, against 5.2 GB with the model stack.
|
|
209
|
+
|
|
210
|
+
Add `[embed]` when *this* process should run the model — because it serves `/search`,
|
|
211
|
+
because it serves `/runner` for others, or because it is a single appliance doing both:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install 'jmfts[embed]' # CUDA build of torch
|
|
215
|
+
# or, for CPU — the wheel index is an install-time choice, so it is two steps:
|
|
216
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
217
|
+
pip install 'jmfts[embed]'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Without it, an install can still *measure* text — `check_fit`, the chunker, matryoshka
|
|
221
|
+
truncation are all tokenizer and numpy — and asks another JMFTS for the vectors:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
JMFTS_RUNNER_URL=http://the-gpu-box:8100 JMFTS_RUNNER_KEY=<shared secret> jmfts-worker
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
That is the intended shape for an ingest worker, and the reason the split exists: a fleet
|
|
228
|
+
is mostly storage-side workers, and none of them need several GB of CUDA to split text and
|
|
229
|
+
write rows. Asking for vectors with no model and no runner raises `ModelStackNotInstalled`,
|
|
230
|
+
which names both ways out — it never silently degrades. See `deploy/README.md`.
|
|
231
|
+
|
|
232
|
+
`./scripts/check_base_install.sh` proves the claim rather than asserting it: it builds an
|
|
233
|
+
empty virtualenv, installs base JMFTS into it, and checks there that the API and the
|
|
234
|
+
worker import, the tokenizer half works, asking for a vector raises, and no office reader
|
|
235
|
+
is reachable.
|
|
236
|
+
|
|
237
|
+
### Configuration
|
|
238
|
+
|
|
239
|
+
Configuration is environment-variable driven (`JMFTS_` prefix) — see
|
|
240
|
+
`.env.example` for the full list, grouped by concern (DB, embedding model,
|
|
241
|
+
token selection, BM25 tuning, auth/CORS, the worker and the runner).
|
|
242
|
+
|
|
243
|
+
Run the test suite against an isolated, throwaway database (never the real
|
|
244
|
+
appliance DB):
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
./scripts/run_tests_docker.sh # easiest: throwaway pgvector + CPU embed
|
|
248
|
+
pytest # native: needs `ALTER ROLE jmfts CREATEDB`
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Layout
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
jmfts_core/ the library: services, repositories, ORM models, office readers
|
|
255
|
+
jmfts_core/rest/ FastAPI surface — routers generated from the @expose registry
|
|
256
|
+
jmfts_core/sql/ schema.sql and the incremental migrations, shipped in the package
|
|
257
|
+
jmfts-client/ the second distribution: wire contracts + the generated HTTP client
|
|
258
|
+
jmfts_batch/ batch summarization over an OpenAI- or Anthropic-style batch API
|
|
259
|
+
scripts/ CLI clients over the REST API
|
|
260
|
+
deploy/ Kubernetes manifests for a worker fleet, and KEDA scaling on queue depth
|
|
261
|
+
plugin/ Claude Code plugin exposing JMFTS as an agent's durable memory
|
|
262
|
+
tests/ pytest suite against an ephemeral database, plus the fidelity corpus
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Where to go next
|
|
266
|
+
|
|
267
|
+
- **Running a worker fleet**: `deploy/README.md` — what routes where, badges, the
|
|
268
|
+
thin worker that does not hold the model, and the one way a badged fleet can
|
|
269
|
+
stall.
|
|
270
|
+
- **Contributing / architecture**: `CLAUDE.md` — the architecture diagram,
|
|
271
|
+
data flow, code style, and the search repository's internals (vector, BM25,
|
|
272
|
+
MaxSim, hybrid).
|
|
273
|
+
- **Using JMFTS as an agent's memory**: `plugin/jmfts/` — a Claude Code
|
|
274
|
+
plugin exposing JMFTS as durable cross-session memory via search/ingest/
|
|
275
|
+
read/explore/analyze skills. `plugin/jmfts/skills/jmfts/SKILL.md` is the
|
|
276
|
+
concept overview.
|
|
277
|
+
- **The batch worker**: `jmfts_batch/README.md` — consuming `summarize:llm`
|
|
278
|
+
through an external batch API.
|
|
279
|
+
- **Calling JMFTS from Python**: `jmfts-client/README.md` — the second
|
|
280
|
+
distribution, and what a generated verb is generated from.
|
|
281
|
+
|
|
282
|
+
## A note on documentation
|
|
283
|
+
|
|
284
|
+
The design documents this code was written against — most importantly the
|
|
285
|
+
ingest specification that dozens of source comments cite by section number —
|
|
286
|
+
are not in this release. They are being refined for publication separately.
|
|
287
|
+
Comments referring to INGEST_SPEC.md, OFFICE_SPEC.md, CORPUS.md,
|
|
288
|
+
KNOWN-DEFECTS.md and ROADMAP.md point at documents that will land later; the
|
|
289
|
+
code stands on its own in the meantime.
|
|
290
|
+
|
|
291
|
+
Those names are deliberately not written as links. There is nothing in this
|
|
292
|
+
tree for them to point at, and marking them up as paths would promise
|
|
293
|
+
otherwise.
|
|
294
|
+
|
|
295
|
+
## Licence
|
|
296
|
+
|
|
297
|
+
MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC. See `LICENSE`.
|
jmfts-0.2.0/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# JMFTS
|
|
2
|
+
|
|
3
|
+
JMFTS (John McCardle's Fusion Tree Search) is a retrieval appliance: a
|
|
4
|
+
research-focused PostgreSQL + pgvector service combining matryoshka
|
|
5
|
+
embeddings, ColBERT-style late interaction (MaxSim), and BM25 into hybrid
|
|
6
|
+
search over a tree-structured document store.
|
|
7
|
+
|
|
8
|
+
Documents form a tree (`parent_id` + a materialized `path`), carry typed
|
|
9
|
+
cross-references to each other, and can hold temporal subject-predicate-object
|
|
10
|
+
triples with supersession instead of deletion. Five retrieval methods sit on
|
|
11
|
+
top: `vector`, `bm25`, `fulltext`, `maxsim`, and `hybrid` (a weighted/RRF
|
|
12
|
+
combination), plus an `auto` router. Ingestion pipelines (`markdown`,
|
|
13
|
+
`conversation`, `raw`, `transcript`, `wiki:url`, `wiki:arxiv`, `wiki:pdf`) are
|
|
14
|
+
idempotent on `(content_hash, parent_id)`.
|
|
15
|
+
|
|
16
|
+
Uploaded files are identified by their bytes, not their extension, and each
|
|
17
|
+
format is read into markdown before anything indexes it. Today that is `.pdf`,
|
|
18
|
+
plain text and markdown, HTML, and — with the `office` extra — `.docx` and
|
|
19
|
+
`.pptx`. An `.xlsx` is identified and described but not yet read. What a given
|
|
20
|
+
file would do is a question you can ask before uploading it: `POST /ingest/explain`
|
|
21
|
+
returns the plan, task by task, with the reason for every row that will not run.
|
|
22
|
+
|
|
23
|
+
## Status
|
|
24
|
+
|
|
25
|
+
Alpha. The retrieval and ingestion paths are exercised by a large test suite and
|
|
26
|
+
by benchmark runs; the interfaces are still moving.
|
|
27
|
+
|
|
28
|
+
**Measured, nDCG@10 on four standard BEIR datasets:**
|
|
29
|
+
|
|
30
|
+
| dataset | vector | bm25 | hybrid |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| SciFact | 0.6769 | 0.6595 | **0.7090** |
|
|
33
|
+
| NFCorpus | 0.3077 | 0.3043 | **0.3255** |
|
|
34
|
+
| FiQA | **0.3908** | 0.2363 | 0.3992 |
|
|
35
|
+
| TREC-COVID | **0.841** | 0.580 | 0.837 |
|
|
36
|
+
| *average* | 0.554 | 0.445 | **0.568** |
|
|
37
|
+
|
|
38
|
+
Hybrid wins three of the four; vector alone wins TREC-COVID, where our BM25
|
|
39
|
+
(0.580) sits below the canonical Anserini BEIR baseline of roughly 0.656.
|
|
40
|
+
Per-dataset weight tuning moves SciFact and NFCorpus by under a point. The full
|
|
41
|
+
breakdown, the caveats, and the comparisons against published ColBERTv2 and
|
|
42
|
+
SPLADE++ numbers are recorded in the development repository, not in this tree.
|
|
43
|
+
|
|
44
|
+
**One thing worth knowing before you rely on it.** `?rerank=true` is unmeasured.
|
|
45
|
+
The cross-encoder second stage loads a standard `CrossEncoder`
|
|
46
|
+
(`JMFTS_RERANKER_MODEL`, default `cross-encoder/ms-marco-MiniLM-L-6-v2`), and it
|
|
47
|
+
surfaces load and scoring errors rather than silently falling back to the
|
|
48
|
+
first-stage ranking. But that default was chosen for size and CPU viability, not
|
|
49
|
+
for measured retrieval quality — `vector→crossenc@100` has never been swept
|
|
50
|
+
against `vector→maxsim@200`. Treat reranked ordering as unvalidated. The
|
|
51
|
+
`maxsim` rerank method reads token embeddings you already stored and loads no
|
|
52
|
+
model at all.
|
|
53
|
+
|
|
54
|
+
## Install and run
|
|
55
|
+
|
|
56
|
+
You need PostgreSQL 14 or newer with the `pgvector` extension available. JMFTS
|
|
57
|
+
does not install or manage it.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install jmfts # base: no torch, cannot embed by itself
|
|
61
|
+
pip install 'jmfts[embed]' # + the model stack, for a single appliance
|
|
62
|
+
jmfts-init-db # create the database and load the schema
|
|
63
|
+
jmfts-server # serve on 0.0.0.0:8100
|
|
64
|
+
jmfts-server --port 9000 # ...or override one setting for this run
|
|
65
|
+
jmfts-server --help # what the JMFTS_* defaults currently resolve to
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For a working PostgreSQL and API in one command, the compose file brings up
|
|
69
|
+
`pgvector/pgvector:pg16` alongside the API:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
docker compose up
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
From a checkout, for development:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ./jmfts-client # the client distribution; jmfts depends on it
|
|
79
|
+
pip install -e ".[dev]" # editable, with pytest/black/ruff; implies [embed]
|
|
80
|
+
uvicorn jmfts_core.rest.main:app --host 0.0.0.0 --port 8100 --reload
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This tree builds two distributions, and the first line is not optional. `jmfts` declares
|
|
84
|
+
`jmfts-client==0.2.0`, which is not on PyPI yet, so the second line alone fails to
|
|
85
|
+
resolve it.
|
|
86
|
+
|
|
87
|
+
### Reading and driving the API
|
|
88
|
+
|
|
89
|
+
`/docs` is Swagger UI over the live route table — 100 operations, grouped by tag, with the
|
|
90
|
+
request and response schemas. `/redoc` is the same document laid out for reading, and
|
|
91
|
+
`/openapi.json` is the document itself.
|
|
92
|
+
|
|
93
|
+
**A generated client already exists — do not write your own against `/openapi.json`.**
|
|
94
|
+
`pip install jmfts-client` gives you every one of those operations as a Python method, with
|
|
95
|
+
the request and response models the appliance itself validates against:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from jmfts_client import RemoteJmftsClient
|
|
99
|
+
from jmfts_client.contracts import DocumentCreate, HybridSearchRequest
|
|
100
|
+
|
|
101
|
+
with RemoteJmftsClient("http://localhost:8100", token="...") as jmfts:
|
|
102
|
+
jmfts.create_document(DocumentCreate(title="Ada", content="Ada Lovelace"))
|
|
103
|
+
hits = jmfts.hybrid_search(HybridSearchRequest(query="Ada", limit=10))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It carries `httpx` and `pydantic` and nothing else, so calling an appliance does not mean
|
|
107
|
+
installing one. Nobody writes those methods: a service method marked `@expose` becomes a
|
|
108
|
+
REST route, an entry in this OpenAPI document, a method on the in-process
|
|
109
|
+
`LocalJmftsClient`, and a method there — four views of one definition, with a test holding
|
|
110
|
+
each of them to it. See `jmfts-client/README.md`.
|
|
111
|
+
|
|
112
|
+
Press **Authorize** and paste `JMFTS_API_TOKEN` before trying an operation; the page names
|
|
113
|
+
which of the two credentials each one takes, since `/runner/*` uses `JMFTS_RUNNER_KEY`
|
|
114
|
+
instead and `GET /health` needs neither. All three pages answer without a token — a browser
|
|
115
|
+
navigating to a page cannot send an `Authorization` header, so gating them would close the
|
|
116
|
+
page rather than protect it. They expose the interface; reaching anything they describe
|
|
117
|
+
still costs a token.
|
|
118
|
+
|
|
119
|
+
Both pages load Swagger UI and ReDoc from `cdn.jsdelivr.net`, which is FastAPI's default. A
|
|
120
|
+
host with no route to the internet renders a blank page and must read `/openapi.json`
|
|
121
|
+
directly, or be given locally served copies of those assets.
|
|
122
|
+
|
|
123
|
+
### The office readers are an extra
|
|
124
|
+
|
|
125
|
+
**`pip install jmfts` reads PDF, text, markdown and HTML. `.docx` and `.pptx` need
|
|
126
|
+
`[office]`:**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install 'jmfts[office]' # python-docx, python-pptx, openpyxl
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The split is the same one the model stack draws, for the same reason. A base install
|
|
133
|
+
already *identifies* an office file and reports what it declares — how many slides, which
|
|
134
|
+
sheets, whether it carries macros or an unplaced member — because that runs on `zipfile`
|
|
135
|
+
alone and `probe` must always run. Opening one to get the text out is what needs the
|
|
136
|
+
readers. So a storage-side worker that never ingests office files is correctly installed
|
|
137
|
+
and correctly has no `python-docx`, and asking one to read a `.docx` raises
|
|
138
|
+
`OfficeStackNotInstalled`, which names the extra rather than reading as a broken
|
|
139
|
+
environment.
|
|
140
|
+
|
|
141
|
+
### The model is an extra
|
|
142
|
+
|
|
143
|
+
**`pip install jmfts` does not install torch.** Base JMFTS is storage, retrieval, the
|
|
144
|
+
tree, BM25, the queue and the whole ingest pipeline; the only step that needs an
|
|
145
|
+
accelerator is producing vectors, and that step can be somebody else's. Measured:
|
|
146
|
+
584 MB installed, against 5.2 GB with the model stack.
|
|
147
|
+
|
|
148
|
+
Add `[embed]` when *this* process should run the model — because it serves `/search`,
|
|
149
|
+
because it serves `/runner` for others, or because it is a single appliance doing both:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install 'jmfts[embed]' # CUDA build of torch
|
|
153
|
+
# or, for CPU — the wheel index is an install-time choice, so it is two steps:
|
|
154
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
155
|
+
pip install 'jmfts[embed]'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Without it, an install can still *measure* text — `check_fit`, the chunker, matryoshka
|
|
159
|
+
truncation are all tokenizer and numpy — and asks another JMFTS for the vectors:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
JMFTS_RUNNER_URL=http://the-gpu-box:8100 JMFTS_RUNNER_KEY=<shared secret> jmfts-worker
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
That is the intended shape for an ingest worker, and the reason the split exists: a fleet
|
|
166
|
+
is mostly storage-side workers, and none of them need several GB of CUDA to split text and
|
|
167
|
+
write rows. Asking for vectors with no model and no runner raises `ModelStackNotInstalled`,
|
|
168
|
+
which names both ways out — it never silently degrades. See `deploy/README.md`.
|
|
169
|
+
|
|
170
|
+
`./scripts/check_base_install.sh` proves the claim rather than asserting it: it builds an
|
|
171
|
+
empty virtualenv, installs base JMFTS into it, and checks there that the API and the
|
|
172
|
+
worker import, the tokenizer half works, asking for a vector raises, and no office reader
|
|
173
|
+
is reachable.
|
|
174
|
+
|
|
175
|
+
### Configuration
|
|
176
|
+
|
|
177
|
+
Configuration is environment-variable driven (`JMFTS_` prefix) — see
|
|
178
|
+
`.env.example` for the full list, grouped by concern (DB, embedding model,
|
|
179
|
+
token selection, BM25 tuning, auth/CORS, the worker and the runner).
|
|
180
|
+
|
|
181
|
+
Run the test suite against an isolated, throwaway database (never the real
|
|
182
|
+
appliance DB):
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
./scripts/run_tests_docker.sh # easiest: throwaway pgvector + CPU embed
|
|
186
|
+
pytest # native: needs `ALTER ROLE jmfts CREATEDB`
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Layout
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
jmfts_core/ the library: services, repositories, ORM models, office readers
|
|
193
|
+
jmfts_core/rest/ FastAPI surface — routers generated from the @expose registry
|
|
194
|
+
jmfts_core/sql/ schema.sql and the incremental migrations, shipped in the package
|
|
195
|
+
jmfts-client/ the second distribution: wire contracts + the generated HTTP client
|
|
196
|
+
jmfts_batch/ batch summarization over an OpenAI- or Anthropic-style batch API
|
|
197
|
+
scripts/ CLI clients over the REST API
|
|
198
|
+
deploy/ Kubernetes manifests for a worker fleet, and KEDA scaling on queue depth
|
|
199
|
+
plugin/ Claude Code plugin exposing JMFTS as an agent's durable memory
|
|
200
|
+
tests/ pytest suite against an ephemeral database, plus the fidelity corpus
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Where to go next
|
|
204
|
+
|
|
205
|
+
- **Running a worker fleet**: `deploy/README.md` — what routes where, badges, the
|
|
206
|
+
thin worker that does not hold the model, and the one way a badged fleet can
|
|
207
|
+
stall.
|
|
208
|
+
- **Contributing / architecture**: `CLAUDE.md` — the architecture diagram,
|
|
209
|
+
data flow, code style, and the search repository's internals (vector, BM25,
|
|
210
|
+
MaxSim, hybrid).
|
|
211
|
+
- **Using JMFTS as an agent's memory**: `plugin/jmfts/` — a Claude Code
|
|
212
|
+
plugin exposing JMFTS as durable cross-session memory via search/ingest/
|
|
213
|
+
read/explore/analyze skills. `plugin/jmfts/skills/jmfts/SKILL.md` is the
|
|
214
|
+
concept overview.
|
|
215
|
+
- **The batch worker**: `jmfts_batch/README.md` — consuming `summarize:llm`
|
|
216
|
+
through an external batch API.
|
|
217
|
+
- **Calling JMFTS from Python**: `jmfts-client/README.md` — the second
|
|
218
|
+
distribution, and what a generated verb is generated from.
|
|
219
|
+
|
|
220
|
+
## A note on documentation
|
|
221
|
+
|
|
222
|
+
The design documents this code was written against — most importantly the
|
|
223
|
+
ingest specification that dozens of source comments cite by section number —
|
|
224
|
+
are not in this release. They are being refined for publication separately.
|
|
225
|
+
Comments referring to INGEST_SPEC.md, OFFICE_SPEC.md, CORPUS.md,
|
|
226
|
+
KNOWN-DEFECTS.md and ROADMAP.md point at documents that will land later; the
|
|
227
|
+
code stands on its own in the meantime.
|
|
228
|
+
|
|
229
|
+
Those names are deliberately not written as links. There is nothing in this
|
|
230
|
+
tree for them to point at, and marking them up as paths would promise
|
|
231
|
+
otherwise.
|
|
232
|
+
|
|
233
|
+
## Licence
|
|
234
|
+
|
|
235
|
+
MIT. Copyright (c) 2026 Fight Fire with Fire Robotics, LLC. See `LICENSE`.
|