cbrkit 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {cbrkit-1.3.0 → cbrkit-1.4.0}/PKG-INFO +69 -42
  2. {cbrkit-1.3.0 → cbrkit-1.4.0}/README.md +57 -37
  3. {cbrkit-1.3.0 → cbrkit-1.4.0}/pyproject.toml +61 -45
  4. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/eval/common.py +2 -2
  5. cbrkit-1.4.0/src/cbrkit/filter.py +81 -0
  6. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/helpers.py +172 -155
  7. cbrkit-1.4.0/src/cbrkit/indexable/__init__.py +54 -0
  8. cbrkit-1.4.0/src/cbrkit/indexable/_common.py +262 -0
  9. cbrkit-1.4.0/src/cbrkit/indexable/chromadb.py +271 -0
  10. cbrkit-1.4.0/src/cbrkit/indexable/lancedb.py +290 -0
  11. cbrkit-1.4.0/src/cbrkit/indexable/pgvector.py +345 -0
  12. cbrkit-1.4.0/src/cbrkit/indexable/sqlalchemy.py +733 -0
  13. cbrkit-1.4.0/src/cbrkit/indexable/sqlite_vec.py +403 -0
  14. cbrkit-1.4.0/src/cbrkit/indexable/zvec.py +353 -0
  15. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/loaders.py +34 -7
  16. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retain/build.py +2 -2
  17. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retrieval/__init__.py +16 -0
  18. cbrkit-1.4.0/src/cbrkit/retrieval/apply.py +293 -0
  19. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/__init__.py +34 -0
  20. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/_common.py +472 -0
  21. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/bm25.py +178 -0
  22. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/chromadb.py +161 -0
  23. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/embed.py +247 -0
  24. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/lancedb.py +162 -0
  25. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/pgvector.py +248 -0
  26. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/sqlite_vec.py +294 -0
  27. cbrkit-1.4.0/src/cbrkit/retrieval/indexable/zvec.py +205 -0
  28. cbrkit-1.4.0/src/cbrkit/retrieval/rerank/__init__.py +18 -0
  29. cbrkit-1.4.0/src/cbrkit/retrieval/rerank/_common.py +53 -0
  30. cbrkit-1.4.0/src/cbrkit/retrieval/rerank/cohere.py +41 -0
  31. cbrkit-1.4.0/src/cbrkit/retrieval/rerank/sentence_transformers.py +101 -0
  32. cbrkit-1.4.0/src/cbrkit/retrieval/rerank/voyageai.py +38 -0
  33. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retrieval/wrappers.py +2 -2
  34. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/reuse/build.py +3 -1
  35. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/revise/build.py +4 -2
  36. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/collections.py +3 -3
  37. cbrkit-1.4.0/src/cbrkit/sim/embed/__init__.py +66 -0
  38. cbrkit-1.4.0/src/cbrkit/sim/embed/core.py +327 -0
  39. cbrkit-1.4.0/src/cbrkit/sim/embed/metrics.py +158 -0
  40. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/__init__.py +43 -0
  41. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/bm25.py +181 -0
  42. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/cohere.py +45 -0
  43. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/ollama.py +39 -0
  44. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/openai.py +65 -0
  45. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/pydantic_ai.py +31 -0
  46. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/sentence_transformers.py +80 -0
  47. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/spacy.py +124 -0
  48. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/sparse_encoder.py +93 -0
  49. cbrkit-1.4.0/src/cbrkit/sim/embed/providers/voyageai.py +38 -0
  50. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/alignment.py +1 -1
  51. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/apply.py +2 -1
  52. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/system.py +1 -1
  53. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/typing.py +104 -2
  54. cbrkit-1.3.0/src/cbrkit/indexable.py +0 -928
  55. cbrkit-1.3.0/src/cbrkit/retrieval/apply.py +0 -164
  56. cbrkit-1.3.0/src/cbrkit/retrieval/indexable.py +0 -1189
  57. cbrkit-1.3.0/src/cbrkit/retrieval/rerank.py +0 -219
  58. cbrkit-1.3.0/src/cbrkit/sim/embed.py +0 -1088
  59. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/__init__.py +0 -0
  60. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/__main__.py +0 -0
  61. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/adapt/__init__.py +0 -0
  62. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/adapt/attribute_value.py +0 -0
  63. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/adapt/generic.py +0 -0
  64. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/adapt/numbers.py +0 -0
  65. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/adapt/strings.py +0 -0
  66. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/api.py +0 -0
  67. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/cli.py +0 -0
  68. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/constants.py +0 -0
  69. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/cycle.py +0 -0
  70. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/dumpers.py +0 -0
  71. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/eval/__init__.py +0 -0
  72. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/eval/retrieval.py +0 -0
  73. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/model/__init__.py +0 -0
  74. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/model/graph.py +0 -0
  75. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/model/result.py +0 -0
  76. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/py.typed +0 -0
  77. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retain/__init__.py +0 -0
  78. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retain/apply.py +0 -0
  79. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retain/storage.py +0 -0
  80. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/retrieval/build.py +0 -0
  81. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/reuse/__init__.py +0 -0
  82. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/reuse/apply.py +0 -0
  83. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/revise/__init__.py +0 -0
  84. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/revise/apply.py +0 -0
  85. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/__init__.py +0 -0
  86. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/aggregator.py +0 -0
  87. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/attribute_value.py +0 -0
  88. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/generic.py +0 -0
  89. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/__init__.py +0 -0
  90. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/astar.py +0 -0
  91. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/brute_force.py +0 -0
  92. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/common.py +0 -0
  93. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/dfs.py +0 -0
  94. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/greedy.py +0 -0
  95. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/lap.py +0 -0
  96. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/qap.py +0 -0
  97. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/graphs/vf2.py +0 -0
  98. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/numbers.py +0 -0
  99. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/pooling.py +0 -0
  100. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/strings.py +0 -0
  101. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/taxonomy.py +0 -0
  102. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/sim/wrappers.py +0 -0
  103. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/__init__.py +0 -0
  104. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/build.py +0 -0
  105. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/model.py +0 -0
  106. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/prompts.py +0 -0
  107. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/__init__.py +0 -0
  108. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/anthropic.py +0 -0
  109. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/cohere.py +0 -0
  110. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/google.py +0 -0
  111. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/instructor.py +0 -0
  112. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/model.py +0 -0
  113. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/ollama.py +0 -0
  114. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/openai_agents.py +0 -0
  115. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/openai_completions.py +0 -0
  116. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/openai_responses.py +0 -0
  117. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/pydantic_ai.py +0 -0
  118. {cbrkit-1.3.0 → cbrkit-1.4.0}/src/cbrkit/synthesis/providers/wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbrkit
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Customizable Case-Based Reasoning (CBR) toolkit for Python with a built-in API and CLI
5
5
  Keywords: cbr,case-based reasoning,api,similarity,nlp,retrieval,cli,tool,library
