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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +425 -0
- data/bench/cassettes/004cad52c71fc8d847bf388eb9c2c288e1169b65ec4a28ce040cb7fea7c55f04.json +1 -0
- data/bench/cassettes/0547d3d23c3fbaae86772ffa0e4e96475aa152a2556b8fa4bc7568effcb81a28.json +1 -0
- data/bench/cassettes/07aa23d166ab3500178e3f3c0838183051b093507bef88e1e2d29efdf5759ee6.json +1 -0
- data/bench/cassettes/0858c5006c0bfac3e970ddaff3c139f6c110dbe3d8539fd95263706c637cdc8e.json +1 -0
- data/bench/cassettes/0c0eb7c27d76bd0b9263690a63cd92d6b68c305826318555fd5b084a09970b78.json +1 -0
- data/bench/cassettes/0fada0c0a103882e2895f7f455d73d2cbe2b4a81bf743bb19b2b4f6e0130126b.json +1 -0
- data/bench/cassettes/1117d79ac7c43b21beaf75e510461f91cd4ecc3cdbbe461f1794644af4aaf8f3.json +1 -0
- data/bench/cassettes/11b7444a074cca4d0ef1535db0cf0edefe4c34eac4be3c593c9c2f33d3b038b8.json +1 -0
- data/bench/cassettes/11d00371d1b993675be00463159164536f1f8ed90a48652b0935eb9c36f8b684.json +1 -0
- data/bench/cassettes/1456ebe12616ce961d68a14eada62778a7271ebd3d1b4cb6eefd6857651c7afc.json +1 -0
- data/bench/cassettes/14c3693f64b0e9ea7e470277fccae6ab501d404185eec5bb9074ef4b75c572c0.json +1 -0
- data/bench/cassettes/14fe9f6d8055d41f5d970dc32ab640c213ea8782d365c82d32b9884ba4f28c67.json +1 -0
- data/bench/cassettes/1a1d8557e4698426c2467aa573a240357715d3d9e3fa77bf066672a8b6bf9a86.json +1 -0
- data/bench/cassettes/1b5a8aaaf98305560588246cd51f53c6c185a951f8018c26bf588954d9de1cea.json +1 -0
- data/bench/cassettes/1fe596be747e33e9c7e4d7c50de32613957ac66bee43679b4bed96c551b1fd0a.json +1 -0
- data/bench/cassettes/21ebce6db6d39b1747dccf25bd3ceb298e1285425190b4d78830c395a7ad786a.json +1 -0
- data/bench/cassettes/23066253621e9cb00fd93cbe95ef1124ff0715585fcb6325b3abbd0802b2c303.json +1 -0
- data/bench/cassettes/2f6576325dfab696cec8d8b35fa89c29960bee15d10dab31c91cef507f4842be.json +1 -0
- data/bench/cassettes/33ec27d22c0f63dbbe93008f50af07fa8667a5c806a9fbfc7897683915412b40.json +1 -0
- data/bench/cassettes/351f745b0c47e0cf3272b0fea7fc4941b6aa733f013c55a9a19707b542543200.json +1 -0
- data/bench/cassettes/37d974fdc0c1876413111c244d363c8d2306e3229fec668c158b5c924ffdb983.json +1 -0
- data/bench/cassettes/39524467463dd612cbf03e314b9cfd8cbe1c5f06969da2289359cf7cf83de5da.json +1 -0
- data/bench/cassettes/3f2c758c111ab7f1163ba579f2329718afe15620507d356d3ebe2a053df237ea.json +1 -0
- data/bench/cassettes/44bbc3a3f0ceb0edb443b080b939af80dd145795505bd13cac113a35365d0f04.json +1 -0
- data/bench/cassettes/45bf701a13c348acb45a3b3a56990570e3caf797c3daee5d1358a477593ce587.json +1 -0
- data/bench/cassettes/4676a0f2df29493fdfa0b7cd75c097e647373862d88373dc0e1fa9fca26707c2.json +1 -0
- data/bench/cassettes/49708a50e3c78f45beed2f1b8d8d5375b99ea4ab3497fed7650e6ece4ec81ba8.json +1 -0
- data/bench/cassettes/54ea30ecca8fa49f607d5c8265296eb16edb48d0620a7399c6480d4cb0f43ae1.json +1 -0
- data/bench/cassettes/550e73a8291f53d0f2b008f166560eff4d10cc7c09a927d3c4f70544f2578300.json +1 -0
- data/bench/cassettes/570039823d5af1b6c1d4bcbe14f9802d40e851f8e4946278a46884c76c0ceee1.json +1 -0
- data/bench/cassettes/5ae9dce82fa7b9084b592f657a24acfd5852c4fcb091a6a8e22f2cbbf947bcdd.json +1 -0
- data/bench/cassettes/5c40794d3bf6e229c6ffd26922388776121f971ef27279e02a35ce004e0794dd.json +1 -0
- data/bench/cassettes/62f2e71a2d9cfdc18fdd6600f5a1450519b25a75c2f799b7695258627250497d.json +1 -0
- data/bench/cassettes/642becd7ad9cd4cc2ac6d389ef8ff14fd9fe125beca834afee625ea2c31476be.json +1 -0
- data/bench/cassettes/656cc1158890e928c129d9cd54c1e08af1b6a2c0f6f94bacf395b13a198dc81f.json +1 -0
- data/bench/cassettes/67b30efd5681046fb5f2252d73f9aedf603be1b7c7895e4e71c6cfb88238309e.json +1 -0
- data/bench/cassettes/691958dd39537d72c4b0ab4ba69fea3c51b8e06a49a772f3ffd5bb5747dfe99d.json +1 -0
- data/bench/cassettes/6931e51f0455b5e3260165aa22a445d3a4f782ae5233a961384149cfbb045fe0.json +1 -0
- data/bench/cassettes/6b79cf1a80fc9edd09bb4606613ff2de2ab2bc61487983dffac1174025270997.json +1 -0
- data/bench/cassettes/6d683884d5d039db072ac18c9420bdcc989838f08a71096076cccc2edcd48234.json +1 -0
- data/bench/cassettes/6df49dc7eae4104adf1b020fd846d893da3e49f4f13a4c6fd1f87a78c410ba31.json +1 -0
- data/bench/cassettes/75cc5d99bbc1d48ace6095d41daae1e2e2c829291f01d40f0ce40d6eea52c0bf.json +1 -0
- data/bench/cassettes/771a86cbb6c025101384a50d01a4745683e1c2b17bbfff73e40e475dbf016efb.json +1 -0
- data/bench/cassettes/7a2a93c357473f4a289a6ec250e3bfe6f51152a0c448e5420ffebee7872f9d11.json +1 -0
- data/bench/cassettes/7a4774a02c3eebe24d4fbd443ea90661711fe5cb8248bce7a2dedfedf6ddbc03.json +1 -0
- data/bench/cassettes/7c1fa04f8eb43251ca3f6652977b698e4e6ba5e120da8301b4dbce79ef01ccb9.json +1 -0
- data/bench/cassettes/7c4dea48c2bc2c413da81307ce3cbb2749b20e16e4d831fca5861decf11ec55b.json +1 -0
- data/bench/cassettes/7dd499da389eaeeb59eba533695c20fd1a43e25f89f21e3456b826f9953b2010.json +1 -0
- data/bench/cassettes/80438d0bbfe106baa236eef358e46260693e6491f67e7a9f8ea1076c6fbfd449.json +1 -0
- data/bench/cassettes/80db562534181514f243ac74d7ddaec1fafc28b2b72ae35482cfc2de8f2d9e61.json +1 -0
- data/bench/cassettes/825a51543665a468a55c823788c68eb5785c90be8131988f72bf98f4f0294177.json +1 -0
- data/bench/cassettes/83d8276c227b73e542bd70c023d9a8ffa1c2cf4dd78e71d61457f097bc743d19.json +1 -0
- data/bench/cassettes/891405ffd7727b6d4c4186e2c60a08aa8b62c7b711bd1558c8033fcf8e4178a4.json +1 -0
- data/bench/cassettes/89fabff05dc4c0abdf630781a324b7802cbc0dc8bd09b20ce59643465a2aeaec.json +1 -0
- data/bench/cassettes/8cbceece3ca6136b6c6bb71c9bf28f409a5fa070596ebfc0f3187aba287bf6d3.json +1 -0
- data/bench/cassettes/8d56da27350d9dea20468eae30fa51cbbb10637c06d977b3e1a00734966be46f.json +1 -0
- data/bench/cassettes/8fb43b452b5e15248f34bf48e41cfc227129cfe349fe4b3fc4ab7133d66914bc.json +1 -0
- data/bench/cassettes/92e5426bd0c1f31d7acca92f77d7f2586e74054467729f0be28d139f515354d5.json +1 -0
- data/bench/cassettes/938be225155e1952b817652778b7cab377017fc4a219628ae89ad01a9c1151bc.json +1 -0
- data/bench/cassettes/9519ad701b28e7b2c5f1e1cad5df1ec3e5254c8cec969b9bee1361af8803d39d.json +1 -0
- data/bench/cassettes/9536e8ed68330eda400d765c3ca16a4dd3bfe0d026b569599a33e7e4951f4860.json +1 -0
- data/bench/cassettes/99c6e5aefd97722f4028bacc11d299eb9036805cd5cf7bf75f879ebe3f27c00a.json +1 -0
- data/bench/cassettes/9cdfd2f3ef8dc4cde917a9d196e3137159067c10ec8d2132f32da034865f9959.json +1 -0
- data/bench/cassettes/a1e3030984d4e1982fb5a2b2068664187358c88c6b71f768841d3e78406e9535.json +1 -0
- data/bench/cassettes/a2140f51b8fa25da73efd387a41f9eb84ffbc1f1ab47dec0c3b37b249ff373a0.json +1 -0
- data/bench/cassettes/a36d248ce5c26fd66430b780c8ecdd176f29830ac4edb2bf0b2e07cb93ffb865.json +1 -0
- data/bench/cassettes/a64329816ca1e537e90b89862a86be119e1e6236c6f3cf0141a96505b0734364.json +1 -0
- data/bench/cassettes/aba24c67dcaefa51c204a529b5ea3fe01333181a608a8ab8899568c731677e7d.json +1 -0
- data/bench/cassettes/aed233524926f2b4ff8c2ec21224bd284595c91eb57e0fb6981cdc8ac7e7d8d3.json +1 -0
- data/bench/cassettes/b00e9da6403e48aa1f4f9346419f88b2a336e1a2ac4a28515d84b2337eb777b2.json +1 -0
- data/bench/cassettes/b02f4c76c073882ff0fd12fddc28e4db52d9e6486a130194bf1057c7693de831.json +1 -0
- data/bench/cassettes/b6a9cb89ae4afd08a6919eaf7f609790c895d4cf22d47dad6c3b287b9278be87.json +1 -0
- data/bench/cassettes/b71b982225eab48d02c81913877571dfa53717c0983d1d72cbe3787242dda055.json +1 -0
- data/bench/cassettes/bda5e115b566e7f955aa45222859fe4f725949d5f91e3d794821701491448510.json +1 -0
- data/bench/cassettes/c0ac703268faf8d49dbc1ad0e877693a9f52181ae6070de844fcd4218d33d5e4.json +1 -0
- data/bench/cassettes/c0b57bcbb4004edd44c2d8c7e1f8cfc3567d954a3d5b21b2cda7257e3bc07437.json +1 -0
- data/bench/cassettes/d1ade9adde20981211c470a77b4d560264cae290941e692be6876a79227e3d18.json +1 -0
- data/bench/cassettes/d28de3038544a5bf9c8425b6976e360fae71c5ca96409784ad6642703701986a.json +1 -0
- data/bench/cassettes/d6418acb9f7f58ffdf3049481cbd17cfb3f0581064e157c9c58c19a3fc26b4d4.json +1 -0
- data/bench/cassettes/d71e8cf7c2be4b6e4089eb40c5a108c0c45fc58398af5138f905129f640afbb8.json +1 -0
- data/bench/cassettes/d9672c45b5358b92f7740abfb20b287235cf949990c88558f41d3a9c00eb6d3b.json +1 -0
- data/bench/cassettes/da0c9ce695111f6acf3cfb58dc0494f79230739d1773377db420bf438fd2e244.json +1 -0
- data/bench/cassettes/db57e3a15971cd2832313172c0d7df6e6a48b13a276ba9877eeb7d39389d74d3.json +1 -0
- data/bench/cassettes/db9c8c4d0a8d855a2578572afe5edc5eebb39c0fd5f04489614ea2557ec4f0cd.json +1 -0
- data/bench/cassettes/e2c396b0475a07a615dde9208b088e9e479720c1ec5889ade07fbfd0d28fb517.json +1 -0
- data/bench/cassettes/e3006ccc97646f43f4ea23694665526e47982fa1ea413359b9669071f6cb990d.json +1 -0
- data/bench/cassettes/e623737636e797d22f80d128cbff46101837044e159f41fa62a8d3d0cc6bb996.json +1 -0
- data/bench/cassettes/ea0cefaaac1a62e8eac962391df025a3b14c3a3d5b69fe57d60b65811010908c.json +1 -0
- data/bench/cassettes/ee3e06456cf4201ab46f579dd53bdea4071ce43bfe02e9b870bcb4b9c1c680b4.json +1 -0
- data/bench/cassettes/f26e675da9088f08cb76dd6b3fd89230b84fd1b9be3767c36857217bda2da15b.json +1 -0
- data/bench/cassettes/f396f34b2356d42af385eac9f5760e97e553a584c3f93da7bd618ad311e04d93.json +1 -0
- data/bench/cassettes/fce02c5c04cb8c27e868f6c54cfcbbbf69bde2f7f612d50ff8fb6d0aec9df3e3.json +1 -0
- data/bench/cassettes/ffc09a04b083822c1597dd96bb27ee4cde365bc8346175ebf6e378ab2ab1b598.json +1 -0
- data/bench/cassettes/manifest.json +1 -0
- data/bench/fixtures/gold.jsonl +27 -0
- data/bench/fixtures/injection.jsonl +6 -0
- data/bench/fixtures/records.jsonl +90 -0
- data/bench/params.yml +30 -0
- data/lib/generators/truffler/install/install_generator.rb +51 -0
- data/lib/generators/truffler/install/templates/channel.rb.tt +17 -0
- data/lib/generators/truffler/install/templates/initializer.rb.tt +23 -0
- data/lib/generators/truffler/install/templates/migration.rb.tt +109 -0
- data/lib/tasks/truffler/bench.rake +30 -0
- data/lib/tasks/truffler/suggestions.rake +8 -0
- data/lib/tasks/truffler.rake +26 -0
- data/lib/truffler/answers.rb +66 -0
- data/lib/truffler/benchmark/adapters.rb +80 -0
- data/lib/truffler/benchmark/database.rb +31 -0
- data/lib/truffler/benchmark/dataset.rb +65 -0
- data/lib/truffler/benchmark/email.rb +50 -0
- data/lib/truffler/benchmark/generator.rb +188 -0
- data/lib/truffler/benchmark/metrics.rb +62 -0
- data/lib/truffler/benchmark/params.rb +91 -0
- data/lib/truffler/benchmark/runner.rb +370 -0
- data/lib/truffler/benchmark/synthetic_jev.rb +85 -0
- data/lib/truffler/benchmark.rb +29 -0
- data/lib/truffler/broadcaster.rb +46 -0
- data/lib/truffler/budget.rb +118 -0
- data/lib/truffler/canonical.rb +27 -0
- data/lib/truffler/clients/base.rb +49 -0
- data/lib/truffler/clients/callable.rb +26 -0
- data/lib/truffler/clients/cassette.rb +44 -0
- data/lib/truffler/clients/fake.rb +85 -0
- data/lib/truffler/clients/ruby_llm_typesafe.rb +32 -0
- data/lib/truffler/configuration.rb +89 -0
- data/lib/truffler/definition.rb +176 -0
- data/lib/truffler/embeddings/backfill.rb +48 -0
- data/lib/truffler/embeddings/column_store.rb +65 -0
- data/lib/truffler/embeddings/embedder.rb +53 -0
- data/lib/truffler/embeddings/fake_embedder.rb +45 -0
- data/lib/truffler/embeddings/label_vector.rb +81 -0
- data/lib/truffler/embeddings/neighbor_store.rb +66 -0
- data/lib/truffler/embeddings/ruby_llm_embedder.rb +25 -0
- data/lib/truffler/embeddings/ruby_store.rb +12 -0
- data/lib/truffler/embeddings/vector_store.rb +84 -0
- data/lib/truffler/embeddings.rb +30 -0
- data/lib/truffler/errors.rb +40 -0
- data/lib/truffler/instrumentation.rb +17 -0
- data/lib/truffler/jobs/backfill_job.rb +45 -0
- data/lib/truffler/jobs/embed_job.rb +33 -0
- data/lib/truffler/jobs/encode_query_job.rb +17 -0
- data/lib/truffler/jobs/expire_lenses_job.rb +15 -0
- data/lib/truffler/jobs/label_flush_job.rb +39 -0
- data/lib/truffler/jobs/lens_backfill_job.rb +33 -0
- data/lib/truffler/jobs/provider_search_job.rb +18 -0
- data/lib/truffler/jobs/prune_query_misses_job.rb +15 -0
- data/lib/truffler/jobs/rerank_chunk_job.rb +14 -0
- data/lib/truffler/jobs/resume_job.rb +61 -0
- data/lib/truffler/jobs/smart_search_job.rb +14 -0
- data/lib/truffler/label_definition.rb +153 -0
- data/lib/truffler/labeling/backfill.rb +189 -0
- data/lib/truffler/labeling/labeler.rb +161 -0
- data/lib/truffler/labeling/queue.rb +125 -0
- data/lib/truffler/labeling/request_builder.rb +87 -0
- data/lib/truffler/labeling/supplied.rb +70 -0
- data/lib/truffler/lenses/activator.rb +51 -0
- data/lib/truffler/lenses/backfill.rb +112 -0
- data/lib/truffler/lenses/draft.rb +16 -0
- data/lib/truffler/lenses/drafter.rb +147 -0
- data/lib/truffler/lenses/fake_generator.rb +58 -0
- data/lib/truffler/lenses/lens.rb +161 -0
- data/lib/truffler/lenses/lens_label.rb +51 -0
- data/lib/truffler/lenses/policy.rb +46 -0
- data/lib/truffler/lenses/previewer.rb +217 -0
- data/lib/truffler/lenses/proposer.rb +53 -0
- data/lib/truffler/lenses/ruby_llm_generator.rb +42 -0
- data/lib/truffler/lenses/scope.rb +35 -0
- data/lib/truffler/lenses/sealed_description.rb +29 -0
- data/lib/truffler/lenses/settings.rb +45 -0
- data/lib/truffler/lenses/validation.rb +69 -0
- data/lib/truffler/lenses/version.rb +34 -0
- data/lib/truffler/lenses.rb +138 -0
- data/lib/truffler/misses/clusterer.rb +69 -0
- data/lib/truffler/misses/recorder.rb +29 -0
- data/lib/truffler/misses/suggestions.rb +49 -0
- data/lib/truffler/misses.rb +83 -0
- data/lib/truffler/model.rb +111 -0
- data/lib/truffler/providers/backup.rb +58 -0
- data/lib/truffler/providers/runner.rb +67 -0
- data/lib/truffler/providers.rb +57 -0
- data/lib/truffler/query_encoding/cache.rb +71 -0
- data/lib/truffler/query_encoding/encoder.rb +183 -0
- data/lib/truffler/query_encoding/prefetch.rb +34 -0
- data/lib/truffler/query_encoding.rb +12 -0
- data/lib/truffler/questions.rb +78 -0
- data/lib/truffler/railtie.rb +11 -0
- data/lib/truffler/records/embedding.rb +39 -0
- data/lib/truffler/records/label.rb +11 -0
- data/lib/truffler/records/query_miss.rb +20 -0
- data/lib/truffler/records/record_state.rb +30 -0
- data/lib/truffler/redaction.rb +24 -0
- data/lib/truffler/registry.rb +16 -0
- data/lib/truffler/search/encoding.rb +84 -0
- data/lib/truffler/search/encoding_cache.rb +68 -0
- data/lib/truffler/search/keystroke.rb +149 -0
- data/lib/truffler/search/query.rb +67 -0
- data/lib/truffler/search/result.rb +100 -0
- data/lib/truffler/search/sql.rb +197 -0
- data/lib/truffler/smart_search/dispatcher.rb +88 -0
- data/lib/truffler/smart_search/reranker.rb +105 -0
- data/lib/truffler/smart_search/run.rb +306 -0
- data/lib/truffler/smart_search/starter.rb +73 -0
- data/lib/truffler/smart_search/store.rb +89 -0
- data/lib/truffler/smart_search.rb +43 -0
- data/lib/truffler/tokens.rb +12 -0
- data/lib/truffler/usage.rb +7 -0
- data/lib/truffler/version.rb +3 -0
- data/lib/truffler/vocabulary.rb +48 -0
- data/lib/truffler.rb +41 -0
- 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}}
|