truffler 0.1.0

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 (214) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +18 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +425 -0
  5. data/bench/cassettes/004cad52c71fc8d847bf388eb9c2c288e1169b65ec4a28ce040cb7fea7c55f04.json +1 -0
  6. data/bench/cassettes/0547d3d23c3fbaae86772ffa0e4e96475aa152a2556b8fa4bc7568effcb81a28.json +1 -0
  7. data/bench/cassettes/07aa23d166ab3500178e3f3c0838183051b093507bef88e1e2d29efdf5759ee6.json +1 -0
  8. data/bench/cassettes/0858c5006c0bfac3e970ddaff3c139f6c110dbe3d8539fd95263706c637cdc8e.json +1 -0
  9. data/bench/cassettes/0c0eb7c27d76bd0b9263690a63cd92d6b68c305826318555fd5b084a09970b78.json +1 -0
  10. data/bench/cassettes/0fada0c0a103882e2895f7f455d73d2cbe2b4a81bf743bb19b2b4f6e0130126b.json +1 -0
  11. data/bench/cassettes/1117d79ac7c43b21beaf75e510461f91cd4ecc3cdbbe461f1794644af4aaf8f3.json +1 -0
  12. data/bench/cassettes/11b7444a074cca4d0ef1535db0cf0edefe4c34eac4be3c593c9c2f33d3b038b8.json +1 -0
  13. data/bench/cassettes/11d00371d1b993675be00463159164536f1f8ed90a48652b0935eb9c36f8b684.json +1 -0
  14. data/bench/cassettes/1456ebe12616ce961d68a14eada62778a7271ebd3d1b4cb6eefd6857651c7afc.json +1 -0
  15. data/bench/cassettes/14c3693f64b0e9ea7e470277fccae6ab501d404185eec5bb9074ef4b75c572c0.json +1 -0
  16. data/bench/cassettes/14fe9f6d8055d41f5d970dc32ab640c213ea8782d365c82d32b9884ba4f28c67.json +1 -0
  17. data/bench/cassettes/1a1d8557e4698426c2467aa573a240357715d3d9e3fa77bf066672a8b6bf9a86.json +1 -0
  18. data/bench/cassettes/1b5a8aaaf98305560588246cd51f53c6c185a951f8018c26bf588954d9de1cea.json +1 -0
  19. data/bench/cassettes/1fe596be747e33e9c7e4d7c50de32613957ac66bee43679b4bed96c551b1fd0a.json +1 -0
  20. data/bench/cassettes/21ebce6db6d39b1747dccf25bd3ceb298e1285425190b4d78830c395a7ad786a.json +1 -0
  21. data/bench/cassettes/23066253621e9cb00fd93cbe95ef1124ff0715585fcb6325b3abbd0802b2c303.json +1 -0
  22. data/bench/cassettes/2f6576325dfab696cec8d8b35fa89c29960bee15d10dab31c91cef507f4842be.json +1 -0
  23. data/bench/cassettes/33ec27d22c0f63dbbe93008f50af07fa8667a5c806a9fbfc7897683915412b40.json +1 -0
  24. data/bench/cassettes/351f745b0c47e0cf3272b0fea7fc4941b6aa733f013c55a9a19707b542543200.json +1 -0
  25. data/bench/cassettes/37d974fdc0c1876413111c244d363c8d2306e3229fec668c158b5c924ffdb983.json +1 -0
  26. data/bench/cassettes/39524467463dd612cbf03e314b9cfd8cbe1c5f06969da2289359cf7cf83de5da.json +1 -0
  27. data/bench/cassettes/3f2c758c111ab7f1163ba579f2329718afe15620507d356d3ebe2a053df237ea.json +1 -0
  28. data/bench/cassettes/44bbc3a3f0ceb0edb443b080b939af80dd145795505bd13cac113a35365d0f04.json +1 -0
  29. data/bench/cassettes/45bf701a13c348acb45a3b3a56990570e3caf797c3daee5d1358a477593ce587.json +1 -0
  30. data/bench/cassettes/4676a0f2df29493fdfa0b7cd75c097e647373862d88373dc0e1fa9fca26707c2.json +1 -0
  31. data/bench/cassettes/49708a50e3c78f45beed2f1b8d8d5375b99ea4ab3497fed7650e6ece4ec81ba8.json +1 -0
  32. data/bench/cassettes/54ea30ecca8fa49f607d5c8265296eb16edb48d0620a7399c6480d4cb0f43ae1.json +1 -0
  33. data/bench/cassettes/550e73a8291f53d0f2b008f166560eff4d10cc7c09a927d3c4f70544f2578300.json +1 -0
  34. data/bench/cassettes/570039823d5af1b6c1d4bcbe14f9802d40e851f8e4946278a46884c76c0ceee1.json +1 -0
  35. data/bench/cassettes/5ae9dce82fa7b9084b592f657a24acfd5852c4fcb091a6a8e22f2cbbf947bcdd.json +1 -0
  36. data/bench/cassettes/5c40794d3bf6e229c6ffd26922388776121f971ef27279e02a35ce004e0794dd.json +1 -0
  37. data/bench/cassettes/62f2e71a2d9cfdc18fdd6600f5a1450519b25a75c2f799b7695258627250497d.json +1 -0
  38. data/bench/cassettes/642becd7ad9cd4cc2ac6d389ef8ff14fd9fe125beca834afee625ea2c31476be.json +1 -0
  39. data/bench/cassettes/656cc1158890e928c129d9cd54c1e08af1b6a2c0f6f94bacf395b13a198dc81f.json +1 -0
  40. data/bench/cassettes/67b30efd5681046fb5f2252d73f9aedf603be1b7c7895e4e71c6cfb88238309e.json +1 -0
  41. data/bench/cassettes/691958dd39537d72c4b0ab4ba69fea3c51b8e06a49a772f3ffd5bb5747dfe99d.json +1 -0
  42. data/bench/cassettes/6931e51f0455b5e3260165aa22a445d3a4f782ae5233a961384149cfbb045fe0.json +1 -0
  43. data/bench/cassettes/6b79cf1a80fc9edd09bb4606613ff2de2ab2bc61487983dffac1174025270997.json +1 -0
  44. data/bench/cassettes/6d683884d5d039db072ac18c9420bdcc989838f08a71096076cccc2edcd48234.json +1 -0
  45. data/bench/cassettes/6df49dc7eae4104adf1b020fd846d893da3e49f4f13a4c6fd1f87a78c410ba31.json +1 -0
  46. data/bench/cassettes/75cc5d99bbc1d48ace6095d41daae1e2e2c829291f01d40f0ce40d6eea52c0bf.json +1 -0
  47. data/bench/cassettes/771a86cbb6c025101384a50d01a4745683e1c2b17bbfff73e40e475dbf016efb.json +1 -0
  48. data/bench/cassettes/7a2a93c357473f4a289a6ec250e3bfe6f51152a0c448e5420ffebee7872f9d11.json +1 -0
  49. data/bench/cassettes/7a4774a02c3eebe24d4fbd443ea90661711fe5cb8248bce7a2dedfedf6ddbc03.json +1 -0
  50. data/bench/cassettes/7c1fa04f8eb43251ca3f6652977b698e4e6ba5e120da8301b4dbce79ef01ccb9.json +1 -0
  51. data/bench/cassettes/7c4dea48c2bc2c413da81307ce3cbb2749b20e16e4d831fca5861decf11ec55b.json +1 -0
  52. data/bench/cassettes/7dd499da389eaeeb59eba533695c20fd1a43e25f89f21e3456b826f9953b2010.json +1 -0
  53. data/bench/cassettes/80438d0bbfe106baa236eef358e46260693e6491f67e7a9f8ea1076c6fbfd449.json +1 -0
  54. data/bench/cassettes/80db562534181514f243ac74d7ddaec1fafc28b2b72ae35482cfc2de8f2d9e61.json +1 -0
  55. data/bench/cassettes/825a51543665a468a55c823788c68eb5785c90be8131988f72bf98f4f0294177.json +1 -0
  56. data/bench/cassettes/83d8276c227b73e542bd70c023d9a8ffa1c2cf4dd78e71d61457f097bc743d19.json +1 -0
  57. data/bench/cassettes/891405ffd7727b6d4c4186e2c60a08aa8b62c7b711bd1558c8033fcf8e4178a4.json +1 -0
  58. data/bench/cassettes/89fabff05dc4c0abdf630781a324b7802cbc0dc8bd09b20ce59643465a2aeaec.json +1 -0
  59. data/bench/cassettes/8cbceece3ca6136b6c6bb71c9bf28f409a5fa070596ebfc0f3187aba287bf6d3.json +1 -0
  60. data/bench/cassettes/8d56da27350d9dea20468eae30fa51cbbb10637c06d977b3e1a00734966be46f.json +1 -0
  61. data/bench/cassettes/8fb43b452b5e15248f34bf48e41cfc227129cfe349fe4b3fc4ab7133d66914bc.json +1 -0
  62. data/bench/cassettes/92e5426bd0c1f31d7acca92f77d7f2586e74054467729f0be28d139f515354d5.json +1 -0
  63. data/bench/cassettes/938be225155e1952b817652778b7cab377017fc4a219628ae89ad01a9c1151bc.json +1 -0
  64. data/bench/cassettes/9519ad701b28e7b2c5f1e1cad5df1ec3e5254c8cec969b9bee1361af8803d39d.json +1 -0
  65. data/bench/cassettes/9536e8ed68330eda400d765c3ca16a4dd3bfe0d026b569599a33e7e4951f4860.json +1 -0
  66. data/bench/cassettes/99c6e5aefd97722f4028bacc11d299eb9036805cd5cf7bf75f879ebe3f27c00a.json +1 -0
  67. data/bench/cassettes/9cdfd2f3ef8dc4cde917a9d196e3137159067c10ec8d2132f32da034865f9959.json +1 -0
  68. data/bench/cassettes/a1e3030984d4e1982fb5a2b2068664187358c88c6b71f768841d3e78406e9535.json +1 -0
  69. data/bench/cassettes/a2140f51b8fa25da73efd387a41f9eb84ffbc1f1ab47dec0c3b37b249ff373a0.json +1 -0
  70. data/bench/cassettes/a36d248ce5c26fd66430b780c8ecdd176f29830ac4edb2bf0b2e07cb93ffb865.json +1 -0
  71. data/bench/cassettes/a64329816ca1e537e90b89862a86be119e1e6236c6f3cf0141a96505b0734364.json +1 -0
  72. data/bench/cassettes/aba24c67dcaefa51c204a529b5ea3fe01333181a608a8ab8899568c731677e7d.json +1 -0
  73. data/bench/cassettes/aed233524926f2b4ff8c2ec21224bd284595c91eb57e0fb6981cdc8ac7e7d8d3.json +1 -0
  74. data/bench/cassettes/b00e9da6403e48aa1f4f9346419f88b2a336e1a2ac4a28515d84b2337eb777b2.json +1 -0
  75. data/bench/cassettes/b02f4c76c073882ff0fd12fddc28e4db52d9e6486a130194bf1057c7693de831.json +1 -0
  76. data/bench/cassettes/b6a9cb89ae4afd08a6919eaf7f609790c895d4cf22d47dad6c3b287b9278be87.json +1 -0
  77. data/bench/cassettes/b71b982225eab48d02c81913877571dfa53717c0983d1d72cbe3787242dda055.json +1 -0
  78. data/bench/cassettes/bda5e115b566e7f955aa45222859fe4f725949d5f91e3d794821701491448510.json +1 -0
  79. data/bench/cassettes/c0ac703268faf8d49dbc1ad0e877693a9f52181ae6070de844fcd4218d33d5e4.json +1 -0
  80. data/bench/cassettes/c0b57bcbb4004edd44c2d8c7e1f8cfc3567d954a3d5b21b2cda7257e3bc07437.json +1 -0
  81. data/bench/cassettes/d1ade9adde20981211c470a77b4d560264cae290941e692be6876a79227e3d18.json +1 -0
  82. data/bench/cassettes/d28de3038544a5bf9c8425b6976e360fae71c5ca96409784ad6642703701986a.json +1 -0
  83. data/bench/cassettes/d6418acb9f7f58ffdf3049481cbd17cfb3f0581064e157c9c58c19a3fc26b4d4.json +1 -0
  84. data/bench/cassettes/d71e8cf7c2be4b6e4089eb40c5a108c0c45fc58398af5138f905129f640afbb8.json +1 -0
  85. data/bench/cassettes/d9672c45b5358b92f7740abfb20b287235cf949990c88558f41d3a9c00eb6d3b.json +1 -0
  86. data/bench/cassettes/da0c9ce695111f6acf3cfb58dc0494f79230739d1773377db420bf438fd2e244.json +1 -0
  87. data/bench/cassettes/db57e3a15971cd2832313172c0d7df6e6a48b13a276ba9877eeb7d39389d74d3.json +1 -0
  88. data/bench/cassettes/db9c8c4d0a8d855a2578572afe5edc5eebb39c0fd5f04489614ea2557ec4f0cd.json +1 -0
  89. data/bench/cassettes/e2c396b0475a07a615dde9208b088e9e479720c1ec5889ade07fbfd0d28fb517.json +1 -0
  90. data/bench/cassettes/e3006ccc97646f43f4ea23694665526e47982fa1ea413359b9669071f6cb990d.json +1 -0
  91. data/bench/cassettes/e623737636e797d22f80d128cbff46101837044e159f41fa62a8d3d0cc6bb996.json +1 -0
  92. data/bench/cassettes/ea0cefaaac1a62e8eac962391df025a3b14c3a3d5b69fe57d60b65811010908c.json +1 -0
  93. data/bench/cassettes/ee3e06456cf4201ab46f579dd53bdea4071ce43bfe02e9b870bcb4b9c1c680b4.json +1 -0
  94. data/bench/cassettes/f26e675da9088f08cb76dd6b3fd89230b84fd1b9be3767c36857217bda2da15b.json +1 -0
  95. data/bench/cassettes/f396f34b2356d42af385eac9f5760e97e553a584c3f93da7bd618ad311e04d93.json +1 -0
  96. data/bench/cassettes/fce02c5c04cb8c27e868f6c54cfcbbbf69bde2f7f612d50ff8fb6d0aec9df3e3.json +1 -0
  97. data/bench/cassettes/ffc09a04b083822c1597dd96bb27ee4cde365bc8346175ebf6e378ab2ab1b598.json +1 -0
  98. data/bench/cassettes/manifest.json +1 -0
  99. data/bench/fixtures/gold.jsonl +27 -0
  100. data/bench/fixtures/injection.jsonl +6 -0
  101. data/bench/fixtures/records.jsonl +90 -0
  102. data/bench/params.yml +30 -0
  103. data/lib/generators/truffler/install/install_generator.rb +51 -0
  104. data/lib/generators/truffler/install/templates/channel.rb.tt +17 -0
  105. data/lib/generators/truffler/install/templates/initializer.rb.tt +23 -0
  106. data/lib/generators/truffler/install/templates/migration.rb.tt +109 -0
  107. data/lib/tasks/truffler/bench.rake +30 -0
  108. data/lib/tasks/truffler/suggestions.rake +8 -0
  109. data/lib/tasks/truffler.rake +26 -0
  110. data/lib/truffler/answers.rb +66 -0
  111. data/lib/truffler/benchmark/adapters.rb +80 -0
  112. data/lib/truffler/benchmark/database.rb +31 -0
  113. data/lib/truffler/benchmark/dataset.rb +65 -0
  114. data/lib/truffler/benchmark/email.rb +50 -0
  115. data/lib/truffler/benchmark/generator.rb +188 -0
  116. data/lib/truffler/benchmark/metrics.rb +62 -0
  117. data/lib/truffler/benchmark/params.rb +91 -0
  118. data/lib/truffler/benchmark/runner.rb +370 -0
  119. data/lib/truffler/benchmark/synthetic_jev.rb +85 -0
  120. data/lib/truffler/benchmark.rb +29 -0
  121. data/lib/truffler/broadcaster.rb +46 -0
  122. data/lib/truffler/budget.rb +118 -0
  123. data/lib/truffler/canonical.rb +27 -0
  124. data/lib/truffler/clients/base.rb +49 -0
  125. data/lib/truffler/clients/callable.rb +26 -0
  126. data/lib/truffler/clients/cassette.rb +44 -0
  127. data/lib/truffler/clients/fake.rb +85 -0
  128. data/lib/truffler/clients/ruby_llm_typesafe.rb +32 -0
  129. data/lib/truffler/configuration.rb +89 -0
  130. data/lib/truffler/definition.rb +176 -0
  131. data/lib/truffler/embeddings/backfill.rb +48 -0
  132. data/lib/truffler/embeddings/column_store.rb +65 -0
  133. data/lib/truffler/embeddings/embedder.rb +53 -0
  134. data/lib/truffler/embeddings/fake_embedder.rb +45 -0
  135. data/lib/truffler/embeddings/label_vector.rb +81 -0
  136. data/lib/truffler/embeddings/neighbor_store.rb +66 -0
  137. data/lib/truffler/embeddings/ruby_llm_embedder.rb +25 -0
  138. data/lib/truffler/embeddings/ruby_store.rb +12 -0
  139. data/lib/truffler/embeddings/vector_store.rb +84 -0
  140. data/lib/truffler/embeddings.rb +30 -0
  141. data/lib/truffler/errors.rb +40 -0
  142. data/lib/truffler/instrumentation.rb +17 -0
  143. data/lib/truffler/jobs/backfill_job.rb +45 -0
  144. data/lib/truffler/jobs/embed_job.rb +33 -0
  145. data/lib/truffler/jobs/encode_query_job.rb +17 -0
  146. data/lib/truffler/jobs/expire_lenses_job.rb +15 -0
  147. data/lib/truffler/jobs/label_flush_job.rb +39 -0
  148. data/lib/truffler/jobs/lens_backfill_job.rb +33 -0
  149. data/lib/truffler/jobs/provider_search_job.rb +18 -0
  150. data/lib/truffler/jobs/prune_query_misses_job.rb +15 -0
  151. data/lib/truffler/jobs/rerank_chunk_job.rb +14 -0
  152. data/lib/truffler/jobs/resume_job.rb +61 -0
  153. data/lib/truffler/jobs/smart_search_job.rb +14 -0
  154. data/lib/truffler/label_definition.rb +153 -0
  155. data/lib/truffler/labeling/backfill.rb +189 -0
  156. data/lib/truffler/labeling/labeler.rb +161 -0
  157. data/lib/truffler/labeling/queue.rb +125 -0
  158. data/lib/truffler/labeling/request_builder.rb +87 -0
  159. data/lib/truffler/labeling/supplied.rb +70 -0
  160. data/lib/truffler/lenses/activator.rb +51 -0
  161. data/lib/truffler/lenses/backfill.rb +112 -0
  162. data/lib/truffler/lenses/draft.rb +16 -0
  163. data/lib/truffler/lenses/drafter.rb +147 -0
  164. data/lib/truffler/lenses/fake_generator.rb +58 -0
  165. data/lib/truffler/lenses/lens.rb +161 -0
  166. data/lib/truffler/lenses/lens_label.rb +51 -0
  167. data/lib/truffler/lenses/policy.rb +46 -0
  168. data/lib/truffler/lenses/previewer.rb +217 -0
  169. data/lib/truffler/lenses/proposer.rb +53 -0
  170. data/lib/truffler/lenses/ruby_llm_generator.rb +42 -0
  171. data/lib/truffler/lenses/scope.rb +35 -0
  172. data/lib/truffler/lenses/sealed_description.rb +29 -0
  173. data/lib/truffler/lenses/settings.rb +45 -0
  174. data/lib/truffler/lenses/validation.rb +69 -0
  175. data/lib/truffler/lenses/version.rb +34 -0
  176. data/lib/truffler/lenses.rb +138 -0
  177. data/lib/truffler/misses/clusterer.rb +69 -0
  178. data/lib/truffler/misses/recorder.rb +29 -0
  179. data/lib/truffler/misses/suggestions.rb +49 -0
  180. data/lib/truffler/misses.rb +83 -0
  181. data/lib/truffler/model.rb +111 -0
  182. data/lib/truffler/providers/backup.rb +58 -0
  183. data/lib/truffler/providers/runner.rb +67 -0
  184. data/lib/truffler/providers.rb +57 -0
  185. data/lib/truffler/query_encoding/cache.rb +71 -0
  186. data/lib/truffler/query_encoding/encoder.rb +183 -0
  187. data/lib/truffler/query_encoding/prefetch.rb +34 -0
  188. data/lib/truffler/query_encoding.rb +12 -0
  189. data/lib/truffler/questions.rb +78 -0
  190. data/lib/truffler/railtie.rb +11 -0
  191. data/lib/truffler/records/embedding.rb +39 -0
  192. data/lib/truffler/records/label.rb +11 -0
  193. data/lib/truffler/records/query_miss.rb +20 -0
  194. data/lib/truffler/records/record_state.rb +30 -0
  195. data/lib/truffler/redaction.rb +24 -0
  196. data/lib/truffler/registry.rb +16 -0
  197. data/lib/truffler/search/encoding.rb +84 -0
  198. data/lib/truffler/search/encoding_cache.rb +68 -0
  199. data/lib/truffler/search/keystroke.rb +149 -0
  200. data/lib/truffler/search/query.rb +67 -0
  201. data/lib/truffler/search/result.rb +100 -0
  202. data/lib/truffler/search/sql.rb +197 -0
  203. data/lib/truffler/smart_search/dispatcher.rb +88 -0
  204. data/lib/truffler/smart_search/reranker.rb +105 -0
  205. data/lib/truffler/smart_search/run.rb +306 -0
  206. data/lib/truffler/smart_search/starter.rb +73 -0
  207. data/lib/truffler/smart_search/store.rb +89 -0
  208. data/lib/truffler/smart_search.rb +43 -0
  209. data/lib/truffler/tokens.rb +12 -0
  210. data/lib/truffler/usage.rb +7 -0
  211. data/lib/truffler/version.rb +3 -0
  212. data/lib/truffler/vocabulary.rb +48 -0
  213. data/lib/truffler.rb +41 -0
  214. metadata +336 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 28c9f27b6811a398815e7cc1026b0d1ed0ef1caf546bd820a59be4efdd4dcffa