6
6
  Author: Mirko Lenz
@@ -30,7 +30,7 @@ Requires-Dist: pyyaml>=6,<7
30
30
  Requires-Dist: rtoml>=0.12,<1
31
31
  Requires-Dist: scipy>=1,<2
32
32
  Requires-Dist: xmltodict>=1,<2
33
- Requires-Dist: cbrkit[anthropic,api,bm25,chromadb,chunking,cohere,eval,google,graphs,graphviz,instructor,lancedb,levenshtein,nltk,ollama,openai,openai-agents,pandas,pydantic-ai,spacy,sql,timeseries,transformers,voyageai,zvec] ; extra == 'all'
33
+ Requires-Dist: cbrkit[anthropic,api,bm25,chromadb,chunking,cohere,eval,google,graphs,graphviz,instructor,lancedb,levenshtein,nltk,ollama,openai,openai-agents,pandas,pgvector,pydantic-ai,spacy,sql,sqlite-vec,timeseries,transformers,voyageai,zvec] ; extra == 'all'
34
34
  Requires-Dist: anthropic>=0.40,<1 ; extra == 'anthropic'
35
35
  Requires-Dist: cbrkit[cli] ; extra == 'api'
36
36
  Requires-Dist: fastapi>=0.100,<1 ; extra == 'api'
@@ -43,9 +43,9 @@ Requires-Dist: chromadb>=1,<2 ; extra == 'chromadb'
43
43
  Requires-Dist: chonkie>=1,<2 ; extra == 'chunking'
44
44
  Requires-Dist: rich>=14,<16 ; extra == 'cli'
45
45
  Requires-Dist: typer>=0.20,<1 ; extra == 'cli'
46
- Requires-Dist: cohere>=6,<7 ; extra == 'cohere'
46
+ Requires-Dist: cohere>=7,<8 ; extra == 'cohere'
47
47
  Requires-Dist: ranx>=0.3,<1 ; extra == 'eval'
48
- Requires-Dist: google-genai>=1,<2 ; extra == 'google'
48
+ Requires-Dist: google-genai>=2,<3 ; extra == 'google'
49
49
  Requires-Dist: networkx>=3,<4 ; extra == 'graphs'
50
50
  Requires-Dist: rustworkx>=0.15,<1 ; extra == 'graphs'
51
51
  Requires-Dist: pygraphviz>=1,<2 ; extra == 'graphviz'
@@ -58,9 +58,14 @@ Requires-Dist: openai>=1,<3 ; extra == 'openai'
58
58
  Requires-Dist: tiktoken>=0.8,<1 ; extra == 'openai'
59
59
  Requires-Dist: openai-agents>=0.2,<1 ; extra == 'openai-agents'
60
60
  Requires-Dist: pandas>=2,<4 ; extra == 'pandas'
61
+ Requires-Dist: pgvector>=0.4,<1 ; extra == 'pgvector'
62
+ Requires-Dist: cbrkit[sql] ; extra == 'pgvector'
61
63
  Requires-Dist: pydantic-ai-slim>=1,<2 ; extra == 'pydantic-ai'
62
64
  Requires-Dist: spacy>=3.8,<4 ; extra == 'spacy'
