diff --git a/Cargo.lock b/Cargo.lock index 783988b..b968491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,20 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -328,12 +342,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "basic-cookies" version = "0.1.5" @@ -351,7 +383,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -360,6 +401,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -417,12 +464,27 @@ version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.36" @@ -497,6 +559,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -531,6 +608,34 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -553,6 +658,91 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.106", +] + [[package]] name = "difflib" version = "0.4.0" @@ -674,6 +864,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + [[package]] name = "event-listener" version = "2.5.3" @@ -711,6 +907,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -746,6 +953,18 @@ dependencies = [ "serde", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.1" @@ -758,12 +977,37 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -966,7 +1210,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -1009,6 +1253,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1016,7 +1271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -1042,7 +1297,7 @@ dependencies = [ "async-object-pool", "async-std", "async-trait", - "base64", + "base64 0.21.7", "basic-cookies", "crossbeam-utils", "form_urlencoded", @@ -1071,7 +1326,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -1091,7 +1346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper", "rustls", "tokio", @@ -1135,7 +1390,7 @@ dependencies = [ "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec", + "smallvec 1.15.1", "zerovec", ] @@ -1184,6 +1439,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1191,7 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec", + "smallvec 1.15.1", "utf8_iter", ] @@ -1268,6 +1529,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1315,6 +1585,8 @@ dependencies = [ "figment-json5", "httpmock", "json5", + "ndarray 0.15.6", + "ort", "ortho_config", "regex", "reqwest", @@ -1325,9 +1597,11 @@ dependencies = [ "serde_json", "serde_yaml", "serial_test", + "sha2", "shlex", "tempfile", "thiserror 1.0.69", + "tokenizers", "toml", "uncased", "xdg", @@ -1340,9 +1614,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", - "itertools", + "itertools 0.11.0", "lalrpop-util", "petgraph", "pico-args", @@ -1390,6 +1664,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags 2.9.3", "libc", + "redox_syscall", ] [[package]] @@ -1423,6 +1698,32 @@ dependencies = [ "value-bag", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.7.5" @@ -1435,6 +1736,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1450,43 +1757,216 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "monostate" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2901b7478a273256ce419c446289eb3c883a790d0bf5ed2f363c0cc3988012" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328f7435b7f54322b33832f1e981ff192781f0d12473f12d062705a55015de8d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] -name = "new_debug_unreachable" -version = "1.0.6" +name = "openssl-probe" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "object" -version = "0.36.7" +name = "openssl-sys" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "memchr", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "once_cell_polyfill" -version = "1.70.1" +name = "ort" +version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" +dependencies = [ + "ndarray 0.16.1", + "ort-sys", + "smallvec 2.0.0-alpha.10", + "tracing", +] [[package]] -name = "option-ext" -version = "0.2.0" +name = "ort-sys" +version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" +dependencies = [ + "flate2", + "pkg-config", + "sha2", + "tar", + "ureq", +] [[package]] name = "ortho_config" @@ -1544,10 +2024,16 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec", + "smallvec 1.15.1", "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pear" version = "0.2.9" @@ -1598,6 +2084,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1696,6 +2191,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "polling" version = "3.10.0" @@ -1710,6 +2211,21 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -1719,6 +2235,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -1798,6 +2323,72 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools 0.14.0", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1870,13 +2461,13 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-rustls", @@ -1888,7 +2479,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -2017,7 +2608,25 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", ] [[package]] @@ -2060,6 +2669,15 @@ dependencies = [ "sdd", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2082,6 +2700,29 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.3", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.26" @@ -2090,18 +2731,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -2239,6 +2890,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smallvec" +version = "2.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" + [[package]] name = "smawk" version = "0.3.2" @@ -2265,12 +2922,41 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -2349,6 +3035,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.21.0" @@ -2449,6 +3146,39 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokenizers" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6475a27088c98ea96d00b39a9ddfb63780d1ad4cceb6f48374349a96ab2b7842" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "fancy-regex", + "getrandom 0.3.3", + "itertools 0.14.0", + "log", + "macro_rules_attribute", + "monostate", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.16", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokio" version = "1.47.1" @@ -2627,6 +3357,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec 1.15.1", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.2.1" @@ -2639,6 +3384,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2651,6 +3402,37 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64 0.22.1", + "http 1.3.1", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.7" @@ -2663,6 +3445,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2681,6 +3469,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2812,6 +3606,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -2855,6 +3658,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-sys" version = "0.48.0" @@ -2891,6 +3700,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2928,7 +3746,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3111,6 +3929,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xdg" version = "3.0.0" @@ -3147,6 +3975,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -3168,6 +4016,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index a659423..2215a5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ serde_yaml = { version = "0.9", optional = true } toml = { version = "0.8", optional = true } regex = "1.10" reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"], optional = true } +ort = { version = "2.0.0-rc.10", optional = true, default-features = false, features = ["download-binaries", "ndarray", "std"] } +tokenizers = { version = "0.22.1", optional = true, default-features = false, features = ["fancy-regex"] } +sha2 = { version = "0.10", optional = true } +ndarray = { version = "0.15", optional = true } [features] default = ["toml", "cli"] @@ -30,6 +34,7 @@ json5 = ["cli", "dep:figment-json5", "dep:json5"] yaml = ["cli", "figment/yaml", "dep:serde_yaml"] toml = ["cli", "figment/toml", "dep:toml"] provider-api = ["dep:reqwest"] +onnx = ["dep:ort", "dep:tokenizers", "dep:sha2", "dep:ndarray"] [[bin]] name = "lagc" diff --git a/docs/adr-depth-classifier-onnx.md b/docs/adr-depth-classifier-onnx.md new file mode 100644 index 0000000..5b46b92 --- /dev/null +++ b/docs/adr-depth-classifier-onnx.md @@ -0,0 +1,272 @@ +# Architecture Decision Record: DepthClassifierOnnx — Model Selection & Architecture for the Cognitive Clutch + +**Status:** Accepted (Revised) \ +**Date:** 24 September 2025 \ +**Owner:** LAG Complexity / Cognitive Clutch \ +**Related:** LAG Complexity design document (Complexity Function & Cognitive +Clutch) + +______________________________________________________________________ + +## 1. Context + +The Cognitive Clutch selects between fast heuristic inference and +logic-augmented generation (LAG) using a composite **Complexity** signal. The +LAG Complexity design document defines a provider-based architecture where +independent signals (e.g., depth, ambiguity) are computed, normalised via +**Sigma**, and aggregated with weights and schedules. The depth provider must +output a scalar score that correlates with the compositional reasoning load of +a textual question while meeting production constraints: low latency, +deterministic behaviour, no network dependencies, and clean integration with +the Rust codebase (tracing, metrics, golden tests). + +## 2. Problem statement + +Select a model family and ONNX architecture for a depth estimator that: + +- Achieves single-digit millisecond latency on CPU and a small memory footprint. +- Integrates with existing provider traits; returns a scalar **raw depth** + compatible with Sigma normalisation and threshold schedules defined in the + LAG Complexity design. +- Preserves determinism and reproducibility across platforms. +- Provides a path to improved robustness and interpretability over time. + +## 3. Decision drivers + +- **Latency & footprint:** p95 < 10 ms per query on CPU; model artefact compact; + limited heap churn. +- **Determinism & portability:** pinned opset, frozen weights, embedded + pre-processing constants; identical outputs across supported platforms via + ONNX Runtime. +- **Integration fit:** implements `TextProcessor`; clean wiring + into Complexity → Sigma → Weights → Schedule; full tracing and metrics. +- **Calibration & monotonicity:** outputs align with ordered difficulty/step + levels; stable after calibration across domains. +- **Maintainability & TCO:** viable lifecycle with monitoring, retraining, and + quantisation; fallbacks available. + +## 4. Options considered + +1. **Transformer-ordinal (selected)** — Compact Transformer encoder (e.g., + DistilBERT-class) exported to ONNX; ordinal regression head over ordered + depth levels; scalar depth derived from ordinal probabilities. +2. **Hybrid Transformer + Explicit Features (roadmap)** — Fuse a small set of + engineered syntactic/lexical features with Transformer representations; + ordinal head as in Option 1. +3. **Fixed-feature MLP (fallback)** — Feed-forward network over a pure-Rust + feature vector predicting a continuous log-steps target; quantised ONNX; + used where tokenisation is constrained. +4. **Hashed n-gram linear/FFN (baseline)** — Extremely small model to validate + regressions and act as an emergency backup. + +## 5. Decision + +Adopt **Transformer-ordinal** as the default architecture for +`DepthClassifierOnnx`, exported to ONNX and executed with ONNX Runtime on CPU. +Commit to a near-term optimization plan (post-training static INT8 +quantisation; intermediate-layer pooling ablation) and a longer-term **Hybrid** +roadmap for increased robustness and interpretability. Ship the **Fixed-feature +MLP** as a supported fallback under a feature flag. + +## 6. Selected architecture — Transformer-ordinal + +### I/O signature + +- Inputs: `input_ids: int64[batch, seq]`, `attention_mask: int64[batch, seq]`. + The Rust tokeniser right-pads and right-truncates every request to a fixed + `seq = max_seq_len` (512 tokens for the launch build). `batch` is dynamic in + the ONNX graph, but the provider currently executes single-query batches. +- Outputs: `logits_ord: float32[batch, K]` where `K` is the number of ordered + depth thresholds. The runtime reads the first batch element and discards the + rest, preserving compatibility with future batched inference. + +### Ordinal head + +- Cumulative/threshold encoding: head `k` predicts `P(depth > τ_k)` via + `sigmoid`. +- Scalar depth for the Cognitive Clutch is computed as either: + - **Expected value:** `E[steps] = Σ_k σ(logits_ord[k])`, yielding a value in + the closed range `[0, K]`. The score can then be shifted and scaled onto + the calibrated step axis before Sigma normalisation. + - **Mid-bin mapping:** count the heads with `σ(logits_ord[k]) ≥ θ` for a + configured decision threshold `θ`. Map that integer in `0..=K` to the + representative step value for the corresponding bin. +- This scalar is returned as the provider’s **raw depth**, then normalised by + Sigma and combined per the design document. + +### Backbone and representation + +- DistilBERT-class encoder (or equivalent lightweight encoder) with + **multi-layer representation pooling**: concatenate or pool the `[CLS]` + hidden states from the last 4 layers before the ordinal head. This exploits + the known stratification of linguistic information across layers. + +### Graph notes + +- Opset: 17. +- Graph includes tokenisation-independent components only; tokenisation + performed in Rust using a deterministic vocabulary (e.g., `tokenizers`). +- Optional calibration added as `Mul`/`Add` after the ordinal expectation; + otherwise applied in Rust before Sigma. + +## 7. Short-term optimisations (committed) + +- **Static INT8 quantisation:** apply post-training static quantisation (weights + and activations) for CPU; benchmark size, latency, and accuracy drift against + FP32. +- **Intermediate-layer ablation:** compare final-layer vs multi-layer pooled + representations; promote the best variant by validation metrics and latency. + +## 8. Long-term roadmap — hybrid fusion + +- **Feature selection:** identify a small, computationally inexpensive set of + syntactic/lexical features (e.g., Average Dependency Distance, + clause-to-sentence ratio, bracket/centre-embedding depth, limited curated + bigrams). +- **Fusion strategies:** start with **early fusion** (concatenate pooled + Transformer vector with engineered features before the ordinal head); + evaluate **multi-layer fusion** and attention-based injection (attributes + influencing attention) as advanced candidates. +- **Objective:** improve robustness to paraphrase and domain shift; increase + interpretability while keeping latency within targets. + +## 9. Training and labelling (offline) + +- **Target definition:** ordered depth levels aligned to the LAG Complexity + notion of “reasoning steps” (bins defined during dataset curation). +- **Encoding & loss:** cumulative targets over `K` thresholds; Binary + Cross-Entropy across heads; class weighting optional. +- **Data:** multi-hop QA, mathematical word problems, code-reasoning prompts, + and general QA. Teacher signals (e.g., chain-of-thought token counts or + supporting-fact hops) inform binning. +- **Validation metrics:** Spearman ρ (rank correlation), MAE on mapped steps, + and Expected Calibration Error (ECE) for the ordinal head. +- **Early stopping:** monitor ρ/MAE/ECE; snapshot best by composite score. + +## 10. Calibration + +- **Method:** 1-D isotonic or temperature-scaled affine mapping fitted on a + held-out split. +- **Location:** either baked into the ONNX graph (`Mul`/`Add`) or applied in + provider code before Sigma; both paths preserve determinism and observability. + +## 11. Quantisation and performance targets + +- **Quantisation:** post-training static INT8 for Transformer and head; retain + FP32 outputs. +- **Targets:** p95 ≤ 10 ms (single query, CPU), ≤ 2 ms for batch of 16 short + queries; ≤ 1% absolute accuracy degradation vs FP32; artefact size reduced by + ≥ 50%. + +## 12. Integration details + +- **Provider type:** `DepthClassifierOnnx { session, input_names, output_names + }`. +- **Trait:** implements `TextProcessor`; returns scalar raw depth + (expected steps or mapped mid-bin). +- **Tokenisation:** performed in Rust with a pinned vocabulary; deterministic + and locale-safe. +- **Runtime pinning:** the `ort` crate is fixed to the `2.0.x` series (bundling + ONNX Runtime 1.18), ensuring the shipped runtime matches the opset required + by the artefacts. +- **Error handling:** ONNX Runtime errors mapped to `Error::Inference` with + model path, opset, and checksum in diagnostics. +- **Tracing & metrics:** instrument `process()`; export latency histograms, + error counters, and token length/batch size tags; integrate with existing + observability per the design document. +- **Configuration:** model path, enablement flags, quantisation variant + selection, and optional in-graph calibration switch; Sigma/Weights/Schedule + remain unchanged. + +## 13. Fixed-feature fallback — MLP-log + +- **Use case:** environments where tokenisation is undesirable or constrained. +- **I/O:** `float32[batch, F] → float32[batch, 1]`, `F ≈ 64–96`. +- **Head:** `Linear(F,128) → GELU → Linear(128,64) → GELU → Linear(64,1)`. +- **Target:** `log1p(steps)`; inference maps via `expm1`. +- **Quantisation:** dynamic or static INT8 for linear layers; FP32 output. +- **Output:** scalar raw depth to Sigma. + +## 14. Testing strategy + +- **Unit:** tokeniser determinism; ordinal head probability semantics; + calibration correctness. +- **Golden traces:** snapshot raw ordinal-derived depth alongside heuristic and + fallback MLP on a fixed corpus; fail on drift beyond tolerance. +- **Property tests:** monotonic response as threshold count increases; + robustness to benign paraphrases. +- **Cross-platform:** identical outputs across Linux/macOS/Windows for pinned + opset and artefact. +- **Performance benches:** `criterion` p50/p95 on CPU for FP32 and INT8; + publish artefacts in CI. +- **Ablations:** last-layer vs multi-layer pooling; early vs advanced fusion + (when enabled). + +## 15. Rollout plan + +- Ship Transformer-ordinal behind the `onnx` feature. +- Retain heuristic depth as a safety net; enable A/B comparison in selected + services and compare composite scores and traces. +- Promote to default after benchmarks, calibration, and acceptance criteria are + met; keep Fixed-feature MLP as a documented fallback. + +## 16. Risks and mitigations + +- **Ordinality mismatch:** addressed by ordinal head and calibration. +- **Domain shift & brittleness:** mitigated by hybrid roadmap, diverse training + data, and periodic recalibration. +- **Quantisation drift:** maintain FP32 reference; CI checks compare INT8 vs + FP32. +- **TCO:** invest in monitoring, anomaly detection, and scheduled retraining; + keep fallback model available. +- **Opset/runtime compatibility:** pin opset; validate ORT versions; verify + model checksum at load. + +## 17. Compliance with LAG Complexity design + +- Respects provider abstraction and returns a scalar depth compatible with + Sigma normalisation and weighting. +- Integrates with tracing, metrics, and golden testing. +- Keeps split logic (threshold schedules) unchanged; only the depth signal + source varies. +- Provides a documented fallback path and a roadmap for hybrid improvements. + +## 18. Operational details and versioning + +- **Filename & checksum:** `depth_transformer_ordinal.onnx` (and + `depth_mlp_log.onnx` fallback) with SHA-256 recorded and verified at load. +- **Tokeniser artefacts:** `tokenizer.json`, `vocab.txt`/`merges.txt`, or + `spiece.model` hashes are pinned alongside the model. Provider start-up + recomputes SHA-256 digests for each artefact and fails closed on mismatch. +- **Versioning:** semantic version in ONNX metadata; minor for calibration + changes, patch for weight updates without interface changes, major for I/O + shape changes. +- **Licensing:** include training-data and model licence notices alongside the + artefacts. + +## 19. Open questions + +- Optimal number of ordinal thresholds `K` and bin boundaries for alignment with + the LAG depth scale. +- Final choice of multi-layer pooling vs single-layer for the backbone. +- Preferred fusion mechanism and feature set for the hybrid variant. + +______________________________________________________________________ + +### Appendix A — Minimal ONNX graph summary (Transformer-ordinal) + +- Nodes: encoder subgraph (attention, FFN), `Sigmoid` × K for ordinal heads, + optional `Mul`/`Add` calibration, small `Reduce`/`Add` to compute expectation. +- Inputs: `input_ids: int64[batch, max_seq_len]`, + `attention_mask: int64[batch, max_seq_len]`. +- Outputs: `logits_ord: float32[batch, K]` and/or `depth_scalar: float32[batch, + 1]` if expectation is embedded. + +### Appendix B — Acceptance criteria + +- Spearman ρ ≥ 0.60 on held-out mixed-domain set; MAE ≤ 1.0 steps (after scalar + mapping); ECE within target for ordinal heads. +- p95 latency ≤ 10 ms CPU (single query), ≤ 2 ms for batch of 16; artefact size + reduced by ≥ 50% with INT8; accuracy drop ≤ 1% absolute vs FP32. +- No change required to Sigma/Weights/Schedule interfaces; golden drift ≤ 2% + across supported platforms for identical inputs. diff --git a/docs/lag-complexity-function-design.md b/docs/lag-complexity-function-design.md index ac01159..b6df5a8 100644 --- a/docs/lag-complexity-function-design.md +++ b/docs/lag-complexity-function-design.md @@ -86,8 +86,9 @@ upgraded without impacting the core logic of the consuming agent. This modularity also promotes reusability across different agent implementations or even other systems that could benefit from a query complexity signal. Therefore, the creation of the `lag_complexity` crate is not merely an -implementation detail; it is a foundational choice that enhances the testability, -maintainability, and scalability of the entire AI ecosystem it serves. +implementation detail; it is a foundational choice that enhances the +testability, maintainability, and scalability of the entire AI ecosystem it +serves. ## 1. Crate architecture and public API @@ -424,7 +425,7 @@ as accurate as a model-based approach, it serves as an excellent low-latency first-pass filter. It operates by identifying and counting linguistic markers that often correlate with syntactic and logical complexity.[^9] -##### Feature Engineering +##### Feature engineering (depth) - **Clause Connectors:** It will identify and score coordinating conjunctions (`and`, `or`, `but`) and subordinating conjunctions (`if`, `because`, @@ -443,7 +444,7 @@ that often correlate with syntactic and logical complexity.[^9] - **Aggregation:** The final heuristic score will be a weighted sum of the counts of these features. The weights will be empirically tuned against a curated set of multi-hop questions. -- **Implementation note:** The heuristic uses shared token normalisation and a +- **Implementation note:** The heuristic uses shared token normalization and a pair of weighted tables for tokens and phrases. Word-boundary matching (implemented via the `regex` crate) prevents partial hits. Empty patterns return zero matches to avoid runaway counts, mirroring the public API. For @@ -468,18 +469,35 @@ that often correlate with syntactic and logical complexity.[^9] For higher accuracy, the crate will provide model-based estimators. -- `DepthClassifierOnnx`: Enabled by the `onnx` feature, this provider uses a - small, pre-trained model in the ONNX format for fast local inference. -- **Architecture:** The model will be a simple feed-forward neural network or a - gradient-boosted tree model, designed for minimal computational overhead. -- **Input:** To ensure low latency, the model will not process raw text. - Instead, its input will be a compact feature vector derived from the same - linguistic signals used by `DepthHeuristic` (e.g., counts of conjunctions, - POS tags, dependency patterns). This transforms the task from a complex - sequence problem into a simple and fast classification/regression problem on - a fixed-size vector. -- **Output:** The model will output a calibrated continuous value representing - the predicted number of reasoning steps. +- `DepthClassifierOnnx`: Enabled by the `onnx` feature, this provider is the + default production path for depth estimation. It loads a compact + Transformer-Ordinal model exported to ONNX and executed on CPU via ONNX + Runtime. The architecture choice, performance targets, and operational + controls are captured in the dedicated + [`DepthClassifierOnnx` ADR](adr-depth-classifier-onnx.md). +- **Architecture:** A DistilBERT-class encoder feeds an ordinal regression head + that predicts `P(depth > τ_k)` for `k` ordered thresholds. The provider + computes the expected step count (or, optionally, a mid-bin mapping) from the + ordinal logits and returns this scalar as the raw depth signal for Sigma. +- **Tokenization & determinism:** Text is tokenized in Rust using a pinned + vocabulary to guarantee platform parity. The ONNX graph omits + tokenization-specific nodes, pins opset 17, and includes optional affine + calibration so that outputs remain deterministic across environments. +- **Runtime & artefact controls:** The crate exposes an optional `onnx` + feature that pulls in a pinned `ort = "2.0"` dependency (bundling ONNX + Runtime 1.18). Provider start-up recomputes SHA-256 digests for + `depth_transformer_ordinal.onnx`, the INT8 variant, the MLP fallback, and the + tokenizer artefacts before constructing inference sessions. Any mismatch + raises an error and the provider fails closed, preventing partially loaded + state. +- **Performance roadmap:** Post-training static INT8 quantization and + intermediate-layer pooling ablations are part of the committed optimization + plan. These keep CPU latency below 10 ms p95 while shrinking artefact size + without sacrificing calibration. +- `DepthClassifierMlpOnnx`: A feature-flagged fallback that consumes the + engineered feature vector also used by `DepthHeuristic`. It serves + tokenization-constrained environments, exports as a compact ONNX MLP with + `log1p`/`expm1` scaling, and is quantised for low footprint. - `DepthFromLLM`: Enabled by the `provider-api` feature, this is the highest-fidelity but also highest-latency option. - It wraps an API call to an external LLM (e.g., GPT-4o-mini). @@ -504,7 +522,7 @@ clarified before attempting to answer. This provides a fast, lightweight signal for common sources of ambiguity in English text.[^10] -##### Feature Engineering +##### Feature engineering (ambiguity) - **Coreference Risk (Anaphora):** The heuristic will count third-person pronouns (`it`, `he`, `she`, `they`) and demonstratives (`this`, `that`). The @@ -523,9 +541,9 @@ English text.[^10] - **Aggregation:** The scores from these risk factors will be combined into a single pseudo-entropy value. Laplace smoothing will be applied to ensure a stable, non-zero score even for queries with no detected ambiguity signals. -- **Implementation note:** The initial heuristic uses a shared normaliser for +- **Implementation note:** The initial heuristic uses a shared normalizer for token casing and punctuation. Ambiguous entity matching applies conservative - singularisation (shared text utils) and uses `regex` word boundaries to avoid + singularization (shared text utils) and uses `regex` word boundaries to avoid partial matches. Pronouns and vague terms carry unit weight, ambiguous entities count double, and Laplace smoothing adds one to the total. Antecedent resolution is deferred to model-based providers. @@ -534,8 +552,12 @@ English text.[^10] - Enabled by the `onnx` feature, this provider uses a lightweight text classification model for more nuanced ambiguity detection. -- **Architecture:** The model could be a fine-tuned DistilBERT 34 or a similar - compact transformer, exported to ONNX for efficient inference.[^12] +- **Runtime & artefact controls:** It shares the same pinned `ort` dependency + and checksum-verification path as `DepthClassifierOnnx`, refusing to run if + model or tokenizer artefacts do not match the recorded digests. +- **Architecture:** The model could be a fine-tuned DistilBERT + ([Sanh et al.](https://arxiv.org/abs/1910.01108)) or a similar compact + transformer, exported to ONNX for efficient inference.[^12] - **Training Data:** The model would be trained on a dataset specifically designed for ambiguity, such as AmbigQA, which contains questions annotated with multiple plausible interpretations.[^13] @@ -562,11 +584,12 @@ production-oriented, industrial-grade system. #### Table 2: provider implementation trade-offs (depth & ambiguity) -| Provider Type | Accuracy | Latency | Cost (Compute/API) | Dependencies | Use Case | -| ------------------- | -------- | --------------- | ------------------ | --------------------------- | -------------------------------------------------------------------- | -| **Heuristic** | Low | Very Low (<1ms) | Negligible | None | Default, low-latency applications, initial signal. | -| **ONNX Classifier** | Medium | Low (~5-20ms) | Low (local CPU) | `ort` crate, model file | Production systems needing a balance of speed and accuracy. | -| **External LLM** | High | High (500ms+) | High | `reqwest`, `tokio`, API key | Systems where accuracy is paramount and latency/cost are acceptable. | +| Provider Type | Accuracy | Latency | Cost (Compute/API) | Dependencies | Use Case | +| ---------------------------- | -------- | --------------- | ------------------ | --------------------------- | ------------------------------------------------------------------------ | +| **Heuristic** | Low | Very Low (<1ms) | Negligible | None | Default, low-latency applications, initial signal. | +| **ONNX Transformer-Ordinal** | High | Low (~5-10ms) | Low (local CPU) | `ort` crate, model file | Production default with deterministic CPU inference and Sigma alignment. | +| **ONNX MLP Fallback** | Medium | Very Low (~2ms) | Low (local CPU) | `ort` crate, model file | Tokenization-constrained deployments needing resilience. | +| **External LLM** | High | High (500ms+) | High | `reqwest`, `tokio`, API key | Systems where accuracy is paramount and latency/cost are acceptable. | ## 3. Configuration deep dive: normalization, scheduling, and halting @@ -693,11 +716,12 @@ this task. The crate will leverage this capability to maximize throughput. `rayon::par_iter()` to process multiple queries in the batch concurrently across available CPU cores. 2. **Intra-Query Parallelism:** Within a single `score` call, the three - independent provider functions (`embed`, `estimate`, `entropy_like`) will be - executed in parallel using `rayon::join`. This allows the system to overlap - I/O-bound operations (like an API call for embeddings) with CPU-bound - operations (like running heuristic estimators), significantly reducing the - end-to-end latency for a single query. + independent provider functions (calling `process()` on the embedding, + depth, and ambiguity providers) will be executed in parallel using + `rayon::join`. This allows the system to overlap I/O-bound operations + (like an API call for embeddings) with CPU-bound operations (like running + heuristic estimators), significantly reducing the end-to-end latency for a + single query. ### Caching strategy @@ -709,11 +733,12 @@ essential for performance and cost reduction. `EmbeddingProvider`. Caching the final `Complexity` score is less effective, as small variations in the query text would lead to cache misses. - **Library Selection:** The `moka` crate will be used for caching.[^14] While - `dashmap` is an excellent general-purpose concurrent hash map 42, `moka` is - a specialized, high-performance caching library inspired by Java's Caffeine. - It provides essential caching features out-of-the-box, such as size-based - eviction (LRU/LFU policies) and time-based expiration (TTL/TTI), which are - critical for managing the cache's memory footprint and data freshness. + [`dashmap`](https://crates.io/crates/dashmap) is an excellent general-purpose + concurrent hash map, `moka` is a specialized, high-performance caching + library inspired by Java's Caffeine. It provides essential caching features + out-of-the-box, such as size-based eviction (LRU/LFU policies) and time-based + expiration (TTL/TTI), which are critical for managing the cache's memory + footprint and data freshness. - **Implementation:** A `CachingEmbeddingProvider` decorator will be implemented. This struct will wrap an existing `EmbeddingProvider` and @@ -771,9 +796,10 @@ The crate will be designed with security as a primary consideration. method `with_redaction_hook`. This allows the consumer to inject a custom function that will be applied to all input strings before any processing occurs. This hook enables integration with PII scrubbing libraries (e.g., - those inspired by Microsoft's Presidio 49 or other custom solutions 50) and - is a critical feature for maintaining data privacy and achieving compliance - in regulated domains. + those inspired by Microsoft's + [Presidio](https://github.com/microsoft/presidio) or other custom solutions) + and is a critical feature for maintaining data privacy and achieving + compliance in regulated domains. ### Determinism @@ -815,8 +841,8 @@ in isolation. - The `Schedule::ExpDecay` logic will be tested to confirm that the threshold `τ(t)` is always positive and monotonically decreasing as the step `t` increases. - -##### Heuristic Components: + +##### Heuristic components - Tests for `DepthHeuristic` will assert that adding linguistic complexity markers (e.g., a subordinating conjunction or a relative clause) never @@ -837,8 +863,8 @@ test writers might miss. - The `score(q)` function must never panic for any valid UTF-8 string `q`. - All components of the returned `Complexity` struct (`total`, `scope`, `depth`, `ambiguity`) must be non-negative. - -##### Behavioural properties: + +##### Behavioural properties - **Idempotence:** `score(q)` should be equal to `score(q.clone())`. - **Scope Order-Insensitivity:** For the `ScopeVariance` component, @@ -906,25 +932,30 @@ academic datasets and report on its performance. be created to automate this process. It will load specified datasets, run the `lag_complexity` scorer on the questions, and compute a suite of validation metrics. - -#### Dataset-to-Component Mapping: + +#### Dataset-to-component mapping - **Reasoning Steps (**`depth`**):** Performance will be measured against - multi-hop question-answering datasets like **HotpotQA** 52, **2WikiMultiHopQA**, - and **MuSiQue**. The number of supporting facts or annotated reasoning hops will - serve as the ground truth for reasoning depth. + multi-hop question-answering datasets like + [**HotpotQA**](https://hotpotqa.github.io), + [**2WikiMultiHopQA**](https://github.com/Alab-NII/2wiki-multihop-qa), and + [**MuSiQue**](https://arxiv.org/abs/2108.01065). The number of supporting + facts or annotated reasoning hops will serve as the ground truth for + reasoning depth. - **Ambiguity (**`ambiguity`**):** The ambiguity score will be validated - against datasets designed to study ambiguity, such as **AmbigQA** 38 and - **ASQA**. The ground truth will be the dataset's annotation indicating - whether a question has multiple plausible interpretations. + against datasets designed to study ambiguity, such as **AmbigQA**[^13] and + [**ASQA**](https://github.com/nyu-mll/ambigqa/tree/main/asqa). The ground + truth will be the dataset's annotation indicating whether a question has + multiple plausible interpretations. - **Semantic Scope (**`scope`**):** As there is no direct ground-truth label for "scope," its behaviour will be evaluated indirectly. Using diverse - datasets from the **BEIR benchmark** 54 and Semantic Textual Similarity (STS) - tasks, the hypothesis is that a set of queries with high conceptual diversity - (low average inter-query similarity) should produce a higher average scope - score than a set of topically narrow, highly similar queries. + datasets from the [**BEIR benchmark**](https://arxiv.org/abs/2104.08663) and + Semantic Textual Similarity (STS) tasks, the hypothesis is that a set of + queries with high conceptual diversity (low average inter-query similarity) + should produce a higher average scope score than a set of topically narrow, + highly similar queries. - **Evaluation Metrics:** - **Rank Correlation:** The primary metrics will be **Kendall's Tau (τ)** and @@ -967,7 +998,7 @@ These benchmarks will isolate and measure the performance of individual, critical components to identify potential bottlenecks and guide optimization efforts. -#### Provider Latency: +#### Provider latency - `EmbeddingProvider::process`: The latency of this method will be measured for each available provider (`ApiEmbedding`, `LocalModelEmbedding` with `tch` and @@ -979,8 +1010,8 @@ efforts. - `ONNX` Model Inference: The latency of a single inference pass for the `DepthClassifierOnnx` and `AmbiguityClassifierOnnx` models will be benchmarked. - -#### Computational Overhead: + +#### Computational Overhead - The time taken for the variance calculation and the application of `Sigma` normalization will be measured to ensure they contribute negligibly to the @@ -1296,19 +1327,19 @@ defining the primary public interfaces. - Initialize the Rust project using `cargo new`. - Define all public traits (`ComplexityFn`, `EmbeddingProvider`, `DepthEstimator`, `AmbiguityEstimator`). - - Define all public data structures (`Complexity`, `Trace`, `ScoringConfig` and - its sub-types) and derive `serde` traits for configuration types. + - Define all public data structures (`Complexity`, `Trace`, `ScoringConfig` + and its sub-types) and derive `serde` traits for configuration types. - Implement the mathematical logic for variance calculation and all `Sigma` normalization strategies. - Create the stub for the `lagc` command-line interface binary using the - `ortho_config` crate (published as `ortho-config` on - crates.io)[^hyphen-underscore], which layers command-line arguments, - environment variables (prefixed with `LAGC_`), and configuration files - without extra boilerplate. Precedence is: command-line arguments > - environment variables > configuration files. - -- **Acceptance Criteria:** - + `ortho_config` crate (published as `ortho-config` on crates.io and imported + as `ortho_config`), which layers command-line arguments, environment + variables (prefixed with `LAGC_`), and configuration files without extra + boilerplate. Precedence is: command-line arguments > environment variables + > configuration files. + +- **Acceptance criteria:** + - The crate and all its core types compile successfully. - A comprehensive suite of unit tests for the mathematical and normalization logic passes. @@ -1321,14 +1352,14 @@ This phase delivers the first end-to-end, functional version of the scorer, relying on fast, lightweight heuristics. - **Tasks:** - + - Implement the `DepthHeuristic` and `AmbiguityHeuristic` providers. - Implement the `ApiEmbedding` provider (behind the `provider-api` feature flag) to enable initial testing with high-quality embeddings. - Create the golden-file integration test suite with an initial set of ~50 curated queries and their expected trace outputs. -- **Acceptance Criteria:** +- **Acceptance criteria:** - The `score()` and `trace()` methods are fully functional using the heuristic providers. @@ -1342,16 +1373,19 @@ optimizing for performance. - **Tasks:** - - Train (or adapt existing) and export the initial ONNX models for depth and - ambiguity classification. + - Train (or adapt existing) and export the Transformer-Ordinal depth model + (including the INT8 variant and `depth_mlp_log.onnx` fallback) alongside + the ambiguity classifier. - Implement the `DepthClassifierOnnx` and `AmbiguityClassifierOnnx` providers, - gated by the `onnx` feature flag. + gated by the `onnx` feature flag, wiring in calibration switches, + deterministic tokenization, and fail-closed SHA-256 verification for model + and tokenizer artefacts. - Implement the `score_batch` method and integrate `rayon` for parallel execution. - Set up the `criterion` benchmarking suite and implement the initial set of micro and macro benchmarks. -- **Acceptance Criteria:** +- **Acceptance criteria:** - The ONNX-based providers can be successfully composed into a `DefaultComplexity` engine and produce valid scores. @@ -1374,7 +1408,7 @@ and tuning its parameters. - Run the evaluation harness and analyse the results to fine-tune the `Sigma` normalization parameters and the weights within the heuristic models. -- **Acceptance Criteria:** +- **Acceptance criteria:** - The evaluation harness successfully generates a report (`EVALUATION.md`). - The report demonstrates a statistically significant positive correlation @@ -1394,9 +1428,9 @@ creating compelling demonstrations. - Develop the interactive "Complexity Meter" web page using the WASM module. - Create the Jupyter notebooks for the "Smart Assistant" and "Ambiguity Resolver" stakeholder demonstrations. - -- **Acceptance Criteria:** - + +- **Acceptance criteria:** + - The Python package can be built, installed via `pip`, and used to score queries. - The WASM demo is fully functional, interactive, and hosted on a static page. @@ -1418,7 +1452,7 @@ observable production deployment. - Finalize the `README.md` to include installation instructions, usage examples, and links to benchmarks and evaluation reports. -- **Acceptance Criteria:** +- **Acceptance criteria:** - The crate is fully documented, with `cargo doc --open` producing a complete and navigable API reference. @@ -1590,7 +1624,3 @@ also understand when a question requires deeper thought. [^20]: Rust Wasm Bindgen guide. [^21]: pyo3 project documentation. [^22]: D. Kahneman, "Thinking, Fast and Slow." - -[^hyphen-underscore]: Cargo converts hyphens to underscores for import paths. - The package is `ortho-config` on crates.io and is - imported as `ortho_config` in code. diff --git a/src/lib.rs b/src/lib.rs index 4dcce34..57c99e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,12 @@ pub use heuristics::{ AmbiguityHeuristic, AmbiguityHeuristicError, DepthHeuristic, DepthHeuristicError, HeuristicComplexity, HeuristicComplexityError, }; +#[cfg(feature = "onnx")] +pub use providers::onnx::{ + AmbiguityClassifierOnnx, AmbiguityClassifierOnnxError, DepthClassifierOnnx, + DepthClassifierOnnxError, OnnxArtefact, OnnxClassifierConfig, OnnxClassifierError, + OrdinalAggregation, +}; pub use providers::{AmbiguityEstimator, DepthEstimator, EmbeddingProvider, TextProcessor}; pub use sigma::Sigma; pub use variance::VarianceError; diff --git a/src/providers.rs b/src/providers/mod.rs similarity index 97% rename from src/providers.rs rename to src/providers/mod.rs index 85ba339..5d6cd45 100644 --- a/src/providers.rs +++ b/src/providers/mod.rs @@ -4,6 +4,9 @@ //! Defines the `TextProcessor` trait and the provider aliases used by the //! scoring engine. +#[cfg(feature = "onnx")] +pub mod onnx; + /// Processes text to produce a structured, thread-safe output. /// /// Providers for embeddings, depth, and ambiguity all implement this trait via diff --git a/src/providers/onnx/aggregation.rs b/src/providers/onnx/aggregation.rs new file mode 100644 index 0000000..2c72166 --- /dev/null +++ b/src/providers/onnx/aggregation.rs @@ -0,0 +1,83 @@ +use super::errors::OnnxClassifierError; + +/// Aggregation strategy that converts ordinal head probabilities into a scalar score. +#[derive(Debug, Clone)] +pub enum OrdinalAggregation { + /// Sum the head probabilities and apply an affine mapping. + /// + /// The sum yields a value in the range `[0, head_count]`. Scaling and offsetting map the + /// count to a calibrated step domain. + ExpectedValue { + /// Additive offset applied after summing the probabilities. + base: f32, + /// Multiplicative factor applied to the summed probabilities. + scale: f32, + }, + /// Choose a representative mid-bin value by counting how many heads exceed a threshold. + MidBin { + /// Representative step value for each possible count of positive heads. Length must be + /// `head_count + 1`. + bin_midpoints: Vec, + /// Probability threshold that marks a head as “positive”. + decision_threshold: f32, + }, +} + +impl OrdinalAggregation { + /// Validates the ordinal aggregation configuration. + /// + /// # Errors + /// + /// Returns `InvalidBinCount` when the mid-bin mapping length does not match `head_count + 1`. + /// Returns `InvalidDecisionThreshold` when the decision threshold falls outside `[0.0, 1.0]`. + pub fn validate(&self, head_count: usize) -> Result<(), OnnxClassifierError> { + match self { + Self::ExpectedValue { .. } => Ok(()), + Self::MidBin { + bin_midpoints, + decision_threshold, + } => { + let expected = head_count + 1; + if !(0.0..=1.0).contains(decision_threshold) { + return Err(OnnxClassifierError::InvalidDecisionThreshold { + min: 0.0, + max: 1.0, + actual: *decision_threshold, + }); + } + if bin_midpoints.len() == expected { + Ok(()) + } else { + Err(OnnxClassifierError::InvalidBinCount { + expected, + actual: bin_midpoints.len(), + }) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::OrdinalAggregation; + + #[test] + fn ordinal_aggregation_validates_mid_bin_length() { + let aggregation = OrdinalAggregation::MidBin { + bin_midpoints: vec![0.0, 1.0, 2.0], + decision_threshold: 0.5, + }; + assert!(aggregation.validate(2).is_ok()); + assert!(aggregation.validate(1).is_err()); + } + + #[test] + fn ordinal_aggregation_rejects_out_of_range_threshold() { + let aggregation = OrdinalAggregation::MidBin { + bin_midpoints: vec![0.0, 1.0], + decision_threshold: 1.5, + }; + assert!(aggregation.validate(1).is_err()); + } +} diff --git a/src/providers/onnx/artefact.rs b/src/providers/onnx/artefact.rs new file mode 100644 index 0000000..6cb7b9d --- /dev/null +++ b/src/providers/onnx/artefact.rs @@ -0,0 +1,84 @@ +use std::{ + fs::File, + io::{BufReader, Read}, + path::{Path, PathBuf}, +}; + +use sha2::{Digest, Sha256}; + +use super::errors::OnnxClassifierError; + +/// File-based artefact that must match a recorded checksum before loading. +#[derive(Debug, Clone)] +pub struct OnnxArtefact { + /// Location of the artefact on disk. + pub path: PathBuf, + /// Expected SHA-256 checksum expressed as lowercase hexadecimal. + pub sha256: String, +} + +impl OnnxArtefact { + /// Verifies the artefact checksum against the expected digest. + /// + /// # Errors + /// + /// Returns `ChecksumMismatch` when the computed digest does not match `sha256` and propagates I/O errors while reading the file. + pub fn verify(&self) -> Result<(), OnnxClassifierError> { + let actual = compute_sha256(&self.path)?; + if actual == normalise_hex(&self.sha256) { + Ok(()) + } else { + Err(OnnxClassifierError::ChecksumMismatch { + path: self.path.clone(), + expected: normalise_hex(&self.sha256), + actual, + }) + } + } +} + +/// Computes the SHA-256 digest of the file at `path`. +/// +/// # Errors +/// +/// Returns I/O errors from opening or reading the file. +pub fn compute_sha256(path: &Path) -> Result { + let file = File::open(path).map_err(|source| OnnxClassifierError::Io { + path: path.to_path_buf(), + source, + })?; + let mut reader = BufReader::new(file); + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 8192]; + loop { + let read = reader + .read(&mut buffer) + .map_err(|source| OnnxClassifierError::Io { + path: path.to_path_buf(), + source, + })?; + if read == 0 { + break; + } + let chunk = buffer.get(..read).ok_or_else(|| OnnxClassifierError::Io { + path: path.to_path_buf(), + source: std::io::Error::other("read reported bytes beyond buffer length"), + })?; + hasher.update(chunk); + } + Ok(format!("{:x}", hasher.finalize())) +} + +pub fn normalise_hex(value: &str) -> String { + value.trim().to_ascii_lowercase() +} + +#[cfg(test)] +mod tests { + use super::normalise_hex; + + #[test] + fn normalise_hex_lowercases_and_trims() { + assert_eq!(normalise_hex(" ABCDEF "), "abcdef"); + } +} diff --git a/src/providers/onnx/classifier.rs b/src/providers/onnx/classifier.rs new file mode 100644 index 0000000..7a16622 --- /dev/null +++ b/src/providers/onnx/classifier.rs @@ -0,0 +1,280 @@ +use std::sync::{Arc, Mutex}; + +use ort::{session::Session, value::TensorRef}; +use tokenizers::{ + Encoding, PaddingDirection, PaddingParams, PaddingStrategy, Tokenizer, TruncationDirection, + TruncationParams, TruncationStrategy, +}; + +use super::{ + aggregation::OrdinalAggregation, config::OnnxClassifierConfig, errors::OnnxClassifierError, +}; + +#[derive(Debug)] +pub struct OnnxTextClassifier { + session: Arc>, + tokenizer: Arc, + input_names: Arc>, + output_names: Arc>, + max_sequence_length: usize, + head_count: usize, + aggregator: OrdinalAggregation, +} + +/// Tokenised request converted to ONNX tensor inputs. +#[derive(Debug)] +struct EncodedInput { + ids: Vec, + attention: Vec, + type_ids: Vec, +} + +impl EncodedInput { + fn from_encoding( + encoding: &Encoding, + expected_len: usize, + ) -> Result { + let ids = encoding.get_ids(); + Self::ensure_length(ids.len(), expected_len)?; + let attention = encoding.get_attention_mask(); + Self::ensure_length(attention.len(), expected_len)?; + let type_ids = encoding.get_type_ids(); + Self::ensure_length(type_ids.len(), expected_len)?; + Ok(Self { + ids: ids.iter().map(|id| i64::from(*id)).collect(), + attention: attention.iter().map(|id| i64::from(*id)).collect(), + type_ids: type_ids.iter().map(|id| i64::from(*id)).collect(), + }) + } + + fn ensure_length(length: usize, expected: usize) -> Result<(), OnnxClassifierError> { + if length == expected { + Ok(()) + } else { + Err(OnnxClassifierError::SequenceLength { + expected, + actual: length, + }) + } + } +} + +impl OnnxTextClassifier { + /// Builds an ONNX classifier from the supplied configuration. + /// + /// # Errors + /// + /// Returns configuration and runtime errors when artefacts cannot be verified, tokeniser setup fails, or the ONNX session cannot be created. + pub fn new(config: OnnxClassifierConfig) -> Result { + if config.max_sequence_length == 0 { + return Err(OnnxClassifierError::ZeroSequenceLength); + } + if config.input_names.len() < 2 { + return Err(OnnxClassifierError::InsufficientInputNames { + expected: 2, + actual: config.input_names.len(), + }); + } + if config.output_names.is_empty() { + return Err(OnnxClassifierError::MissingOutputNames); + } + if config.ordinal_head_count == 0 { + return Err(OnnxClassifierError::ZeroHeadCount); + } + + config.model.verify()?; + config.tokenizer.verify()?; + + let mut tokenizer = Tokenizer::from_file(&config.tokenizer.path).map_err(|source| { + OnnxClassifierError::LoadTokenizer { + path: config.tokenizer.path.clone(), + source, + } + })?; + + tokenizer + .with_truncation(Some(TruncationParams { + max_length: config.max_sequence_length, + strategy: TruncationStrategy::OnlyFirst, + stride: 0, + direction: TruncationDirection::Right, + })) + .map_err(OnnxClassifierError::ConfigureTruncation)?; + + tokenizer.with_padding(Some(PaddingParams { + strategy: PaddingStrategy::Fixed(config.max_sequence_length), + direction: PaddingDirection::Right, + pad_to_multiple_of: None, + pad_id: config.pad_id, + pad_type_id: 0, + pad_token: config.pad_token.clone(), + })); + + config.aggregator.validate(config.ordinal_head_count)?; + + let session = Session::builder() + .map_err(OnnxClassifierError::CreateSessionBuilder)? + .commit_from_file(&config.model.path) + .map_err(OnnxClassifierError::CreateSession)?; + + Ok(Self { + session: Arc::new(Mutex::new(session)), + tokenizer: Arc::new(tokenizer), + input_names: Arc::new(config.input_names), + output_names: Arc::new(config.output_names), + max_sequence_length: config.max_sequence_length, + head_count: config.ordinal_head_count, + aggregator: config.aggregator, + }) + } + + /// Runs inference for a single input string and returns the aggregated ordinal score. + /// + /// # Errors + /// + /// Returns encoding, inference, or aggregation errors when tokenisation fails, the ONNX runtime errors, or the logits do not match the configured head count. + pub fn predict(&self, input: &str) -> Result { + let encoding = self + .tokenizer + .encode(input, true) + .map_err(OnnxClassifierError::Encode)?; + let encoded = EncodedInput::from_encoding(&encoding, self.max_sequence_length)?; + let type_ids_name = self.input_names.get(2).map(String::as_str); + let (output_name, logits) = self.run_session(&encoded, type_ids_name)?; + self.aggregate_logits(&output_name, &logits) + } + + fn run_session( + &self, + encoded: &EncodedInput, + type_ids_name: Option<&str>, + ) -> Result<(String, Vec), OnnxClassifierError> { + let ids_tensor = TensorRef::from_array_view(( + [1usize, self.max_sequence_length], + encoded.ids.as_slice(), + )) + .map_err(OnnxClassifierError::EncodeTensor)?; + let attention_tensor = TensorRef::from_array_view(( + [1usize, self.max_sequence_length], + encoded.attention.as_slice(), + )) + .map_err(OnnxClassifierError::EncodeTensor)?; + + let mut session = self + .session + .lock() + .map_err(|_| OnnxClassifierError::SessionPoisoned)?; + + let input_ids_name = self + .input_names + .first() + .ok_or(OnnxClassifierError::MissingInputNames)? + .as_str(); + let attention_mask_name = self + .input_names + .get(1) + .ok_or(OnnxClassifierError::InsufficientInputNames { + expected: 2, + actual: self.input_names.len(), + })? + .as_str(); + + let outputs = match type_ids_name { + Some(type_ids_name) => { + let type_ids_tensor = TensorRef::from_array_view(( + [1usize, self.max_sequence_length], + encoded.type_ids.as_slice(), + )) + .map_err(OnnxClassifierError::EncodeTensor)?; + session + .run(ort::inputs! { + input_ids_name => ids_tensor, + attention_mask_name => attention_tensor, + type_ids_name => type_ids_tensor, + }) + .map_err(OnnxClassifierError::Inference)? + } + None => session + .run(ort::inputs! { + input_ids_name => ids_tensor, + attention_mask_name => attention_tensor, + }) + .map_err(OnnxClassifierError::Inference)?, + }; + + let output_name = self + .output_names + .first() + .ok_or(OnnxClassifierError::MissingOutputNames)? + .clone(); + let logits_value = + outputs + .get(&output_name) + .ok_or_else(|| OnnxClassifierError::OutputMissing { + name: output_name.clone(), + })?; + let (_, logits) = logits_value + .try_extract_tensor::() + .map_err(OnnxClassifierError::Inference)?; + Ok((output_name, logits.to_vec())) + } + + fn aggregate_logits( + &self, + output_name: &str, + logits: &[f32], + ) -> Result { + if logits.len() != self.head_count { + return Err(OnnxClassifierError::UnexpectedLogitCount { + name: output_name.to_string(), + expected: self.head_count, + actual: logits.len(), + }); + } + + let probabilities: Vec = logits.iter().map(|value| logistic(*value)).collect(); + match &self.aggregator { + OrdinalAggregation::ExpectedValue { base, scale } => { + let sum = probabilities.iter().copied().sum::(); + #[expect( + clippy::float_arithmetic, + reason = "ordinal aggregation requires float scaling" + )] + let value = base + scale * sum; + Ok(value) + } + OrdinalAggregation::MidBin { + bin_midpoints, + decision_threshold, + } => { + let positives = probabilities + .iter() + .filter(|probability| **probability >= *decision_threshold) + .count(); + bin_midpoints + .get(positives) + .copied() + .ok_or(OnnxClassifierError::InvalidBinCount { + expected: self.head_count + 1, + actual: bin_midpoints.len(), + }) + } + } + } +} + +#[expect(clippy::float_arithmetic, reason = "sigmoid requires float operations")] +fn logistic(value: f32) -> f32 { + 1.0 / (1.0 + (-value).exp()) +} + +#[cfg(test)] +mod tests { + use super::logistic; + + #[test] + fn logistic_is_monotonic() { + assert!(logistic(-5.0) < logistic(0.0)); + assert!(logistic(0.0) < logistic(5.0)); + } +} diff --git a/src/providers/onnx/config.rs b/src/providers/onnx/config.rs new file mode 100644 index 0000000..5a2aef8 --- /dev/null +++ b/src/providers/onnx/config.rs @@ -0,0 +1,28 @@ +//! Configuration types for ONNX-based ordinal text classifiers. + +use super::{aggregation::OrdinalAggregation, artefact::OnnxArtefact}; + +/// Configuration for an ordinal ONNX text classifier. +#[derive(Debug, Clone)] +pub struct OnnxClassifierConfig { + /// Model artefact (ONNX graph) to load. + pub model: OnnxArtefact, + /// Tokeniser artefact consumed by `tokenizers`. + pub tokenizer: OnnxArtefact, + /// Ordered input names as defined in the ONNX graph. + pub input_names: Vec, + /// Ordered output names to query after inference. + pub output_names: Vec, + /// Maximum token count accepted by the model. Inputs are padded and truncated to this size. + /// Must be greater than zero so every encoding produces fixed-length tensors. + pub max_sequence_length: usize, + /// Token inserted when padding shorter sequences. + pub pad_token: String, + /// Identifier of the padding token. + pub pad_id: u32, + /// Number of ordinal heads exposed by the model output. + /// Must be greater than zero so aggregation matches the logits tensor. + pub ordinal_head_count: usize, + /// Mapping from ordinal probabilities to the scalar score. + pub aggregator: OrdinalAggregation, +} diff --git a/src/providers/onnx/errors.rs b/src/providers/onnx/errors.rs new file mode 100644 index 0000000..cf9cf3c --- /dev/null +++ b/src/providers/onnx/errors.rs @@ -0,0 +1,80 @@ +use std::path::PathBuf; + +use thiserror::Error; + +/// Errors produced by the shared ONNX classifier implementation. +#[derive(Debug, Error)] +pub enum OnnxClassifierError { + #[error("failed to read artefact at {path}: {source}")] + Io { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("artefact at {path} expected SHA-256 {expected} but found {actual}")] + ChecksumMismatch { + path: PathBuf, + expected: String, + actual: String, + }, + #[error("failed to load tokenizer from {path}: {source}")] + LoadTokenizer { + path: PathBuf, + #[source] + source: tokenizers::Error, + }, + #[error("failed to configure tokenizer truncation: {0}")] + ConfigureTruncation(#[source] tokenizers::Error), + #[error("failed to construct ONNX session builder: {0}")] + CreateSessionBuilder(#[source] ort::Error), + #[error("failed to create ONNX session: {0}")] + CreateSession(#[source] ort::Error), + #[error("classifier requires at least one input name")] + MissingInputNames, + #[error("classifier requires at least {expected} input names but found {actual}")] + InsufficientInputNames { expected: usize, actual: usize }, + #[error("classifier requires at least one output name")] + MissingOutputNames, + #[error("classifier requires max_sequence_length > 0")] + ZeroSequenceLength, + #[error("classifier requires ordinal_head_count > 0")] + ZeroHeadCount, + #[error("tokenizer produced sequence of length {actual} but expected {expected}")] + SequenceLength { expected: usize, actual: usize }, + #[error("failed to encode text: {0}")] + Encode(#[source] tokenizers::Error), + #[error("failed to convert encoded text into tensor: {0}")] + EncodeTensor(#[source] ort::Error), + #[error("session mutex was poisoned by a previous panic")] + SessionPoisoned, + #[error("failed to run inference: {0}")] + Inference(#[source] ort::Error), + #[error("ONNX output \"{name}\" missing from session results")] + OutputMissing { name: String }, + #[error("ONNX output \"{name}\" has {actual} logits but expected {expected}")] + UnexpectedLogitCount { + name: String, + expected: usize, + actual: usize, + }, + #[error("mid-bin mapping requires {expected} bins but found {actual}")] + InvalidBinCount { expected: usize, actual: usize }, + #[error("decision threshold must be within [{min}, {max}] but was {actual}")] + InvalidDecisionThreshold { min: f32, max: f32, actual: f32 }, +} + +/// Errors returned by [`DepthClassifierOnnx`]. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum DepthClassifierOnnxError { + #[error(transparent)] + Classifier(#[from] OnnxClassifierError), +} + +/// Errors returned by [`AmbiguityClassifierOnnx`]. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum AmbiguityClassifierOnnxError { + #[error(transparent)] + Classifier(#[from] OnnxClassifierError), +} diff --git a/src/providers/onnx/mod.rs b/src/providers/onnx/mod.rs new file mode 100644 index 0000000..0cc16b4 --- /dev/null +++ b/src/providers/onnx/mod.rs @@ -0,0 +1,136 @@ +//! ONNX-backed text classifiers for depth and ambiguity with artefact verification. +mod aggregation; +mod artefact; +mod classifier; +mod config; +mod errors; + +pub use aggregation::OrdinalAggregation; +pub use artefact::OnnxArtefact; +pub use config::OnnxClassifierConfig; +pub use errors::{AmbiguityClassifierOnnxError, DepthClassifierOnnxError, OnnxClassifierError}; + +use crate::providers::TextProcessor; +use classifier::OnnxTextClassifier; + +/// Depth classifier using an ONNX Transformer-Ordinal model. +/// +/// Returns the expected reasoning steps as a raw depth score for Sigma normalisation. +#[derive(Debug)] +pub struct DepthClassifierOnnx { + inner: OnnxTextClassifier, +} + +impl DepthClassifierOnnx { + /// Builds a depth classifier from disk artefacts. + /// + /// # Errors + /// + /// Returns [`DepthClassifierOnnxError`] when artefact verification or ONNX initialisation fails. + /// + /// # Examples + /// ```no_run + /// use lag_complexity::providers::onnx::{DepthClassifierOnnx, DepthClassifierOnnxError, OnnxArtefact, OnnxClassifierConfig, OrdinalAggregation}; + /// + /// # fn main() -> Result<(), DepthClassifierOnnxError> { + /// let config = OnnxClassifierConfig { + /// model: OnnxArtefact { + /// path: std::path::PathBuf::from("/models/depth_transformer_ordinal.onnx"), + /// sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), + /// }, + /// tokenizer: OnnxArtefact { + /// path: std::path::PathBuf::from("/models/depth_tokenizer.json"), + /// sha256: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210".into(), + /// }, + /// input_names: vec!["input_ids".into(), "attention_mask".into()], + /// output_names: vec!["logits_ord".into()], + /// max_sequence_length: 512, + /// pad_token: "[PAD]".into(), + /// pad_id: 0, + /// ordinal_head_count: 5, + /// aggregator: OrdinalAggregation::ExpectedValue { base: 0.0, scale: 1.0 }, + /// }; + /// let classifier = DepthClassifierOnnx::new(config)?; + /// let score = classifier.process("Which university did the CEO of the company that developed the original iPhone attend?")?; + /// # println!("{score}"); + /// # Ok(()) + /// # } + /// ``` + pub fn new(config: OnnxClassifierConfig) -> Result { + OnnxTextClassifier::new(config) + .map(|inner| Self { inner }) + .map_err(DepthClassifierOnnxError::Classifier) + } +} + +impl TextProcessor for DepthClassifierOnnx { + type Output = f32; + type Error = DepthClassifierOnnxError; + + fn process(&self, input: &str) -> Result { + self.inner + .predict(input) + .map_err(DepthClassifierOnnxError::Classifier) + } +} + +/// Ambiguity classifier using an ONNX Transformer-Ordinal model. +/// +/// Returns a raw ambiguity score in probability units for Sigma normalisation. +#[derive(Debug)] +pub struct AmbiguityClassifierOnnx { + inner: OnnxTextClassifier, +} + +impl AmbiguityClassifierOnnx { + /// Builds an ambiguity classifier from disk artefacts. + /// + /// # Errors + /// + /// Returns [`AmbiguityClassifierOnnxError`] when artefact verification or ONNX initialisation fails. + /// + /// # Examples + /// ```no_run + /// use lag_complexity::providers::onnx::{AmbiguityClassifierOnnx, AmbiguityClassifierOnnxError, OnnxArtefact, OnnxClassifierConfig, OrdinalAggregation}; + /// + /// # fn main() -> Result<(), AmbiguityClassifierOnnxError> { + /// let config = OnnxClassifierConfig { + /// model: OnnxArtefact { + /// path: std::path::PathBuf::from("/models/ambiguity_transformer_ordinal.onnx"), + /// sha256: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff".into(), + /// }, + /// tokenizer: OnnxArtefact { + /// path: std::path::PathBuf::from("/models/ambiguity_tokenizer.json"), + /// sha256: "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100".into(), + /// }, + /// input_names: vec!["input_ids".into(), "attention_mask".into()], + /// output_names: vec!["logits_ord".into()], + /// max_sequence_length: 384, + /// pad_token: "[PAD]".into(), + /// pad_id: 0, + /// ordinal_head_count: 4, + /// aggregator: OrdinalAggregation::ExpectedValue { base: 0.0, scale: 1.0 }, + /// }; + /// let classifier = AmbiguityClassifierOnnx::new(config)?; + /// let score = classifier.process("What is the primary source of the Nile?")?; + /// # println!("{score}"); + /// # Ok(()) + /// # } + /// ``` + pub fn new(config: OnnxClassifierConfig) -> Result { + OnnxTextClassifier::new(config) + .map(|inner| Self { inner }) + .map_err(AmbiguityClassifierOnnxError::Classifier) + } +} + +impl TextProcessor for AmbiguityClassifierOnnx { + type Output = f32; + type Error = AmbiguityClassifierOnnxError; + + fn process(&self, input: &str) -> Result { + self.inner + .predict(input) + .map_err(AmbiguityClassifierOnnxError::Classifier) + } +}