4
+ data.tar.gz: 1889959cc8dfd3f32af19a37dda640673e08d6459f5a5e7722747cd32a7422fd
5
+ SHA512:
6
+ metadata.gz: 76974d97ae716de75fc3cc1992868c2cabbd94c6851d6a85267fb5c3c1d18ca21f2f998c6ae1d135192081bd7b544351801fc88d33cde4070c56464a01be4d98
7
+ data.tar.gz: 8a35786e72b7e5b2996ce915da8419cb8d0d0873c8839829155b7d831f043caf4df9a9be29b9219f75d2403abbbd2c915a09b06b31e0135566a0dbe53f14774e
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - `truffler do ... end` declarations on Active Record models: tenant, fields, noul/choice/score labels, keyword and exact sources, embeddings, a provider, surfaces, ranking weights, and `weak_below`.
6
+ - Labeling on commit through a tenant-grouped queue, with packed Jev requests, per-record fingerprints, relabeling when watched fields change, `ResumeJob`, and a spend-capped backfill (`rake truffler:backfill`, `rake truffler:status`).
7
+ - Host-supplied label answers: `label ..., from: ->(record) { ... }` stores answers your app already computes (with optional `watch:`, `version:`, and `description:`) and never asks Jev about them: no request, no budget slot, no spend, and written even while Jev is down. They filter, boost, fill label vectors, and take part in query encoding like asked labels. `record.truffler_refresh_labels!` rewrites them on demand.
8
+ - One account-wide Jev budget with headroom, priority ceilings (live, encode, rerank, backfill), per-user caps, and a per-tenant live cap.
9
+ - Jev clients for `ruby_llm-typesafe`, a `Callable` for host clients, a fake, and cassettes. `truffler.*` notifications carry only ids, numbers, and digests.
10
+ - Optional text embeddings, and label vectors stored as named-dimension embeddings. Vector stores for pgvector/sqlite-vec (`neighbor`), exact cosine in Ruby, or a host-maintained column.
11
+ - Keystroke search in one SQL query with hybrid dot-product scoring, chips, the Smart search invite row, the encoding status, and a watermark for "new matches".
12
+ - Query encoding with deduplicated prefetch, cached late answers, and per-user caching when lenses are visible.
13
+ - Smart search: a candidate snapshot, chunked Jev rerank into append-only Strong/Possible/Unlikely buckets, pause and cancel, and data-free Action Cable pings, plus a generated `TrufflerChannel`.
14
+ - Provider backup search for exact-text or weak-local queries, in its own section.
15
+ - Query miss log with distinct-user gating, retention (`PruneQueryMissesJob`), and `rake truffler:suggestions`.
16
+ - Lenses: drafting, preview, version comparison, activation with backfill, regenerate, restore, history, proposals from misses, expiry (`ExpireLensesJob`), a creators policy, and the `authorize_lens` hook.
17
+ - `rake truffler:bench` benchmark with synthetic fixtures, cassette replay, packed-batch agreement, and injection checks.
18
+ - Install generator, README, and host integration guide.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Kieran Klaassen
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,425 @@
1
+ # Truffler
2
+
3
+ Intent search for Rails apps, built on your own database and TypeSafe Jev.
4
+
5
+ When a record is saved, Truffler asks Jev the typed questions you declared about it (yes/no "nouls", choices, and scores) and stores each answer as a number in `truffler_labels`. Taken together, a record's answers form a named-dimension embedding: one float per label, and one per option for choice labels. Optional text embeddings sit beside them. A search query is encoded the same way: for each label, Jev decides whether the query filters on it, prefers it, or ignores it. Keystroke search then ranks records in one SQL query that adds up a weighted dot product over the label dimensions, text similarity, keyword hits, and exact-source hits:
6
+
7
+ ```
8
+ score = w_label * SUM(intent weight * label value) + w_text * text similarity
9
+ + w_keyword * keyword hit + w_exact * exact hit
10
+ ```
11
+
12
+ Truffler also provides:
13
+
14
+ - **Smart search.** When the searcher asks for it explicitly, Jev reranks the top candidates in chunks. Results stream into Strong, Possible, and Unlikely buckets, announced by data-free Action Cable pings.
15
+ - **Provider backup.** An external search (Gmail, for example) runs in its own section when the query asks for exact text or the local results are weak.
16
+ - **Lenses.** A permitted user describes a kind of search in plain language. Truffler drafts label questions for it, previews them on a sample, and activates them as new, versioned dimensions in that user's scope.
17
+
18
+ Truffler ships no UI components. It returns plain Ruby data, which your controllers pass to your own views. [docs/host-integration.md](docs/host-integration.md) maps every field to the UI state it drives.
19
+
20
+ Requirements: Ruby 3.2+, Rails (Active Record, Active Job, Active Support) 7.2 or 8.x, a job backend, and a cache store shared by web and job processes that supports `increment` (Solid Cache, Redis, or Memcached).
21
+
22
+ ## Install
23
+
24
+ ```ruby
25
+ # Gemfile
26
+ gem "truffler"
27
+ gem "ruby_llm-typesafe" # the default Jev client; skip it if you wrap your own client
28
+ ```
29
+
30
+ ```bash
31
+ bin/rails generate truffler:install
32
+ bin/rails db:migrate
33
+ ```
34
+
35
+ The generator writes three files:
36
+
37
+ - A migration that creates `truffler_labels`, `truffler_record_states`, `truffler_embeddings`, `truffler_query_misses`, `truffler_lenses`, and `truffler_lens_versions`.
38
+ - `config/initializers/truffler.rb`.
39
+ - `app/channels/truffler_channel.rb`.
40
+
41
+ It takes two options. `--record-id-type=bigint|integer|string|uuid` must match your primary keys. `--vector-dimensions=N` stores embeddings in a pgvector column of that width; it needs Postgres and the `neighbor` gem, which provides the `t.vector` column type.
42
+
43
+ Truffler digests user keys and query misses with `secret_key_base`. Rails supplies it automatically; outside Rails, set `config.secret_key_base`.
44
+
45
+ ## Declaring a model
46
+
47
+ Include `Truffler::Model` and declare what Jev reads and asks:
48
+
49
+ ```ruby
50
+ class Email < ApplicationRecord
51
+ include Truffler::Model
52
+
53
+ truffler do
54
+ tenant :account_id # every search and Jev request stays inside one tenant
55
+ reads :subject, :body, :sender_name # the fields Jev sees
56
+
57
+ label :needs_action, :noul, question: "Does this email need the reader to act or reply?",
58
+ criteria: { true => "Asks for a reply, a decision, a payment, or a task", false => "FYI, receipts, newsletters" },
59
+ filter_at: 0.6, boost: 2.0
60
+ label :urgent, :noul, question: "Is this email time-sensitive?", boost: 2.0
61
+ label :category, :choice, question: "Which category fits this email?",
62
+ options: { billing: "Invoices, receipts, and payments", travel: "Trips and bookings", other: nil },
63
+ filter_at: 0.5
64
+ label :importance, :score, question: "How important is this email to the reader?",
65
+ legend: { 0 => "Ignorable", 1 => "Worth a look", 2 => "Must read" }
66
+
67
+ keyword :subject, :body # LIKE match on plaintext columns
68
+ exact :sender, ->(scope, token) { scope.where(sender_email: token) }
69
+ embeddings dimensions: 256 # optional text embeddings
70
+ provider :gmail, label: "Gmail", search: ->(query, tenant:, user:) { GmailSearch.call(query, account_id: tenant, user_key: user) }
71
+ order :received_at, :desc
72
+ arrived_at :received_at # drives "N new matches" and lens samples (default created_at)
73
+ surface :inbox, explicit_action: :enter # :enter, :key, or :row
74
+ ranking label: 1.0, text: 1.0, keyword: 0.5, exact: 1.0
75
+ weak_below 3 # fewer keystroke results than this counts as weak
76
+ end
77
+ end
78
+ ```
79
+
80
+ Here is what each option does:
81
+
82
+ - `filter_at` makes a label a hard filter at that probability when the query asks for it. `boost` is the label's weight when the query prefers it. `filter_weight:` (default 0) is how much intent weight a filter adds on top.
83
+ - Choice `options:` may be a callable of the tenant key, which gives each tenant its own vocabulary.
84
+ - `keyword` also accepts a single callable, `->(scope, tokens) { relation }`.
85
+ - `embeddings column: :my_vector` searches a vector column you maintain yourself. Truffler never writes it.
86
+ - `embeddings` refuses to send encrypted fields to the embedding provider unless you pass `allow_encrypted: true`.
87
+
88
+ `label key, type, **options` takes:
89
+
90
+ | Option | Applies to | Meaning |
91
+ |---|---|---|
92
+ | `question:` | asked labels (required); optional with `from:` | What Jev is asked about each record. |
93
+ | `criteria:` | `:noul` | `{ true => "...", false => "..." }` guidance for Jev. |
94
+ | `options:` | `:choice` (required) | Option names, `{ option => description }`, or a callable of the tenant key. |
95
+ | `legend:` | `:score` (required) | Two or more ordered levels, as an array or `{ index => description }`. |
96
+ | `filter_at:`, `boost:`, `filter_weight:` | all | Filter threshold, boost weight, and the intent weight a filter adds. |
97
+ | `description:` | all | The label's wording in query encoding. Defaults to the question, then to the key. |
98
+ | `from:` | all | `->(record) { answer }`. Makes the label host-supplied; Jev is never asked. See [Labels you already compute](#labels-you-already-compute). |
99
+ | `watch:` | with `from:` | Extra columns whose change refreshes the label, in addition to `reads`. |
100
+ | `version:` | with `from:` | Any value; changing it rewrites the label for every record on the next backfill. |
101
+
102
+ Keys must be lowercase snake case without a double underscore, and `lens` is reserved.
103
+
104
+ Saving a record enqueues labeling (and embedding, when enabled) after commit, but only when a field in `reads`, the tenant column, or a supplied label's `watch:` column changed. Destroying a record removes its labels, state, and embeddings.
105
+
106
+ ### Labels you already compute
107
+
108
+ If your app already classifies records, declare those answers with `from:` instead of asking Jev again. happyhappy, for example, stores sentiment, anger, category, product, actionability, and author role on each item's classification:
109
+
110
+ ```ruby
111
+ class Item < ApplicationRecord
112
+ include Truffler::Model
113
+
114
+ truffler do
115
+ tenant :workspace_id
116
+ reads :title, :body
117
+
118
+ label :sentiment, :choice, options: %w[positive neutral negative mixed],
119
+ from: ->(item) { item.classification&.sentiment }, filter_at: 0.5
120
+ label :anger, :noul, from: ->(item) { item.classification&.anger }, boost: 2.0
121
+ label :category, :choice, options: ->(workspace_id) { Category.names_for(workspace_id) },
122
+ from: ->(item) { item.classification&.category_probabilities }, description: "what the feedback is about"
123
+ label :actionability, :score, legend: %w[none vague clear], from: ->(item) { item.classification&.actionability },
124
+ watch: [ :triaged_at ], version: 2
125
+
126
+ label :needs_reply, :noul, question: "Does this item ask the team for a reply?" # still asked of Jev
127
+ end
128
+ end
129
+ ```
130
+
131
+ `from:` receives the record and returns the answer in the shape Jev answers are stored in:
132
+
133
+ | Type | `from:` returns | Stored |
134
+ |---|---|---|
135
+ | `:noul` | a probability from 0 to 1, or `true`/`false` | the probability under `label` |
136
+ | `:choice` | one option string (meaning 1.0 for it), or `{ option => probability }` | one row per option, `label:option`; options left out store 0.0 |
137
+ | `:score` | a level index into `legend:` | `index / (levels - 1)`, as for Jev scores |
138
+
139
+ `nil` stores nothing: the record reads as missing that label, not as 0. An answer out of shape (an undeclared option, a probability outside 0..1, a level past the legend) or a `from:` that raises stores nothing for that label and emits `truffler.supplied_label_failed` with the label key and error class. Rows already stored keep serving until the next good write.
140
+
141
+ Supplied labels never reach Jev. They are never in a labeling request, take no budget slot, cost nothing, and do not count toward a lens or backfill spend cap. A flush or backfill where only supplied labels are stale makes no Jev call. The labeling job writes them before it asks Jev anything, so a Jev outage or a budget denial never holds them back. Otherwise they behave like asked labels: same `truffler_labels` rows, the same filters, boosts, chips, and label vectors, and query encoding asks Jev how a query uses them (using `description:`), within the one encoding call per query.
142
+
143
+ To keep them fresh:
144
+
145
+ - Changing a `reads` field, the tenant column, or one of the label's `watch:` columns refreshes it after commit. Stored values keep serving until the labeling job rewrites them.
146
+ - Call `record.truffler_refresh_labels!` when the answers change somewhere truffler cannot see, such as the job that runs your classifier after insert. It rewrites the record's supplied labels immediately, with no Jev call.
147
+ - Bump `version:` when the logic behind `from:` changes. That changes the vocabulary version, so `rake truffler:backfill` rewrites the label for every record, again at no cost.
148
+
149
+ A supplied label's fingerprint digests its type, options, legend, description, and `version:`, never the Jev model, so changing `config.model` does not rewrite supplied labels.
150
+
151
+ ## Clients
152
+
153
+ The default client is `Truffler::Clients::RubyLLMTypeSafe`. It needs ruby_llm 2 and `ruby_llm-typesafe`, and it calls `RubyLLM.chat(model:, provider: :typesafe).with_schema(...)`.
154
+
155
+ If you already have a TypeSafe client, wrap it in `Truffler::Clients::Callable`. The wrapped object must respond to `evaluate(state:, schema:)`, and may also accept `model:`. It returns either the answers hash or `{"answers" => ..., "model" => ..., "usage" => {"input_tokens" => ...}}`:
156
+
157
+ ```ruby
158
+ config.client = Truffler::Clients::Callable.new(TypeSafeClient.new)
159
+ ```
160
+
161
+ `schema:` holds the questions in TypeSafe wire shape. When a response carries no token count, Truffler estimates it from the request size and flags the estimate. Errors reach you as `Truffler::ClientError`, carrying only the HTTP status and the error class name.
162
+
163
+ ## Keystroke search
164
+
165
+ ```ruby
166
+ result = Email.truffler(params[:q], tenant: Current.account.id, scope: Current.account.emails, user: Current.user,
167
+ suppressed: params[:removed_chips], surface: :inbox)
168
+ ```
169
+
170
+ `tenant:` is required on a model that declares a tenant. `scope:` must be a relation of the model, and results never leave it. `suppressed:` takes the keys of chips the searcher removed. The call also accepts `limit:` (default 50) and `weights:`, which overrides `ranking` for one call.
171
+
172
+ A keystroke search makes no network call. It reads the query encoding and query vector from the cache. When the cache misses, it enqueues `EncodeQueryJob`, and the next keystroke or reload picks up the result.
173
+
174
+ The returned `Truffler::Search::Result` exposes:
175
+
176
+ - `records` and `ids`.
177
+ - `chips`: `[{key:, label:, kind: :filter | :boost, name:}]`.
178
+ - `invite_row`: `{query:, reason: :weak | :empty | :encoding_pending}` or nil.
179
+ - `encoding_status`: `:cached`, `:pending`, or `:none`.
180
+ - `watermark` and `new_matches_count`.
181
+ - `explicit_action`: the surface's declared action.
182
+ - `score(record)`, `breakdown(record)`, and `contributions(record)`, for debugging.
183
+ - `promoted_ids(run)` and `smart_ranking_paused?(run)`, used alongside a Smart run.
184
+
185
+ For the "N new matches" row on a later request, pass the watermark back in:
186
+
187
+ ```ruby
188
+ Email.jev_new_matches_count(params[:q], tenant: account.id, scope: account.emails, user: current_user, since: Time.iso8601(params[:since]))
189
+ ```
190
+
191
+ ## Smart search and streaming
192
+
193
+ The searcher's explicit action (the `explicit_action` their surface declares) starts a Smart run:
194
+
195
+ ```ruby
196
+ run = Email.jev_smart_search(params[:q], tenant: account.id, scope: account.emails, user: Current.user,
197
+ surface: :inbox, suppressed: params[:removed_chips])
198
+ ```
199
+
200
+ Starting a run happens in three steps:
201
+
202
+ 1. Truffler takes a snapshot of candidate ids from your scope. The keystroke ranking comes first, topped up with the newest records when no encoding exists yet.
203
+ 2. It supersedes the searcher's previous run.
204
+ 3. It enqueues `SmartSearchJob`.
205
+
206
+ The keystroke list is left untouched. The job then does the following:
207
+
208
+ 1. Starts the provider backup, if one is declared.
209
+ 2. Takes a rerank slot under the per-user cap. If no slot is available, the run pauses.
210
+ 3. Waits up to `encoding_deadline` for an in-flight query encoding and applies its filters.
211
+ 4. Fans out one `RerankChunkJob` per `rerank_chunk_size` candidates.
212
+
213
+ Each chunk appends to the buckets and pings. When the searcher edits the query or accepts or removes a chip, call `Email.jev_cancel_smart_search(tenant:, user:, surface:)`.
214
+
215
+ Runs live in the cache store for `smart_run_ttl`. Load one with `Truffler::SmartSearch.find(run_id, user: Current.user, tenant: Current.account.id)`, passing the same `user:` and `tenant:` you gave `jev_smart_search`. A run that belongs to another searcher or tenant reads as expired, so a leaked run id shows nothing. `run.to_h` is the whole `smart` prop: ids, scores, and states, never record data.
216
+
217
+ ### Pings and Inertia partial reloads
218
+
219
+ The generated `TrufflerChannel` streams `truffler:<user_key>`. The user key is `"User:42"` for a record and `to_s` for anything else, and it must match the `user:` you pass to searches. Each ping is `{run_id, section, changed_at}` with `section` either `"smart"` or `"provider"`. No result data travels over the socket.
220
+
221
+ The client ignores pings for other runs and reloads only the Smart prop, following happyhappy's `use-mood-stream.ts` pattern:
222
+
223
+ ```ts
224
+ consumer.subscriptions.create({ channel: "TrufflerChannel" }, {
225
+ received({ run_id }: { run_id: string; section: string }) {
226
+ if (run_id === currentRunId) router.reload({ only: ["smart"] })
227
+ },
228
+ })
229
+ ```
230
+
231
+ On the server, the controller renders the run as a lazy prop, so a partial reload recomputes only that prop:
232
+
233
+ ```ruby
234
+ run = params[:run_id] && Truffler::SmartSearch.find(params[:run_id], user: Current.user, tenant: account.id)
235
+
236
+ render inertia: "Emails/Index", props: {
237
+ emails: -> { serialize(result.records) },
238
+ chips: result.chips,
239
+ invite_row: result.invite_row,
240
+ smart: -> { run&.to_h },
241
+ }
242
+ ```
243
+
244
+ The provider section is part of `smart` (`sections.provider`), so reloading `smart` covers both kinds of ping. To render a bucket, load its ids through your own scope, for example `account.emails.where(id: ids)`, and keep the order the ids arrive in. To route pings through something other than `ActionCable.server`, set `config.broadcaster` to any object that responds to `broadcast(stream, payload)`.
245
+
246
+ ## Provider backup
247
+
248
+ A model can declare one `provider`. Its `search:` callable receives `(query, tenant:, user:)` and returns an array of result hashes. The provider runs during the Smart run under either of two conditions:
249
+
250
+ - **Exact text.** The query contains a quoted phrase, digits, an email address, or an identifier such as `INV-4471`.
251
+ - **Weak local results.** The keystroke results were weak.
252
+
253
+ Its state appears in `run.provider_section`, which has one of the statuses `:absent`, `:pending`, `:results`, `:empty`, or `:unavailable`. Each provider search is scoped to the run's own tenant and user. Results are stored in the cache store for the run's TTL, so return only the fields your UI needs.
254
+
255
+ ## Lenses
256
+
257
+ A lens is a set of drafted label questions for one model and scope: the whole app (`Scope.app`), one tenant (`Scope.tenant(key)`), or one user within a tenant (`Scope.user(tenant_key, user_key)`). A lens's answers are stored as `lens:<id>:<label>` rows in `truffler_labels`. Filters, label vectors, staleness, backfill, and query encoding treat them exactly like declared labels.
258
+
259
+ ```ruby
260
+ Lenses = Truffler::Lenses
261
+ scope = Lenses::Scope.tenant(account.id)
262
+
263
+ draft = Lenses::Drafter.draft("customers asking for a refund", model: Email, scope: scope)
264
+ preview = Lenses::Previewer.preview(draft, relation: account.emails, by: Current.user)
265
+ preview.distribution # {label => {bucket => count}} on a sample of recent records
266
+ preview.examples # up to 5 record ids per bucket
267
+ preview.estimate # records, requests, input_tokens, cost_usd, duration_seconds, within_cap
268
+
269
+ lens = Lenses::Activator.activate(draft, by: Current.user) # enqueues LensBackfillJob
270
+
271
+ version = lens.regenerate(by: Current.user, description: "customers asking for a refund or a credit")
272
+ comparison = Lenses::Previewer.compare(version, lens, relation: account.emails, by: Current.user)
273
+ comparison.shift # per-label bucket and mean movement on the same sample
274
+ Lenses::Activator.activate(version, by: Current.user)
275
+
276
+ lens.restore!(1, by: Current.user) # copies version 1 forward as a new active version
277
+ lens.history # number, status, label keys, reused keys, author digests, and times
278
+ lens.promote! # prints `label ...` lines to paste into the model's declaration
279
+ ```
280
+
281
+ Some rules that apply throughout:
282
+
283
+ - The drafting model sees the description and the existing label vocabulary, never record text.
284
+ - Search keeps using the active version until a new one is activated.
285
+ - Labels written by an earlier version keep serving searches until they are relabeled.
286
+ - Each lens has its own spend cap. Previews run at encode priority and fail with `LensSpendCapExceeded` when the lens would exceed its cap.
287
+ - A lens nobody searches with for `expire_after` expires.
288
+ - `Lenses::Proposer.propose(Email, tenant_key: account.id.to_s)` drafts `proposed` lenses from query-miss clusters when `config.lenses.proposals` is on. A proposed lens changes nothing until someone activates it.
289
+
290
+ Who may change lenses is set by `config.lenses.creators`:
291
+
292
+ - `:developers` (the default) allows app, tenant, and user lenses.
293
+ - `:tenant_users` allows tenant and user lenses.
294
+ - `:each_user` allows only personal lenses, and only for their own user.
295
+
296
+ The `authorize_lens` hook always has the final word. Without it, every change raises `Truffler::NotAuthorized`:
297
+
298
+ ```ruby
299
+ config.lenses.creators = :tenant_users
300
+ config.lenses.authorize_lens = ->(user, scope) { user.admin? || scope.tenant_key == user.account_id.to_s }
301
+ ```
302
+
303
+ For personal lenses, build the scope with the same key your searches use: `Scope.user(account.id, Truffler::Search::Keystroke.user_key(Current.user))`.
304
+
305
+ ## Configuration
306
+
307
+ Set these in `Truffler.configure do |config| ... end`.
308
+
309
+ | Setting | Default | Meaning |
310
+ |---|---|---|
311
+ | `model` | `"jev-latest"` | Jev model pin. Changing it makes every asked label stale; supplied (`from:`) labels are unaffected. |
312
+ | `client` | `Clients::RubyLLMTypeSafe.new` | Jev client (see Clients). |
313
+ | `cache_store` | `Rails.cache` | Budget counters, encodings, Smart runs. Must be shared and support `increment`. |
314
+ | `requests_per_minute` | 1,200 (`TYPESAFE_REQUESTS_PER_MINUTE`) | TypeSafe account limit. |
315
+ | `headroom` | 0.25 | Share of the limit left for your app's other Jev calls. |
316
+ | `priority_ceilings` | `{live: 1.0, encode: 0.9, rerank: 0.75, backfill: 0.5}` | Share of the gem's per-second budget each priority may fill. Lower priorities give way first. |
317
+ | `user_caps` | `{encode: 30, rerank: 10}` | Per-user requests per minute. A denied rerank pauses the run. |
318
+ | `tenant_live_cap` | 120 | Records per tenant per minute labeled live. The rest wait at backfill priority. |
319
+ | `max_wait` | 5.0 | Seconds a live labeling call may wait for a budget slot. |
320
+ | `batch_size`, `grouping_window` | 10, 0 | Records per labeling request and the flush delay. |
321
+ | `max_attempts` | 5 | Labeling attempts per record before it is marked failed. |
322
+ | `max_field_chars`, `request_token_budget`, `max_questions_per_request` | 4,000, 48,000, 200 | Request packing limits. |
323
+ | `queue_name` | `:default` | Queue for every Truffler job. |
324
+ | `cost_per_million_tokens` | 0.042 | Jev input price, used in usage events and estimates. |
325
+ | `backfill_spend_cap` | nil | Dollar cap per backfill run. Supplied labels cost nothing and are still written once it is reached. |
326
+ | `resume_pending_after` | 5 minutes | How long before `ResumeJob` treats work as stuck. |
327
+ | `embedder` | `Embeddings::RubyLLMEmbedder.new` | Any `Embeddings::Embedder` subclass. The default calls `RubyLLM.embed`, which works on ruby_llm 1.x and 2. |
328
+ | `embedding_cost_per_million_tokens` | 0.02 | Embedding price. |
329
+ | `vector_store` | `:auto` | `:neighbor` (pgvector `<=>`, or sqlite-vec `vec_distance_cosine` when you load the extension), `:ruby` (exact cosine in Ruby), or `:auto` (neighbor when available, otherwise Ruby). A model declaring `embeddings column:` always reads its own column. |
330
+ | `encoding_prefetch` | `QueryEncoding::Prefetch.new` | Cache-miss hook, called as `call(model, query, cache_key:, tenant_key:, user_key:)`. |
331
+ | `encoding_deadline` | 1.0 | Seconds a Smart run waits for an in-flight query encoding. |
332
+ | `rerank_depth`, `rerank_chunk_size`, `rerank_max_field_chars` | 30, 10, 1,200 | Candidates reranked, candidates per Jev request, characters per field sent. |
333
+ | `smart_thresholds` | `{strong: 0.70, possible: 0.35}` | Bucket cutoffs on the relevance probability. |
334
+ | `smart_run_ttl`, `smart_candidate_pool` | 15 minutes, 200 | Run lifetime and snapshot size cap. |
335
+ | `broadcaster` | `ActionCable.server` | Ping target. |
336
+ | `miss_retention`, `miss_min_distinct_users` | 30 days, 5 | Miss log retention, and the number of distinct users a miss cluster needs before anyone sees it. |
337
+ | `secret_key_base` | Rails' | Keys user digests, miss digests, and Smart query encryption. |
338
+ | `lenses.creators`, `lenses.authorize_lens`, `lenses.proposals` | `:developers`, nil, false | Lens policy (see Lenses). |
339
+ | `lenses.spend_cap_usd`, `lenses.sample_size`, `lenses.max_questions`, `lenses.expire_after` | 1.0, 20, 8, 30 days | Lens limits. |
340
+ | `lenses.generator`, `lenses.drafter_model`, `lenses.user_key` | `RubyLLMGenerator`, nil, `"User:42"` style | Drafting model seam and user key mapping. |
341
+
342
+ ## Jobs to schedule
343
+
344
+ Truffler enqueues most of its own jobs. Run a worker for `config.queue_name` and schedule these yourself:
345
+
346
+ | Job or task | When |
347
+ |---|---|
348
+ | `Truffler::Jobs::ResumeJob` | Every few minutes. Requeues failed and stuck labeling after an outage or a crashed worker, and enqueues up to 1,000 missing or stale embeddings per model per hour. |
349
+ | `Truffler::Jobs::PruneQueryMissesJob` | Daily. Enforces `miss_retention`. |
350
+ | `Truffler::Jobs::ExpireLensesJob` | Daily. Expires lenses unused for `lenses.expire_after`. |
351
+ | `bin/rails "truffler:backfill[Email]"` (`SPEND_CAP=5`) or `Truffler::Jobs::BackfillJob.perform_later("Email")` | After adopting Truffler, changing a declaration, or changing the model pin. |
352
+ | `Truffler::Embeddings::Backfill.new(Email).enqueue` | After enabling embeddings or changing the embedding model, width, or fields, to re-embed everything now instead of through the `ResumeJob` sweep. It enqueues 1,000 jobs at a time; pass `limit:` to cap the total. |
353
+
354
+ `bin/rails "truffler:status[Email]"` prints labeling counts. `bin/rails "truffler:suggestions[Email]"` prints candidate questions drawn from logged query misses.
355
+
356
+ With Solid Queue, for example:
357
+
358
+ ```yaml
359
+ # config/recurring.yml
360
+ truffler_resume:
361
+ class: Truffler::Jobs::ResumeJob
362
+ schedule: every 5 minutes
363
+ truffler_prune_misses:
364
+ class: Truffler::Jobs::PruneQueryMissesJob
365
+ schedule: every day at 3am
366
+ truffler_expire_lenses:
367
+ class: Truffler::Jobs::ExpireLensesJob
368
+ schedule: every day at 3am
369
+ ```
370
+
371
+ ## Privacy
372
+
373
+ These guarantees hold for every model, and matter most for models that use Active Record encryption:
374
+
375
+ - Decrypted text exists only in process memory during a labeling, embedding, encoding, rerank, or preview call.
376
+ - Tables, job arguments, and logs carry only ids, numbers, and digests. The same goes for `truffler.*` notifications: `jev_call`, `search`, `smart_search`, `embed_call`, `lens_draft`, and the rest.
377
+ - Client errors keep only the error class and HTTP status.
378
+ - On encrypted models, pending query payloads and Smart run queries in the cache are encrypted with a key derived from `secret_key_base`. Search notifications carry a query digest instead of the query.
379
+ - On encrypted models, the miss log and lens descriptions keep Active Record ciphertext when encryption is configured, and nothing otherwise. Miss clusters are shown only once they reach `miss_min_distinct_users`.
380
+ - Embeddings refuse encrypted fields unless you opt in with `allow_encrypted: true`, or declare a `column:` you fill yourself.
381
+ - `keyword` runs `LIKE` against columns, so it cannot see inside encrypted columns. Use `exact` with deterministic encryption for blind-index lookups.
382
+ - A Smart rerank sends only candidates from the snapshot of your scope, one tenant per request, with fields truncated to `rerank_max_field_chars`. Query and record text appear only in the request state, marked as untrusted data.
383
+
384
+ ## Cost and rate limits
385
+
386
+ Jev costs $0.042 per million input tokens. The default account limit is 1,200 requests per minute. With the default 25% headroom, Truffler takes up to 900 requests per minute, or 15 per second, split by priority ceilings in this order: live labeling, then query encoding, then rerank, then backfill.
387
+
388
+ Here is what a typical session consumes:
389
+
390
+ - **Labeling.** Each labeling request packs `batch_size` records from one tenant.
391
+ - **Query encoding.** Each query is encoded once per vocabulary version and then cached.
392
+ - **Smart search.** A run costs `rerank_depth / rerank_chunk_size` requests, 3 by default.
393
+
394
+ Every call emits `truffler.jev_call` with input tokens (estimated and flagged when the client reports none), cost, model, and latency, so you can meter spend yourself. For a higher throughput ceiling, raise `TYPESAFE_REQUESTS_PER_MINUTE` once TypeSafe raises your account limit.
395
+
396
+ ## Benchmark
397
+
398
+ ```bash
399
+ bundle exec rake truffler:bench # replay committed cassettes (CI runs this)
400
+ bundle exec rake truffler:bench MODE=synthetic BENCH_RECORDS=5000
401
+ bundle exec rake truffler:bench PARAMS=bench/params.yml OUT=tmp/bench.json
402
+ TYPESAFE_API_KEY=... bundle exec rake truffler:bench MODE=record JEV=live
403
+ ```
404
+
405
+ The benchmark loads fixtures into its own SQLite database and labels them through the real pipeline. It prints a JSON report with the following:
406
+
407
+ - Recall and precision for intent and exact-text queries.
408
+ - Keystroke p50 and p95 latency.
409
+ - Labeling throughput.
410
+ - Cost per labeled record and per query.
411
+ - Packed-batch agreement and the adopted batch size.
412
+ - Prompt-injection checks for labels and rerank.
413
+ - Rerank bucket counts.
414
+
415
+ It exits non-zero when a check fails. `bench/params.yml` holds the tunable thresholds, boosts, and blend weights.
416
+
417
+ ## Development
418
+
419
+ ```bash
420
+ bundle install
421
+ bundle exec rake test
422
+ bundle exec rubocop
423
+ ```
424
+
425
+ Tests run on in-memory SQLite with fake clients. Any live Jev call raises `Truffler::LiveCallInTest`.
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"billing","confidence":0.8225,"probabilities":{"billing":0.8225,"newsletter":0.0444,"personal":0.0444,"travel":0.0444,"work":0.0444},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r001__needs_action":{"noul":0.0573,"type":"noul"},"r001__urgent":{"noul":0.0257,"type":"noul"}},"model":"synthetic-jev","request_hash":"004cad52c71fc8d847bf388eb9c2c288e1169b65ec4a28ce040cb7fea7c55f04","usage":{"input_tokens":436}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"personal","confidence":0.7978,"probabilities":{"billing":0.0506,"newsletter":0.0506,"personal":0.7978,"travel":0.0506,"work":0.0506},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r001__needs_action":{"noul":0.8113,"type":"noul"},"r001__urgent":{"noul":0.3778,"type":"noul"},"r002__category":{"choice":"work","confidence":0.8259,"probabilities":{"billing":0.0435,"newsletter":0.0435,"personal":0.0435,"travel":0.0435,"work":0.8259},"type":"choice"},"r002__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r002__needs_action":{"noul":0.8905,"type":"noul"},"r002__urgent":{"noul":0.7213,"type":"noul"},"r003__category":{"choice":"billing","confidence":0.7929,"probabilities":{"billing":0.7929,"newsletter":0.0518,"personal":0.0518,"travel":0.0518,"work":0.0518},"type":"choice"},"r003__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r003__needs_action":{"noul":0.891,"type":"noul"},"r003__urgent":{"noul":0.4753,"type":"noul"},"r004__category":{"choice":"work","confidence":0.8143,"probabilities":{"billing":0.0464,"newsletter":0.0464,"personal":0.0464,"travel":0.0464,"work":0.8143},"type":"choice"},"r004__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r004__needs_action":{"noul":0.1103,"type":"noul"},"r004__urgent":{"noul":0.024,"type":"noul"},"r005__category":{"choice":"work","confidence":0.8285,"probabilities":{"billing":0.0429,"newsletter":0.0429,"personal":0.0429,"travel":0.0429,"work":0.8285},"type":"choice"},"r005__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r005__needs_action":{"noul":0.1037,"type":"noul"},"r005__urgent":{"noul":0.0564,"type":"noul"},"r006__category":{"choice":"personal","confidence":0.7725,"probabilities":{"billing":0.0569,"newsletter":0.0569,"personal":0.7725,"travel":0.0569,"work":0.0569},"type":"choice"},"r006__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r006__needs_action":{"noul":0.7797,"type":"noul"},"r006__urgent":{"noul":0.3717,"type":"noul"},"r007__category":{"choice":"travel","confidence":0.7932,"probabilities":{"billing":0.0517,"newsletter":0.0517,"personal":0.0517,"travel":0.7932,"work":0.0517},"type":"choice"},"r007__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r007__needs_action":{"noul":0.1218,"type":"noul"},"r007__urgent":{"noul":0.1914,"type":"noul"},"r008__category":{"choice":"travel","confidence":0.8153,"probabilities":{"billing":0.0462,"newsletter":0.0462,"personal":0.0462,"travel":0.8153,"work":0.0462},"type":"choice"},"r008__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r008__needs_action":{"noul":0.8375,"type":"noul"},"r008__urgent":{"noul":0.9082,"type":"noul"},"r009__category":{"choice":"travel","confidence":0.812,"probabilities":{"billing":0.047,"newsletter":0.047,"personal":0.047,"travel":0.812,"work":0.047},"type":"choice"},"r009__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r009__needs_action":{"noul":0.8489,"type":"noul"},"r009__urgent":{"noul":0.8866,"type":"noul"},"r010__category":{"choice":"work","confidence":0.8034,"probabilities":{"billing":0.0492,"newsletter":0.0492,"personal":0.0492,"travel":0.0492,"work":0.8034},"type":"choice"},"r010__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r010__needs_action":{"noul":0.8752,"type":"noul"},"r010__urgent":{"noul":0.6943,"type":"noul"}},"model":"synthetic-jev","request_hash":"0547d3d23c3fbaae86772ffa0e4e96475aa152a2556b8fa4bc7568effcb81a28","usage":{"input_tokens":3531}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"travel","confidence":0.8125,"probabilities":{"billing":0.0469,"newsletter":0.0469,"personal":0.0469,"travel":0.8125,"work":0.0469},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r001__needs_action":{"noul":0.8712,"type":"noul"},"r001__urgent":{"noul":0.8741,"type":"noul"}},"model":"synthetic-jev","request_hash":"07aa23d166ab3500178e3f3c0838183051b093507bef88e1e2d29efdf5759ee6","usage":{"input_tokens":440}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"billing","confidence":0.8192,"probabilities":{"billing":0.8192,"newsletter":0.0452,"personal":0.0452,"travel":0.0452,"work":0.0452},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r001__needs_action":{"noul":0.9025,"type":"noul"},"r001__urgent":{"noul":0.8583,"type":"noul"},"r002__category":{"choice":"billing","confidence":0.7741,"probabilities":{"billing":0.7741,"newsletter":0.0565,"personal":0.0565,"travel":0.0565,"work":0.0565},"type":"choice"},"r002__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r002__needs_action":{"noul":0.065,"type":"noul"},"r002__urgent":{"noul":0.049,"type":"noul"},"r003__category":{"choice":"travel","confidence":0.8219,"probabilities":{"billing":0.0445,"newsletter":0.0445,"personal":0.0445,"travel":0.8219,"work":0.0445},"type":"choice"},"r003__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r003__needs_action":{"noul":0.1646,"type":"noul"},"r003__urgent":{"noul":0.1702,"type":"noul"},"r004__category":{"choice":"travel","confidence":0.8075,"probabilities":{"billing":0.0481,"newsletter":0.0481,"personal":0.0481,"travel":0.8075,"work":0.0481},"type":"choice"},"r004__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r004__needs_action":{"noul":0.8251,"type":"noul"},"r004__urgent":{"noul":0.8935,"type":"noul"},"r005__category":{"choice":"work","confidence":0.8234,"probabilities":{"billing":0.0441,"newsletter":0.0441,"personal":0.0441,"travel":0.0441,"work":0.8234},"type":"choice"},"r005__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r005__needs_action":{"noul":0.888,"type":"noul"},"r005__urgent":{"noul":0.6915,"type":"noul"},"r006__category":{"choice":"work","confidence":0.7839,"probabilities":{"billing":0.054,"newsletter":0.054,"personal":0.054,"travel":0.054,"work":0.7839},"type":"choice"},"r006__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r006__needs_action":{"noul":0.1048,"type":"noul"},"r006__urgent":{"noul":0.0444,"type":"noul"},"r007__category":{"choice":"personal","confidence":0.7725,"probabilities":{"billing":0.0569,"newsletter":0.0569,"personal":0.7725,"travel":0.0569,"work":0.0569},"type":"choice"},"r007__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r007__needs_action":{"noul":0.7797,"type":"noul"},"r007__urgent":{"noul":0.3717,"type":"noul"},"r008__category":{"choice":"personal","confidence":0.7886,"probabilities":{"billing":0.0529,"newsletter":0.0529,"personal":0.7886,"travel":0.0529,"work":0.0529},"type":"choice"},"r008__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r008__needs_action":{"noul":0.0428,"type":"noul"},"r008__urgent":{"noul":0.0375,"type":"noul"},"r009__category":{"choice":"newsletter","confidence":0.8016,"probabilities":{"billing":0.0496,"newsletter":0.8016,"personal":0.0496,"travel":0.0496,"work":0.0496},"type":"choice"},"r009__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r009__needs_action":{"noul":0.0394,"type":"noul"},"r009__urgent":{"noul":0.0,"type":"noul"},"r010__category":{"choice":"billing","confidence":0.7726,"probabilities":{"billing":0.7726,"newsletter":0.0569,"personal":0.0569,"travel":0.0569,"work":0.0569},"type":"choice"},"r010__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r010__needs_action":{"noul":0.8982,"type":"noul"},"r010__urgent":{"noul":0.8671,"type":"noul"}},"model":"synthetic-jev","request_hash":"0858c5006c0bfac3e970ddaff3c139f6c110dbe3d8539fd95263706c637cdc8e","usage":{"input_tokens":3521}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"personal","confidence":0.8027,"probabilities":{"billing":0.0493,"newsletter":0.0493,"personal":0.8027,"travel":0.0493,"work":0.0493},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r001__needs_action":{"noul":0.8213,"type":"noul"},"r001__urgent":{"noul":0.4195,"type":"noul"}},"model":"synthetic-jev","request_hash":"0c0eb7c27d76bd0b9263690a63cd92d6b68c305826318555fd5b084a09970b78","usage":{"input_tokens":430}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"work","confidence":0.7918,"probabilities":{"billing":0.0521,"newsletter":0.0521,"personal":0.0521,"travel":0.0521,"work":0.7918},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r001__needs_action":{"noul":0.0878,"type":"noul"},"r001__urgent":{"noul":0.034,"type":"noul"},"r002__category":{"choice":"personal","confidence":0.7842,"probabilities":{"billing":0.054,"newsletter":0.054,"personal":0.7842,"travel":0.054,"work":0.054},"type":"choice"},"r002__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r002__needs_action":{"noul":0.7832,"type":"noul"},"r002__urgent":{"noul":0.3762,"type":"noul"},"r003__category":{"choice":"personal","confidence":0.8173,"probabilities":{"billing":0.0457,"newsletter":0.0457,"personal":0.8173,"travel":0.0457,"work":0.0457},"type":"choice"},"r003__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r003__needs_action":{"noul":0.4445,"type":"noul"},"r003__urgent":{"noul":0.0372,"type":"noul"},"r004__category":{"choice":"newsletter","confidence":0.8104,"probabilities":{"billing":0.0474,"newsletter":0.8104,"personal":0.0474,"travel":0.0474,"work":0.0474},"type":"choice"},"r004__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r004__needs_action":{"noul":0.0467,"type":"noul"},"r004__urgent":{"noul":0.0067,"type":"noul"},"r005__category":{"choice":"personal","confidence":0.7808,"probabilities":{"billing":0.0548,"newsletter":0.0548,"personal":0.7808,"travel":0.0548,"work":0.0548},"type":"choice"},"r005__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r005__needs_action":{"noul":0.8272,"type":"noul"},"r005__urgent":{"noul":0.3827,"type":"noul"}},"model":"synthetic-jev","request_hash":"0fada0c0a103882e2895f7f455d73d2cbe2b4a81bf743bb19b2b4f6e0130126b","usage":{"input_tokens":1779}}
@@ -0,0 +1 @@
1
+ {"answers":{"c001__relevance":{"noul":0.0,"type":"noul"},"c002__relevance":{"noul":0.0,"type":"noul"}},"model":"synthetic-jev","request_hash":"1117d79ac7c43b21beaf75e510461f91cd4ecc3cdbbe461f1794644af4aaf8f3","usage":{"input_tokens":436}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"work","confidence":0.7904,"probabilities":{"billing":0.0524,"newsletter":0.0524,"personal":0.0524,"travel":0.0524,"work":0.7904},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r001__needs_action":{"noul":0.8789,"type":"noul"},"r001__urgent":{"noul":0.6915,"type":"noul"},"r002__category":{"choice":"work","confidence":0.8104,"probabilities":{"billing":0.0474,"newsletter":0.0474,"personal":0.0474,"travel":0.0474,"work":0.8104},"type":"choice"},"r002__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r002__needs_action":{"noul":0.9278,"type":"noul"},"r002__urgent":{"noul":0.7102,"type":"noul"},"r003__category":{"choice":"work","confidence":0.8266,"probabilities":{"billing":0.0434,"newsletter":0.0434,"personal":0.0434,"travel":0.0434,"work":0.8266},"type":"choice"},"r003__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r003__needs_action":{"noul":0.8918,"type":"noul"},"r003__urgent":{"noul":0.6986,"type":"noul"},"r004__category":{"choice":"personal","confidence":0.8208,"probabilities":{"billing":0.0448,"newsletter":0.0448,"personal":0.8208,"travel":0.0448,"work":0.0448},"type":"choice"},"r004__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r004__needs_action":{"noul":0.0408,"type":"noul"},"r004__urgent":{"noul":0.0458,"type":"noul"},"r005__category":{"choice":"personal","confidence":0.7967,"probabilities":{"billing":0.0508,"newsletter":0.0508,"personal":0.7967,"travel":0.0508,"work":0.0508},"type":"choice"},"r005__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r005__needs_action":{"noul":0.0645,"type":"noul"},"r005__urgent":{"noul":0.0284,"type":"noul"},"r006__category":{"choice":"travel","confidence":0.7933,"probabilities":{"billing":0.0517,"newsletter":0.0517,"personal":0.0517,"travel":0.7933,"work":0.0517},"type":"choice"},"r006__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r006__needs_action":{"noul":0.1244,"type":"noul"},"r006__urgent":{"noul":0.1961,"type":"noul"},"r007__category":{"choice":"personal","confidence":0.7742,"probabilities":{"billing":0.0565,"newsletter":0.0565,"personal":0.7742,"travel":0.0565,"work":0.0565},"type":"choice"},"r007__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r007__needs_action":{"noul":0.8159,"type":"noul"},"r007__urgent":{"noul":0.4012,"type":"noul"},"r008__category":{"choice":"travel","confidence":0.7855,"probabilities":{"billing":0.0536,"newsletter":0.0536,"personal":0.0536,"travel":0.7855,"work":0.0536},"type":"choice"},"r008__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r008__needs_action":{"noul":0.8406,"type":"noul"},"r008__urgent":{"noul":0.8831,"type":"noul"},"r009__category":{"choice":"billing","confidence":0.7992,"probabilities":{"billing":0.7992,"newsletter":0.0502,"personal":0.0502,"travel":0.0502,"work":0.0502},"type":"choice"},"r009__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r009__needs_action":{"noul":0.9102,"type":"noul"},"r009__urgent":{"noul":0.8601,"type":"noul"},"r010__category":{"choice":"travel","confidence":0.8016,"probabilities":{"billing":0.0496,"newsletter":0.0496,"personal":0.0496,"travel":0.8016,"work":0.0496},"type":"choice"},"r010__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r010__needs_action":{"noul":0.8473,"type":"noul"},"r010__urgent":{"noul":0.9068,"type":"noul"}},"model":"synthetic-jev","request_hash":"11b7444a074cca4d0ef1535db0cf0edefe4c34eac4be3c593c9c2f33d3b038b8","usage":{"input_tokens":3511}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"personal","confidence":0.7831,"probabilities":{"billing":0.0542,"newsletter":0.0542,"personal":0.7831,"travel":0.0542,"work":0.0542},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r001__needs_action":{"noul":0.0485,"type":"noul"},"r001__urgent":{"noul":0.0715,"type":"noul"}},"model":"synthetic-jev","request_hash":"11d00371d1b993675be00463159164536f1f8ed90a48652b0935eb9c36f8b684","usage":{"input_tokens":425}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"personal","confidence":0.7831,"probabilities":{"billing":0.0542,"newsletter":0.0542,"personal":0.7831,"travel":0.0542,"work":0.0542},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r001__needs_action":{"noul":0.0485,"type":"noul"},"r001__urgent":{"noul":0.0715,"type":"noul"}},"model":"synthetic-jev","request_hash":"1456ebe12616ce961d68a14eada62778a7271ebd3d1b4cb6eefd6857651c7afc","usage":{"input_tokens":424}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"personal","confidence":0.7896,"probabilities":{"billing":0.0526,"newsletter":0.0526,"personal":0.7896,"travel":0.0526,"work":0.0526},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r001__needs_action":{"noul":0.82,"type":"noul"},"r001__urgent":{"noul":0.3702,"type":"noul"},"r002__category":{"choice":"work","confidence":0.7839,"probabilities":{"billing":0.054,"newsletter":0.054,"personal":0.054,"travel":0.054,"work":0.7839},"type":"choice"},"r002__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r002__needs_action":{"noul":0.1048,"type":"noul"},"r002__urgent":{"noul":0.0444,"type":"noul"},"r003__category":{"choice":"personal","confidence":0.8208,"probabilities":{"billing":0.0448,"newsletter":0.0448,"personal":0.8208,"travel":0.0448,"work":0.0448},"type":"choice"},"r003__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r003__needs_action":{"noul":0.0408,"type":"noul"},"r003__urgent":{"noul":0.0458,"type":"noul"},"r004__category":{"choice":"billing","confidence":0.7833,"probabilities":{"billing":0.7833,"newsletter":0.0542,"personal":0.0542,"travel":0.0542,"work":0.0542},"type":"choice"},"r004__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":1.0,"1":0.0,"2":0.0},"score":0,"type":"score"},"r004__needs_action":{"noul":0.0336,"type":"noul"},"r004__urgent":{"noul":0.0657,"type":"noul"},"r005__category":{"choice":"personal","confidence":0.7725,"probabilities":{"billing":0.0569,"newsletter":0.0569,"personal":0.7725,"travel":0.0569,"work":0.0569},"type":"choice"},"r005__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r005__needs_action":{"noul":0.7797,"type":"noul"},"r005__urgent":{"noul":0.3717,"type":"noul"},"r006__category":{"choice":"billing","confidence":0.8193,"probabilities":{"billing":0.8193,"newsletter":0.0452,"personal":0.0452,"travel":0.0452,"work":0.0452},"type":"choice"},"r006__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r006__needs_action":{"noul":0.5286,"type":"noul"},"r006__urgent":{"noul":0.223,"type":"noul"},"r007__category":{"choice":"work","confidence":0.8234,"probabilities":{"billing":0.0441,"newsletter":0.0441,"personal":0.0441,"travel":0.0441,"work":0.8234},"type":"choice"},"r007__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r007__needs_action":{"noul":0.888,"type":"noul"},"r007__urgent":{"noul":0.6915,"type":"noul"},"r008__category":{"choice":"work","confidence":0.8163,"probabilities":{"billing":0.0459,"newsletter":0.0459,"personal":0.0459,"travel":0.0459,"work":0.8163},"type":"choice"},"r008__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r008__needs_action":{"noul":0.1228,"type":"noul"},"r008__urgent":{"noul":0.0797,"type":"noul"},"r009__category":{"choice":"travel","confidence":0.785,"probabilities":{"billing":0.0537,"newsletter":0.0537,"personal":0.0537,"travel":0.785,"work":0.0537},"type":"choice"},"r009__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r009__needs_action":{"noul":0.1577,"type":"noul"},"r009__urgent":{"noul":0.2185,"type":"noul"},"r010__category":{"choice":"work","confidence":0.7812,"probabilities":{"billing":0.0547,"newsletter":0.0547,"personal":0.0547,"travel":0.0547,"work":0.7812},"type":"choice"},"r010__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r010__needs_action":{"noul":0.8861,"type":"noul"},"r010__urgent":{"noul":0.6774,"type":"noul"}},"model":"synthetic-jev","request_hash":"14c3693f64b0e9ea7e470277fccae6ab501d404185eec5bb9074ef4b75c572c0","usage":{"input_tokens":3515}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"work","confidence":0.8121,"probabilities":{"billing":0.047,"newsletter":0.047,"personal":0.047,"travel":0.047,"work":0.8121},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":1.0,"2":0.0},"score":1,"type":"score"},"r001__needs_action":{"noul":0.0957,"type":"noul"},"r001__urgent":{"noul":0.0274,"type":"noul"}},"model":"synthetic-jev","request_hash":"14fe9f6d8055d41f5d970dc32ab640c213ea8782d365c82d32b9884ba4f28c67","usage":{"input_tokens":437}}
@@ -0,0 +1 @@
1
+ {"answers":{"r001__category":{"choice":"billing","confidence":0.7922,"probabilities":{"billing":0.7922,"newsletter":0.052,"personal":0.052,"travel":0.052,"work":0.052},"type":"choice"},"r001__importance":{"confidence":1.0,"legend":{"0":"0","1":"1","2":"2"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"score":2,"type":"score"},"r001__needs_action":{"noul":0.8768,"type":"noul"},"r001__urgent":{"noul":0.2418,"type":"noul"}},"model":"synthetic-jev","request_hash":"1a1d8557e4698426c2467aa573a240357715d3d9e3fa77bf066672a8b6bf9a86","usage":{"input_tokens":444}}
@@ -0,0 +1 @@
1
+ {"answers":{"c001__relevance":{"noul":0.0,"type":"noul"},"c002__relevance":{"noul":0.0,"type":"noul"}},"model":"synthetic-jev","request_hash":"1b5a8aaaf98305560588246cd51f53c6c185a951f8018c26bf588954d9de1cea","usage":{"input_tokens":438}}