63
- Requires-Dist: sqlalchemy>=2,<3 ; extra == 'sql'
65
+ Requires-Dist: sqlalchemy[asyncio]>=2,<3 ; extra == 'sql'
66
+ Requires-Dist: sqlite-vec>=0.1,<1 ; extra == 'sqlite-vec'
67
+ Requires-Dist: aiosqlite>=0.20,<1 ; extra == 'sqlite-vec'
68
+ Requires-Dist: cbrkit[sql] ; extra == 'sqlite-vec'
64
69
  Requires-Dist: minineedle>=3,<4 ; extra == 'timeseries'
65
70
  Requires-Dist: sentence-transformers>=4,<6 ; extra == 'transformers'
66
71
  Requires-Dist: torch>=2.5,<3 ; extra == 'transformers'
@@ -92,9 +97,11 @@ Provides-Extra: ollama
92
97
  Provides-Extra: openai
93
98
  Provides-Extra: openai-agents
94
99
  Provides-Extra: pandas
100
+ Provides-Extra: pgvector
95
101
  Provides-Extra: pydantic-ai
96
102
  Provides-Extra: spacy
97
103
  Provides-Extra: sql
104
+ Provides-Extra: sqlite-vec
98
105
  Provides-Extra: timeseries
99
106
  Provides-Extra: transformers
100
107
  Provides-Extra: voyageai
@@ -231,12 +238,14 @@ df = pl.read_csv("path/to/cases.csv")
231
238
  casebase = cbrkit.loaders.polars(df)
232
239
  ```
233
240
 
234
- For database access, CBRkit provides `sqlite` and `sqlalchemy` loaders (the latter requires the `sql` extra):
241
+ For ad-hoc SQLite loading, CBRkit ships a stdlib-based loader:
235
242
 
236
243
  ```python
237
244
  casebase = cbrkit.loaders.sqlite("path/to/database.db", "SELECT * FROM cases")
238
245
  ```
239
246
 
247
+ For richer relational backends (filters, upserts, vector/FTS search via pgvector on PostgreSQL or sqlite-vec on SQLite), see `cbrkit.indexable.sqlalchemy`, `cbrkit.indexable.pgvector`, and `cbrkit.indexable.sqlite_vec`.
248
+
240
249
  **Tip:** You can validate a loaded casebase against a Pydantic model using `cbrkit.loaders.validate()`:
241
250
 
242
251
  ```python
@@ -682,8 +691,7 @@ The result contains `similarities` with quality assessment scores for each case.
682
691
  ## Retain
683
692
 
684
693
  The retain phase decides whether and how to integrate new cases into the casebase.
685
- The `cbrkit.retain` module provides utility functions for this purpose.
686
- You build a retain pipeline by specifying an assessment function and a storage function:
694
+ Build a retain pipeline from an assessment function and a storage function:
687
695
 
688
696
  ```python
689
697
  retainer = cbrkit.retain.build(
@@ -695,27 +703,9 @@ retainer = cbrkit.retain.build(
695
703
  )
696
704
  ```
697
705
 
698
- CBRkit provides several built-in storage functions:
699
-
700
- - `static`: Generates keys from a fixed reference casebase to avoid collisions.
701
- - `indexable`: Keeps an `IndexableFunc`'s index in sync with the casebase.
702
-
703
- You can filter retained cases based on their assessment scores using the `dropout` wrapper:
704
-
705
- ```python
706
- retainer = cbrkit.retain.dropout(
707
- retainer_func=cbrkit.retain.build(...),
708
- min_similarity=0.5,
709
- )
710
- ```
711
-
712
- The retainer can be applied to a revise result:
713
-
714
- ```python
715
- result = cbrkit.retain.apply_result(revise_result, retainer)
716
- ```
717
-
718
- The result contains `similarities` with fitness scores and `casebase` with the updated cases.
706
+ The built-in storage functions are `static` (generates collision-free keys from a reference casebase) and `indexable` (keeps an `IndexableFunc`'s index in sync with the casebase).
707
+ Wrap a retainer with `dropout` to filter by assessment score (e.g. `min_similarity=0.5`), then apply it to a revise result via `cbrkit.retain.apply_result(revise_result, retainer)`.
708
+ The result exposes `similarities` (fitness scores) and `casebase` (updated cases).
719
709
 
720
710
  ## Full CBR Cycle
721
711
 
@@ -848,37 +838,74 @@ result = cbrkit.retrieval.apply_query(casebase, query, (retriever, reranker))
848
838
 
849
839
  ### Indexed Retrieval
850
840
 
851
- Retrievers like `bm25`, `embed`, `lancedb`, `chromadb`, and `zvec` support **indexed retrieval**, where the casebase is pre-indexed once and then queried without passing the full casebase each time.
852
- This is useful for large casebases or when using external search backends.
841
+ Indexed retrieval pre-indexes the casebase once and then queries it without passing the full casebase each time, which helps for large casebases or external search backends.
842
+ Index maintenance lives on whichever object owns the index.
853
843
 
854
- To use indexed retrieval, first create a retriever and call its `put_index()` method:
844
+ The self-contained `bm25` and `embed` retrievers own their index, so you call `put_index()` on the retriever:
855
845
 
856
846
  ```python
857
847
  from frozendict import frozendict
858
848
 
859
- bm25_func = cbrkit.sim.embed.bm25(language="en")
860
- retriever = cbrkit.retrieval.bm25(conversion_func=bm25_func)
849
+ retriever = cbrkit.retrieval.bm25(conversion_func=cbrkit.sim.embed.bm25(language="en"))
861
850
  retriever.put_index(frozendict(casebase))
862
851
  ```
863
852
 
864
- Then pass an empty casebase (`{}`) to signal that the retriever should use its pre-indexed data:
853
+ The storage-backed `lancedb`, `chromadb`, `zvec`, `pgvector`, and `sqlite_vec` retrievers are pure query paths over a separate `cbrkit.indexable` storage that owns the index, so you index on the storage and wrap it for querying:
865
854
 
866
855
  ```python
867
- result = cbrkit.retrieval.apply_query({}, query, retriever)
856
+ storage = cbrkit.indexable.lancedb(uri="./cases", table_name="cases")
857
+ storage.put_index(frozendict(casebase))
858
+ retriever = cbrkit.retrieval.lancedb(storage=storage, search_type="dense")
868
859
  ```
869
860
 
870
- As a convenience, CBRkit provides `apply_query_indexed` and `apply_queries_indexed` which handle the empty casebase automatically:
861
+ Query a pre-indexed retriever with `apply_query_indexed` / `apply_queries_indexed` (or pass an empty casebase `{}` to `apply_query`); querying an un-indexed retriever raises `ValueError`:
871
862
 
872
863
  ```python
873
864
  result = cbrkit.retrieval.apply_query_indexed(query, retriever)
874
- # or for multiple queries:
875
- result = cbrkit.retrieval.apply_queries_indexed(queries, retriever)
876
865
  ```
877
866
 
878
- If a retriever receives an empty casebase but has not been indexed yet, a `ValueError` is raised with a message to call `put_index()` first.
867
+ The `System` class also defaults its casebase to `{}`, so a system of pre-indexed retrievers needs no casebase at query time.
868
+
869
+ #### Typed Values and the Retain Caveat
870
+
871
+ Each backend has one text-field knob — `value_column` (`value_field` for `zvec`/`chromadb`) — naming the embeddable text, and the value type `V` follows the schema source:
872
+
873
+ - **Plain text** (`V = str`, the default) — the bare string is stored under the text knob and read back as a string.
874
+ - **Typed model** (`V = YourModel`) — pass a `model`: a dataclass or Pydantic model for `lancedb`/`zvec`/`chromadb` (fields become columns), or a SQLAlchemy mapped class for `sqlalchemy`/`pgvector`/`sqlite_vec` (its `__table__` defines the schema). Reads reconstruct model instances.
875
+ - **Mapping** (`V = Mapping[str, Any]`) — `sqlalchemy`/`pgvector`/`sqlite_vec` only, via a host-supplied `table` or `reflect=True`.
876
+
877
+ ```python
878
+ # plain strings — cbrkit builds the table
879
+ store = cbrkit.indexable.pgvector[str, str](
880
+ url=..., value_column="body", pgvector_dim=384, conversion_func=embed,
881
+ )
882
+
883
+ # typed rows — a SQLAlchemy mapped class defines the schema
884
+ class Car(Base):
885
+ __tablename__ = "cars"
886
+ key: Mapped[str] = mapped_column(primary_key=True)
887
+ desc: Mapped[str] = mapped_column()
888
+ _pgvec: Mapped[Any] = mapped_column(cbrkit.indexable.PGVECTOR(384), nullable=False)
889
+
890
+ store = cbrkit.indexable.pgvector[str, Car](url=..., model=Car, value_column="desc", ...)
891
+ ```
892
+
893
+ Pass `vector_type="halfvec"` for half-precision storage (~2x smaller, negligible recall loss); for a typed model, declare the column with the re-exported `cbrkit.indexable.HALFVEC` instead of `PGVECTOR`.
894
+
895
+ For a self-contained, file-based store, `sqlite_vec` offers the same dense/sparse/hybrid API on SQLite via the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) extension (loaded automatically). Dense KNN uses a `vec0` virtual table (so the backend inherits future `vec0` capabilities such as approximate search, and supports quantized `vector_type="int8"` storage today), sparse search uses built-in FTS5, and `Filter` `WHERE` clauses compose by joining matches back to the main table:
896
+
897
+ ```python
898
+ store = cbrkit.indexable.sqlite_vec[str, str](
899
+ url="sqlite+aiosqlite:///cases.db",
900
+ value_column="body", vector_dim=384, index_type="hybrid", conversion_func=embed,
901
+ )
902
+ store.put_index(frozendict(casebase))
903
+ retriever = cbrkit.retrieval.indexable.sqlite_vec(storage=store, search_type="hybrid")
904
+ ```
879
905
 
880
- The `System` class also supports indexed retrieval by defaulting the casebase to an empty dict.
881
- This allows creating a system where all retrievers are pre-indexed and no casebase needs to be provided at query time.
906
+ **Retain caveat:** the storage-backed retrievers search by the text column and always return `Casebase[K, str]`, projecting richer values down to their text.
907
+ A retrieve retain round-trip via `cbrkit.retrieval.indexable.*` + `cbrkit.retain.indexable` therefore lines up cleanly only when `V = str`.
908
+ For model or mapping stores, either use the backend as a typed store (read `storage.index` as `Casebase[K, V]` and retrieve with a value-based retriever like `cbrkit.retrieval.build(...)`), or re-hydrate full rows by key from `storage.index` after text retrieval.
882
909
 
883
910
  ## Evaluation
884
911
 
@@ -128,12 +128,14 @@ df = pl.read_csv("path/to/cases.csv")
128
128
  casebase = cbrkit.loaders.polars(df)
129
129
  ```
130
130
 
131
- For database access, CBRkit provides `sqlite` and `sqlalchemy` loaders (the latter requires the `sql` extra):
131
+ For ad-hoc SQLite loading, CBRkit ships a stdlib-based loader:
132
132
 
133
133
  ```python
134
134
  casebase = cbrkit.loaders.sqlite("path/to/database.db", "SELECT * FROM cases")
135
135
  ```
136
136
 
137
+ For richer relational backends (filters, upserts, vector/FTS search via pgvector on PostgreSQL or sqlite-vec on SQLite), see `cbrkit.indexable.sqlalchemy`, `cbrkit.indexable.pgvector`, and `cbrkit.indexable.sqlite_vec`.
138
+
137
139
  **Tip:** You can validate a loaded casebase against a Pydantic model using `cbrkit.loaders.validate()`:
138
140
 
139
141
  ```python
@@ -579,8 +581,7 @@ The result contains `similarities` with quality assessment scores for each case.
579
581
  ## Retain
580
582
 
581
583
  The retain phase decides whether and how to integrate new cases into the casebase.
582
- The `cbrkit.retain` module provides utility functions for this purpose.
583
- You build a retain pipeline by specifying an assessment function and a storage function:
584
+ Build a retain pipeline from an assessment function and a storage function:
584
585
 
585
586
  ```python
586
587
  retainer = cbrkit.retain.build(
@@ -592,27 +593,9 @@ retainer = cbrkit.retain.build(
592
593
  )
593
594
  ```
594
595
 
595
- CBRkit provides several built-in storage functions:
596
-
597
- - `static`: Generates keys from a fixed reference casebase to avoid collisions.
598
- - `indexable`: Keeps an `IndexableFunc`'s index in sync with the casebase.
599
-
600
- You can filter retained cases based on their assessment scores using the `dropout` wrapper:
601
-
602
- ```python
603
- retainer = cbrkit.retain.dropout(
604
- retainer_func=cbrkit.retain.build(...),
605
- min_similarity=0.5,
606
- )
607
- ```
608
-
609
- The retainer can be applied to a revise result:
610
-
611
- ```python
612
- result = cbrkit.retain.apply_result(revise_result, retainer)
613
- ```
614
-
615
- The result contains `similarities` with fitness scores and `casebase` with the updated cases.
596
+ The built-in storage functions are `static` (generates collision-free keys from a reference casebase) and `indexable` (keeps an `IndexableFunc`'s index in sync with the casebase).
597
+ Wrap a retainer with `dropout` to filter by assessment score (e.g. `min_similarity=0.5`), then apply it to a revise result via `cbrkit.retain.apply_result(revise_result, retainer)`.
598
+ The result exposes `similarities` (fitness scores) and `casebase` (updated cases).
616
599
 
617
600
  ## Full CBR Cycle
618
601
 
@@ -745,37 +728,74 @@ result = cbrkit.retrieval.apply_query(casebase, query, (retriever, reranker))
745
728
 
746
729
  ### Indexed Retrieval
747
730
 
748
- Retrievers like `bm25`, `embed`, `lancedb`, `chromadb`, and `zvec` support **indexed retrieval**, where the casebase is pre-indexed once and then queried without passing the full casebase each time.
749
- This is useful for large casebases or when using external search backends.
731
+ Indexed retrieval pre-indexes the casebase once and then queries it without passing the full casebase each time, which helps for large casebases or external search backends.
732
+ Index maintenance lives on whichever object owns the index.
750
733
 
751
- To use indexed retrieval, first create a retriever and call its `put_index()` method:
734
+ The self-contained `bm25` and `embed` retrievers own their index, so you call `put_index()` on the retriever:
752
735
 
753
736
  ```python
754
737
  from frozendict import frozendict
755
738
 
756
- bm25_func = cbrkit.sim.embed.bm25(language="en")
757
- retriever = cbrkit.retrieval.bm25(conversion_func=bm25_func)
739
+ retriever = cbrkit.retrieval.bm25(conversion_func=cbrkit.sim.embed.bm25(language="en"))
758
740
  retriever.put_index(frozendict(casebase))
759
741
  ```
760
742
 
761
- Then pass an empty casebase (`{}`) to signal that the retriever should use its pre-indexed data:
743
+ The storage-backed `lancedb`, `chromadb`, `zvec`, `pgvector`, and `sqlite_vec` retrievers are pure query paths over a separate `cbrkit.indexable` storage that owns the index, so you index on the storage and wrap it for querying:
762
744
 
763
745
  ```python
764
- result = cbrkit.retrieval.apply_query({}, query, retriever)
746
+ storage = cbrkit.indexable.lancedb(uri="./cases", table_name="cases")
747
+ storage.put_index(frozendict(casebase))
748
+ retriever = cbrkit.retrieval.lancedb(storage=storage, search_type="dense")
765
749
  ```
766
750
 
767
- As a convenience, CBRkit provides `apply_query_indexed` and `apply_queries_indexed` which handle the empty casebase automatically:
751
+ Query a pre-indexed retriever with `apply_query_indexed` / `apply_queries_indexed` (or pass an empty casebase `{}` to `apply_query`); querying an un-indexed retriever raises `ValueError`:
768
752
 
769
753
  ```python
770
754
  result = cbrkit.retrieval.apply_query_indexed(query, retriever)
771
- # or for multiple queries:
772
- result = cbrkit.retrieval.apply_queries_indexed(queries, retriever)
773
755
  ```
774
756
 
775
- If a retriever receives an empty casebase but has not been indexed yet, a `ValueError` is raised with a message to call `put_index()` first.
757
+ The `System` class also defaults its casebase to `{}`, so a system of pre-indexed retrievers needs no casebase at query time.
758
+
759
+ #### Typed Values and the Retain Caveat
760
+
761
+ Each backend has one text-field knob — `value_column` (`value_field` for `zvec`/`chromadb`) — naming the embeddable text, and the value type `V` follows the schema source:
762
+
763
+ - **Plain text** (`V = str`, the default) — the bare string is stored under the text knob and read back as a string.
764
+ - **Typed model** (`V = YourModel`) — pass a `model`: a dataclass or Pydantic model for `lancedb`/`zvec`/`chromadb` (fields become columns), or a SQLAlchemy mapped class for `sqlalchemy`/`pgvector`/`sqlite_vec` (its `__table__` defines the schema). Reads reconstruct model instances.
765
+ - **Mapping** (`V = Mapping[str, Any]`) — `sqlalchemy`/`pgvector`/`sqlite_vec` only, via a host-supplied `table` or `reflect=True`.
766
+
767
+ ```python
768
+ # plain strings — cbrkit builds the table
769
+ store = cbrkit.indexable.pgvector[str, str](
770
+ url=..., value_column="body", pgvector_dim=384, conversion_func=embed,
771
+ )
772
+
773
+ # typed rows — a SQLAlchemy mapped class defines the schema
774
+ class Car(Base):
775
+ __tablename__ = "cars"
776
+ key: Mapped[str] = mapped_column(primary_key=True)
777
+ desc: Mapped[str] = mapped_column()
778
+ _pgvec: Mapped[Any] = mapped_column(cbrkit.indexable.PGVECTOR(384), nullable=False)
779
+
780
+ store = cbrkit.indexable.pgvector[str, Car](url=..., model=Car, value_column="desc", ...)
781
+ ```
782
+
783
+ Pass `vector_type="halfvec"` for half-precision storage (~2x smaller, negligible recall loss); for a typed model, declare the column with the re-exported `cbrkit.indexable.HALFVEC` instead of `PGVECTOR`.
784
+
785
+ For a self-contained, file-based store, `sqlite_vec` offers the same dense/sparse/hybrid API on SQLite via the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) extension (loaded automatically). Dense KNN uses a `vec0` virtual table (so the backend inherits future `vec0` capabilities such as approximate search, and supports quantized `vector_type="int8"` storage today), sparse search uses built-in FTS5, and `Filter` `WHERE` clauses compose by joining matches back to the main table:
786
+
787
+ ```python
788
+ store = cbrkit.indexable.sqlite_vec[str, str](
789
+ url="sqlite+aiosqlite:///cases.db",
790
+ value_column="body", vector_dim=384, index_type="hybrid", conversion_func=embed,
791
+ )
792
+ store.put_index(frozendict(casebase))
793
+ retriever = cbrkit.retrieval.indexable.sqlite_vec(storage=store, search_type="hybrid")
794
+ ```
776
795
 
777
- The `System` class also supports indexed retrieval by defaulting the casebase to an empty dict.
778
- This allows creating a system where all retrievers are pre-indexed and no casebase needs to be provided at query time.
796
+ **Retain caveat:** the storage-backed retrievers search by the text column and always return `Casebase[K, str]`, projecting richer values down to their text.
797
+ A retrieve retain round-trip via `cbrkit.retrieval.indexable.*` + `cbrkit.retain.indexable` therefore lines up cleanly only when `V = str`.
798
+ For model or mapping stores, either use the backend as a typed store (read `storage.index` as `Casebase[K, V]` and retrieve with a value-based retriever like `cbrkit.retrieval.build(...)`), or re-hydrate full rows by key from `storage.index` after text retrieval.
779
799
 
780
800
  ## Evaluation
781
801
 
@@ -1,56 +1,56 @@
1
1
  [project]
2
2
  name = "cbrkit"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "Customizable Case-Based Reasoning (CBR) toolkit for Python with a built-in API and CLI"
5
5
  authors = [{ name = "Mirko Lenz", email = "mirko@mirkolenz.com" }]
6
6
  readme = "README.md"
7
7
  license = "MIT"
8
8
  keywords = [
9
- "cbr",
10
- "case-based reasoning",
11
- "api",
12
- "similarity",
13
- "nlp",
14
- "retrieval",
15
- "cli",
16
- "tool",
17
- "library",
9
+ "cbr",
10
+ "case-based reasoning",
11
+ "api",
12
+ "similarity",
13
+ "nlp",
14
+ "retrieval",
15
+ "cli",
16
+ "tool",
17
+ "library",
18
18
  ]
19
19
  classifiers = [
20
- "Development Status :: 4 - Beta",
21
- "Environment :: Console",
22
- "Framework :: Pytest",
23
- "Intended Audience :: Developers",
24
- "Intended Audience :: Science/Research",
25
- "Natural Language :: English",
26
- "Operating System :: OS Independent",
27
- "Programming Language :: Python :: 3.13",
28
- "Programming Language :: Python :: 3.14",
29
- "Programming Language :: Python :: 3",
30
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
- "Topic :: Scientific/Engineering :: Information Analysis",
32
- "Topic :: Software Development :: Libraries :: Python Modules",
33
- "Topic :: Utilities",
34
- "Typing :: Typed",
20
+ "Development Status :: 4 - Beta",
21
+ "Environment :: Console",
22
+ "Framework :: Pytest",
23
+ "Intended Audience :: Developers",
24
+ "Intended Audience :: Science/Research",
25
+ "Natural Language :: English",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Programming Language :: Python :: 3",
30
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
+ "Topic :: Scientific/Engineering :: Information Analysis",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Topic :: Utilities",
34
+ "Typing :: Typed",
35
35
  ]
36
36
  requires-python = ">=3.13,<4"
37
37
  dependencies = [
38
- "frozendict>=2,<3",
39
- "numpy>=2,<3",
40
- "orjson>=3,<4",
41
- "polars>=1,<2",
42
- "pydantic>=2,<3",
43
- "pyyaml>=6,<7",
44
- "rtoml>=0.12,<1",
45
- "scipy>=1,<2",
46
- "xmltodict>=1,<2",
38
+ "frozendict>=2,<3",
39
+ "numpy>=2,<3",
40
+ "orjson>=3,<4",
41
+ "polars>=1,<2",
42
+ "pydantic>=2,<3",
43
+ "pyyaml>=6,<7",
44
+ "rtoml>=0.12,<1",
45
+ "scipy>=1,<2",
46
+ "xmltodict>=1,<2",
47
47
  ]
48
48
 
49
49
  [project.optional-dependencies]
50
50
  # LLM providers
51
51
  anthropic = ["anthropic>=0.40,<1"]
52
- cohere = ["cohere>=6,<7"]
53
- google = ["google-genai>=1,<2"]
52
+ cohere = ["cohere>=7,<8"]
53
+ google = ["google-genai>=2,<3"]
54
54
  instructor = ["instructor>=1,<2"]
55
55
  ollama = ["ollama>=0.3,<1"]
56
56
  openai = ["openai>=1,<3", "tiktoken>=0.8,<1"]
@@ -76,7 +76,9 @@ graphviz = ["pygraphviz>=1,<2"]
76
76
  chromadb = ["chromadb>=1,<2"]
77
77
  lancedb = ["lancedb>=0.20,<1"]
78
78
  pandas = ["pandas>=2,<4"]
79
- sql = ["sqlalchemy>=2,<3"]
79
+ pgvector = ["pgvector>=0.4,<1", "cbrkit[sql]"]
80
+ sql = ["sqlalchemy[asyncio]>=2,<3"]
81
+ sqlite-vec = ["sqlite-vec>=0.1,<1", "aiosqlite>=0.20,<1", "cbrkit[sql]"]
80
82
  zvec = ["zvec>=0.2,<1"]
81
83
 
82
84
  # Tools
@@ -86,16 +88,18 @@ timeseries = ["minineedle>=3,<4"]
86
88
 
87
89
  # Entry points
88
90
  api = [
89
- "cbrkit[cli]",
90
- "fastapi>=0.100,<1",
91
- "pydantic-settings>=2,<3",
92
- "python-multipart>=0.0.15,<1",
93
- "uvicorn[standard]>=0.30,<1",
94
- "fastmcp>=3,<4",
91
+ "cbrkit[cli]",
92
+ "fastapi>=0.100,<1",
93
+ "pydantic-settings>=2,<3",
94
+ "python-multipart>=0.0.15,<1",
95
+ "uvicorn[standard]>=0.30,<1",
96
+ "fastmcp>=3,<4",
95
97
  ]
96
98
 
97
99
  # Bundle
98
- all = ["cbrkit[anthropic,api,bm25,chromadb,chunking,cohere,eval,google,graphs,graphviz,instructor,lancedb,levenshtein,nltk,ollama,openai,openai-agents,pandas,pydantic-ai,spacy,sql,timeseries,transformers,voyageai,zvec]"]
100
+ all = [
101
+ "cbrkit[anthropic,api,bm25,chromadb,chunking,cohere,eval,google,graphs,graphviz,instructor,lancedb,levenshtein,nltk,ollama,openai,openai-agents,pandas,pgvector,pydantic-ai,spacy,sql,sqlite-vec,timeseries,transformers,voyageai,zvec]",
102
+ ]
99
103
 
100
104
  [project.urls]
101
105
  Repository = "https://github.com/wi2trier/cbrkit"
@@ -117,11 +121,23 @@ build-backend = "uv_build"
117
121
 
118
122
  [tool.pytest]
119
123
  testpaths = ["src", "tests"]
120
- addopts = ["--cov=src/cbrkit", "--cov-report=term-missing", "--doctest-modules", "--import-mode=importlib"]
124
+ addopts = [
125
+ "--cov=src/cbrkit",
126
+ "--cov-report=term-missing",
127
+ "--doctest-modules",
128
+ "--import-mode=importlib",
129
+ ]
121
130
  doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL", "ELLIPSIS"]
122
131
 
123
132
  [tool.uv]
124
133
  default-groups = ["dev", "test", "docs"]
125
134
 
135
+ [tool.uv.extra-build-dependencies]
136
+ pygraphviz = ["setuptools"]
137
+ cbor = ["setuptools"]
138
+ warc3-wet-clueweb09 = ["setuptools"]
139
+ zlib-state = ["setuptools"]
140
+ pystemmer = ["setuptools", "cython"]
141
+
126
142
  [tool.ruff.lint.pydocstyle]
127
143
  convention = "google"
@@ -7,7 +7,7 @@ from typing import Literal, cast
7
7
  from ..helpers import (
8
8
  get_logger,
9
9
  normalize_and_scale,
10
- round,
10
+ round_int,
11
11
  sim_map2ranking,
12
12
  unpack_float,
13
13
  unpack_floats,
@@ -531,7 +531,7 @@ def similarities_to_qrels[Q, C](
531
531
 
532
532
  return {
533
533
  query: {
534
- case: round(
534
+ case: round_int(
535
535
  normalize_and_scale(sim, min_sim, max_sim, min_qrel, max_qrel),
536
536
  round_mode,
537
537
  )
@@ -0,0 +1,81 @@
1
+ """Backend-agnostic filter AST for filterable storage backends.
2
+
3
+ Filter expressions are compiled per backend (`postgresql` → SQLAlchemy
4
+ `ColumnElement[bool]`, `lancedb` → SQL string) so the same predicate
5
+ travels unchanged from the host to the storage layer.
6
+ """
7
+
8
+ from collections.abc import Collection
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(slots=True, frozen=True)
13
+ class Eq:
14
+ """Equality predicate: ``column = value``."""
15
+
16
+ column: str
17
+ value: int | str | bool
18
+
19
+
20
+ @dataclass(slots=True, frozen=True)
21
+ class In:
22
+ """Set-membership predicate: ``column IN (...)``."""
23
+
24
+ column: str
25
+ values: Collection[int | str]
26
+
27
+
28
+ @dataclass(slots=True, frozen=True)
29
+ class Like:
30
+ """Pattern predicate: ``column LIKE pattern``."""
31
+
32
+ column: str
33
+ pattern: str
34
+ escape: str | None = None
35
+
36
+
37
+ @dataclass(slots=True, frozen=True)
38
+ class And:
39
+ """Conjunction of filters."""
40
+
41
+ filters: tuple["Filter", ...]
42
+
43
+
44
+ @dataclass(slots=True, frozen=True)
45
+ class Or:
46
+ """Disjunction of filters."""
47
+
48
+ filters: tuple["Filter", ...]
49
+
50
+
51
+ @dataclass(slots=True, frozen=True)
52
+ class Not:
53
+ """Negation of a filter."""
54
+
55
+ inner: "Filter"
56
+
57
+
58
+ @dataclass(slots=True, frozen=True)
59
+ class Raw:
60
+ """Backend-native escape hatch.
61
+
62
+ The string is passed verbatim to the backend (LanceDB SQL string,
63
+ SQLAlchemy ``sa.text``). Never build this from untrusted input.
64
+ """
65
+
66
+ sql: str
67
+
68
+
69
+ type Filter = Eq | In | Like | And | Or | Not | Raw
70
+
71
+
72
+ __all__ = [
73
+ "Eq",
74
+ "In",
75
+ "Like",
76
+ "And",
77
+ "Or",
78
+ "Not",
79
+ "Raw",
80
+ "Filter",
81
+ ]