From 694ca28d6fb11aad672b4aed25b874c94617dd8d Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sat, 4 Oct 2025 20:42:49 -0300 Subject: [PATCH] - First LLM version like v5. --- Cargo.lock | 1731 +++++++++++++---- Cargo.toml | 72 +- src/PROMPT.md | 7 + src/main.rs | 169 +- src/scripts/containers/cache.sh | 7 + src/scripts/containers/email.sh | 19 +- src/scripts/containers/vector-db.sh | 4 + src/scripts/database/0004.sql | 129 ++ src/scripts/database/001_init.sql | 57 + src/services/auth/mod.rs | 90 + src/services/{ => automation}/automation.rs | 0 src/services/{ => bot}/bot.md | 0 src/services/bot/orchestrator.rs | 884 +++++++++ src/services/channels/mod.rs | 153 ++ src/services/chart/chart.rs | 92 + src/services/{config.rs => config/mod.rs} | 0 src/services/context/mod.rs | 97 + src/services/{ => email}/email.rs | 0 src/services/{ => email}/llm-email.md | 0 src/services/{ => file}/file.rs | 0 src/services/{ => llm}/llm.rs | 0 src/services/{ => llm}/llm_generic.rs | 0 src/services/{ => llm}/llm_local.rs | 0 src/services/{ => llm}/llm_provider.rs | 0 src/services/llm/mod.rs | 121 ++ src/{services.rs => services/mod.rs} | 12 +- src/services/{ => org}/org.md | 0 src/services/{ => script}/script.rs | 0 src/services/session/mod.rs | 175 ++ src/services/shared/mod.rs | 3 + src/services/shared/shared.rs | 58 + src/services/{ => shared}/state.rs | 5 + src/services/{ => shared}/utils.rs | 0 src/services/tools/mod.rs | 536 +++++ .../mod.rs} | 0 src/services/whatsapp/mod.rs | 176 ++ static/index.html | 484 +++++ test.rs | 0 38 files changed, 4650 insertions(+), 431 deletions(-) create mode 100644 src/PROMPT.md create mode 100644 src/scripts/containers/cache.sh create mode 100644 src/scripts/containers/vector-db.sh create mode 100644 src/scripts/database/0004.sql create mode 100644 src/scripts/database/001_init.sql create mode 100644 src/services/auth/mod.rs rename src/services/{ => automation}/automation.rs (100%) rename src/services/{ => bot}/bot.md (100%) create mode 100644 src/services/bot/orchestrator.rs create mode 100644 src/services/channels/mod.rs create mode 100644 src/services/chart/chart.rs rename src/services/{config.rs => config/mod.rs} (100%) create mode 100644 src/services/context/mod.rs rename src/services/{ => email}/email.rs (100%) rename src/services/{ => email}/llm-email.md (100%) rename src/services/{ => file}/file.rs (100%) rename src/services/{ => llm}/llm.rs (100%) rename src/services/{ => llm}/llm_generic.rs (100%) rename src/services/{ => llm}/llm_local.rs (100%) rename src/services/{ => llm}/llm_provider.rs (100%) create mode 100644 src/services/llm/mod.rs rename src/{services.rs => services/mod.rs} (53%) rename src/services/{ => org}/org.md (100%) rename src/services/{ => script}/script.rs (100%) create mode 100644 src/services/session/mod.rs create mode 100644 src/services/shared/mod.rs create mode 100644 src/services/shared/shared.rs rename src/services/{ => shared}/state.rs (66%) rename src/services/{ => shared}/utils.rs (100%) create mode 100644 src/services/tools/mod.rs rename src/services/{web_automation.rs => web_automation/mod.rs} (100%) create mode 100644 src/services/whatsapp/mod.rs create mode 100644 static/index.html delete mode 100644 test.rs diff --git a/Cargo.lock b/Cargo.lock index d906a7b..c44b8cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,13 +21,13 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" dependencies = [ "actix-utils", "actix-web", - "derive_more 0.99.20", + "derive_more 2.0.1", "futures-util", "log", "once_cell", @@ -52,7 +52,7 @@ dependencies = [ "derive_more 2.0.1", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.1.5", "futures-core", "h2 0.3.26", "http 0.2.12", @@ -70,7 +70,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "zstd", + "zstd 0.13.3", ] [[package]] @@ -85,14 +85,13 @@ dependencies = [ [[package]] name = "actix-multipart" -version = "0.6.2" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d974dd6c4f78d102d057c672dcf6faa618fafa9df91d44f9c466688fc1275a3a" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "bytes", "derive_more 0.99.20", "futures-core", "futures-util", @@ -111,9 +110,9 @@ dependencies = [ [[package]] name = "actix-multipart-derive" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" dependencies = [ "darling", "parse-size", @@ -205,7 +204,7 @@ dependencies = [ "cookie", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -268,6 +267,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -279,6 +288,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -403,6 +426,24 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash 0.5.0", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -475,7 +516,7 @@ checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.3.0", + "fastrand", "futures-lite", "pin-project-lite", "slab", @@ -662,6 +703,53 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backoff" version = "0.4.0" @@ -745,6 +833,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -767,6 +864,16 @@ dependencies = [ "piper", ] +[[package]] +name = "bmrng" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54df9073108f1558f90ae6c5bf5ab9c917c4185f5527b280c87a993cbead0ac" +dependencies = [ + "futures-core", + "tokio", +] + [[package]] name = "brotli" version = "8.0.1" @@ -834,24 +941,51 @@ dependencies = [ [[package]] name = "bzip2" -version = "0.6.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ - "libbz2-rs-sys", + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", ] [[package]] name = "cc" -version = "1.2.27" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.1" @@ -886,7 +1020,17 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", ] [[package]] @@ -899,6 +1043,43 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -914,6 +1095,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -949,6 +1144,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1059,6 +1260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1106,6 +1308,78 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cxx" +version = "1.0.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9c4fe7f2f5dc5c62871a1b43992d197da6fa1394656a94276ac2894a90a6fe" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5cf2909d37d80633ddd208676fc27c2608a7f035fff69c882421168038b26dd" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap 2.9.0", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.103", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077f5ee3d3bfd8d27f83208fdaa96ddd50af7f096c77077cc4b94da10bfacefd" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap 2.9.0", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0108748615125b9f2e915dfafdffcbdabbca9b15102834f6d7e9a768f2f2864" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e896681ef9b8dc462cfa6961d61909704bde0984b30bcb4082fe102b478890" +dependencies = [ + "indexmap 2.9.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.103", +] + [[package]] name = "darling" version = "0.20.11" @@ -1365,9 +1639,9 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.2.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87260449b06739ee78d6281c68d2a0ff3e3af64a78df63d3a1aeb3c06997c8a" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" dependencies = [ "base64 0.22.1", "memchr", @@ -1398,19 +1672,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -1489,6 +1750,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fancy-regex" version = "0.12.0" @@ -1499,43 +1766,24 @@ dependencies = [ "regex", ] -[[package]] -name = "fantoccini" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64" -dependencies = [ - "base64 0.13.1", - "cookie", - "futures-core", - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "hyper-rustls 0.23.2", - "mime", - "serde", - "serde_json", - "time", - "tokio", - "url", - "webdriver", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.2" @@ -1543,7 +1791,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", ] @@ -1555,7 +1802,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1570,6 +1817,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1594,6 +1847,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futf" version = "0.1.5" @@ -1669,7 +1932,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ - "fastrand 2.3.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -1740,27 +2003,34 @@ dependencies = [ "actix-multipart", "actix-web", "actix-ws", + "aes-gcm", "anyhow", + "argon2", "async-stream", + "async-trait", + "base64 0.22.1", "bytes", "chrono", "dotenv", "downloader", - "env_logger 0.10.2", + "env_logger", "futures", "futures-util", "imap", "langchain-rust", "lettre", + "livekit", "log", "mailparse", "minio", "native-tls", "num-format", + "qdrant-client", + "redis", "regex", - "reqwest 0.11.27", + "reqwest 0.12.20", "rhai", - "scraper 0.18.1", + "scraper", "serde", "serde_json", "smartstring", @@ -1773,7 +2043,7 @@ dependencies = [ "tracing-subscriber", "urlencoding", "uuid", - "zip", + "zip 2.4.2", ] [[package]] @@ -1822,6 +2092,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1858,7 +2138,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1877,13 +2157,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1899,14 +2185,19 @@ name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.4", ] [[package]] @@ -1914,9 +2205,6 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -1965,13 +2253,13 @@ dependencies = [ [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows-link 0.1.3", ] [[package]] @@ -2079,12 +2367,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "hyper" version = "0.14.32" @@ -2132,17 +2414,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ + "futures-util", "http 0.2.12", "hyper 0.14.32", - "log", - "rustls 0.20.9", - "rustls-native-certs 0.6.3", + "rustls 0.21.12", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", ] [[package]] @@ -2160,6 +2441,20 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.6.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] @@ -2333,16 +2628,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -2395,6 +2680,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -2439,23 +2734,30 @@ dependencies = [ "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2495,6 +2797,28 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.33" @@ -2515,6 +2839,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2541,14 +2878,17 @@ dependencies = [ "html-escape", "log", "mockito", + "pgvector", + "qdrant-client", "readability", "regex", "reqwest 0.12.20", "reqwest-eventsource", - "scraper 0.20.0", + "scraper", "secrecy", "serde", "serde_json", + "sqlx", "strum_macros", "text-splitter", "thiserror 1.0.69", @@ -2557,6 +2897,7 @@ dependencies = [ "tokio-stream", "url", "urlencoding", + "uuid", ] [[package]] @@ -2571,33 +2912,35 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] [[package]] name = "lettre" -version = "0.10.4" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" dependencies = [ "async-trait", - "base64 0.21.7", + "base64 0.22.1", + "chumsky", "email-encoding", "email_address", - "fastrand 1.9.0", + "fastrand", "futures-io", "futures-util", "hostname", "httpdate", - "idna 0.3.0", + "idna", "mime", "native-tls", - "nom 7.1.3", - "once_cell", + "nom 8.0.0", + "percent-encoding", "quoted_printable", - "socket2 0.4.10", + "socket2 0.6.0", "tokio", "tokio-native-tls", + "url", ] [[package]] @@ -2613,12 +2956,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "libbz2-rs-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775bf80d5878ab7c2b1080b5351a48b2f737d9f6f8b383574eebcc22be0dfccb" - [[package]] name = "libc" version = "0.2.174" @@ -2626,23 +2963,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] -name = "liblzma" -version = "0.4.2" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "liblzma-sys", -] - -[[package]] -name = "liblzma-sys" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" -dependencies = [ - "cc", - "libc", - "pkg-config", + "cfg-if", + "windows-link 0.2.0", ] [[package]] @@ -2653,9 +2980,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -2663,12 +2990,36 @@ dependencies = [ ] [[package]] -name = "libz-rs-sys" -version = "0.5.1" +name = "libwebrtc" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +checksum = "dcab6479e94215dc7cebc4e7c160a8f3f1e183627fca5a398f8c60fd41057ae1" dependencies = [ - "zlib-rs", + "cxx", + "jni", + "js-sys", + "lazy_static", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webrtc-sys", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", ] [[package]] @@ -2683,6 +3034,88 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "livekit" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01bc9ff6e8aca0bd58dc64a577d41fc9723961601e6fd9960657bac965f477b9" +dependencies = [ + "bmrng", + "bytes", + "chrono", + "futures-util", + "lazy_static", + "libloading", + "libwebrtc", + "livekit-api", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "prost 0.12.6", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "livekit-api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2862901b8feed084795e1163632e47f15d6f2c181ab62c15db02225f8621553a" +dependencies = [ + "base64 0.21.7", + "futures-util", + "http 0.2.12", + "jsonwebtoken", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "pbjson-types", + "prost 0.12.6", + "rand 0.9.1", + "reqwest 0.11.27", + "scopeguard", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "url", +] + +[[package]] +name = "livekit-protocol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768d2eb8242eb9b5f87868ae5498eb3f7653f6830d99d0131433a6fe00e26d20" +dependencies = [ + "futures-util", + "livekit-runtime", + "parking_lot", + "pbjson", + "pbjson-types", + "prost 0.12.6", + "prost-types 0.12.6", + "serde", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "livekit-runtime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" +dependencies = [ + "tokio", + "tokio-stream", +] + [[package]] name = "local-channel" version = "0.1.5" @@ -2725,6 +3158,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -2733,9 +3187,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mailparse" -version = "0.13.8" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +checksum = "3da03d5980411a724e8aaf7b61a7b5e386ec55a7fb49ee3d0ff79efc7e5e7c7e" dependencies = [ "charset", "data-encoding", @@ -2783,10 +3237,10 @@ dependencies = [ ] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "md-5" @@ -2848,7 +3302,7 @@ dependencies = [ "crc", "dashmap", "derivative", - "env_logger 0.11.8", + "env_logger", "futures", "futures-util", "hex", @@ -2968,6 +3422,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2978,6 +3441,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -3065,6 +3538,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.73" @@ -3150,12 +3629,83 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbjson" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +dependencies = [ + "heck 0.4.1", + "itertools 0.11.0", + "prost 0.12.6", + "prost-types 0.12.6", +] + +[[package]] +name = "pbjson-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +dependencies = [ + "bytes", + "chrono", + "pbjson", + "pbjson-build", + "prost 0.12.6", + "prost-build", + "serde", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash 0.4.2", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3181,6 +3731,27 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.9.0", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "bytes", + "postgres-types", + "sqlx", +] + [[package]] name = "phf" version = "0.10.1" @@ -3271,6 +3842,26 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3290,7 +3881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.3.0", + "fastrand", "futures-io", ] @@ -3336,6 +3927,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -3351,6 +3954,35 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postgres-protocol" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.1", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a120daaabfcb0e324d5bf6e411e9222994cb3795c79943a0ef28ed27ea76e4" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -3366,12 +3998,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3387,6 +4013,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.103", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -3396,6 +4032,100 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.12.6", + "prost-types 0.12.6", + "regex", + "syn 2.0.103", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost 0.13.5", +] + +[[package]] +name = "psm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +dependencies = [ + "cc", +] + [[package]] name = "pulldown-cmark" version = "0.12.2" @@ -3407,6 +4137,27 @@ dependencies = [ "unicase", ] +[[package]] +name = "qdrant-client" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c53f69cf32af8172f77d707618cdd605e32a4b90cd17f3c6fb48058e2181ad0" +dependencies = [ + "anyhow", + "derive_builder", + "futures", + "futures-util", + "prost 0.13.5", + "prost-types 0.13.5", + "reqwest 0.12.20", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tonic", +] + [[package]] name = "quinn" version = "0.11.8" @@ -3437,7 +4188,7 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.1", - "ring 0.17.14", + "ring", "rustc-hash 2.1.1", "rustls 0.23.28", "rustls-pki-types", @@ -3473,9 +4224,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.8" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r-efi" @@ -3556,6 +4307,30 @@ dependencies = [ "url", ] +[[package]] +name = "redis" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itertools 0.13.0", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.13" @@ -3615,6 +4390,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", + "hyper-rustls 0.24.2", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -3624,7 +4400,9 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -3632,12 +4410,11 @@ dependencies = [ "system-configuration 0.5.1", "tokio", "tokio-native-tls", - "tokio-util", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "winreg", ] @@ -3680,7 +4457,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", - "tower", + "tower 0.5.2", "tower-http", "tower-service", "url", @@ -3688,6 +4465,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 1.0.2", ] [[package]] @@ -3734,21 +4512,6 @@ dependencies = [ "syn 2.0.103", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.14" @@ -3759,7 +4522,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -3823,25 +4586,14 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "ring 0.17.14", + "log", + "ring", "rustls-webpki 0.101.7", "sct", ] @@ -3852,8 +4604,9 @@ version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ + "log", "once_cell", - "ring 0.17.14", + "ring", "rustls-pki-types", "rustls-webpki 0.103.3", "subtle", @@ -3867,7 +4620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "schannel", "security-framework 2.11.1", ] @@ -3893,6 +4646,15 @@ dependencies = [ "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" @@ -3909,8 +4671,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -3919,9 +4681,9 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "ring 0.17.14", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -3936,6 +4698,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.27" @@ -3951,22 +4722,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scraper" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" -dependencies = [ - "ahash", - "cssparser", - "ego-tree", - "getopts", - "html5ever 0.26.0", - "once_cell", - "selectors", - "tendril", -] - [[package]] name = "scraper" version = "0.20.0" @@ -3983,14 +4738,20 @@ dependencies = [ "tendril", ] +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "sct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -4090,7 +4851,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap", + "indexmap 2.9.0", "itoa", "memchr", "ryu", @@ -4149,6 +4910,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -4229,6 +4996,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smartstring" @@ -4241,16 +5011,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.10" @@ -4262,10 +5022,14 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.5.2" +name = "socket2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] [[package]] name = "spin" @@ -4286,21 +5050,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom 7.1.3", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4311,71 +5065,66 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "chrono", "crc", "crossbeam-queue", "either", - "event-listener 2.5.3", - "futures-channel", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.4", "hashlink", - "hex", - "indexmap", + "indexmap 2.9.0", "log", "memchr", + "native-tls", "once_cell", - "paste", "percent-encoding", - "rustls 0.21.12", - "rustls-pemfile", + "rustls 0.23.28", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.103", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -4387,20 +5136,19 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn 2.0.103", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.9.1", "byteorder", "bytes", @@ -4431,7 +5179,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tracing", "uuid", @@ -4440,12 +5188,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.9.1", "byteorder", "chrono", @@ -4454,7 +5202,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -4472,7 +5219,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tracing", "uuid", @@ -4481,9 +5228,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -4497,11 +5244,12 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror 2.0.12", "time", "tracing", "url", - "urlencoding", "uuid", ] @@ -4511,6 +5259,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -4544,9 +5305,9 @@ dependencies = [ [[package]] name = "stringmatch" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c0faab770316c3838f895fc2dfc3a8707ef4da48676f1014e1061ebd583b40" +checksum = "6aadc0801d92f0cdc26127c67c4b8766284f52a5ba22894f285e3101fa57d05d" dependencies = [ "regex", ] @@ -4692,7 +5453,7 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "fastrand 2.3.0", + "fastrand", "getrandom 0.3.3", "once_cell", "rustix", @@ -4728,7 +5489,7 @@ dependencies = [ "ahash", "auto_enums", "either", - "itertools", + "itertools 0.13.0", "once_cell", "pulldown-cmark", "regex", @@ -4746,27 +5507,42 @@ checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" [[package]] name = "thirtyfour" -version = "0.30.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60aded5a858cc767f950549a9c0eecd61cb4648ad2c7f0ada5cbd7f441cb6ac3" +checksum = "940c3778665cf311d848d8fa4207377c2ee8e5ddbb8c6fb4cff3f33072b2eb26" dependencies = [ + "arc-swap", "async-trait", - "base64 0.13.1", - "chrono", - "cookie", - "fantoccini", + "base64 0.22.1", + "bytes", + "cfg-if", "futures", - "http 0.2.12", - "log", + "http 1.3.1", + "indexmap 2.9.0", "parking_lot", + "paste", + "reqwest 0.12.20", "serde", "serde_json", "serde_repr", "stringmatch", + "strum", + "thirtyfour-macros", "thiserror 1.0.69", "tokio", + "tracing", "url", - "urlparse", +] + +[[package]] +name = "thirtyfour-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72d056365e368fc57a56d0cec9e41b02fb4a3474a61c8735262b1cfebe67425" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", ] [[package]] @@ -4939,13 +5715,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.20.9", + "rustls 0.21.12", "tokio", - "webpki", ] [[package]] @@ -4969,6 +5744,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.15" @@ -4982,6 +5769,60 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "flate2", + "h2 0.4.10", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.5", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", + "socket2 0.5.10", + "tokio", + "tokio-rustls 0.26.2", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -5010,7 +5851,7 @@ dependencies = [ "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -5091,6 +5932,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.18.0" @@ -5149,16 +6009,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] name = "untrusted" @@ -5173,7 +6031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] @@ -5183,12 +6041,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "urlparse" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" - [[package]] name = "utf-8" version = "0.7.6" @@ -5249,6 +6101,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5384,39 +6246,51 @@ dependencies = [ ] [[package]] -name = "webdriver" -version = "0.46.0" +name = "webpki-roots" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "base64 0.13.1", - "bytes", - "cookie", - "http 0.2.12", - "log", - "serde", - "serde_derive", - "serde_json", - "time", - "unicode-segmentation", - "url", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "0.25.4" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webrtc-sys" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3427b276be65e4018ff02886221b8f4aefafca22b36d0748dc3e795b1679dcca" +dependencies = [ + "cc", + "cxx", + "cxx-build", + "glob", + "log", + "webrtc-sys-build", +] + +[[package]] +name = "webrtc-sys-build" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a265f069d8fedb448d206b51554000a45d1f09b2efc62b57ef4deb6fd2384f8c" +dependencies = [ + "anyhow", + "fs2", + "regex", + "reqwest 0.11.27", + "scratch", + "semver", + "zip 0.6.6", +] [[package]] name = "whoami" @@ -5467,7 +6341,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -5500,13 +6374,19 @@ 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-registry" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -5517,7 +6397,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5526,7 +6406,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -5565,6 +6454,21 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5612,6 +6516,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5630,6 +6540,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5648,6 +6564,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5678,6 +6600,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5696,6 +6624,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5714,6 +6648,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5732,6 +6672,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -5801,6 +6747,15 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.0" @@ -5921,36 +6876,53 @@ dependencies = [ [[package]] name = "zip" -version = "4.3.0" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "aes", - "arbitrary", - "bzip2", - "constant_time_eq", + "byteorder", + "bzip2 0.4.4", + "constant_time_eq 0.1.5", "crc32fast", - "deflate64", + "crossbeam-utils", "flate2", - "getrandom 0.3.3", "hmac", - "indexmap", - "liblzma", - "memchr", - "pbkdf2", - "ppmd-rust", + "pbkdf2 0.11.0", "sha1", "time", - "zeroize", - "zopfli", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] -name = "zlib-rs" -version = "0.5.1" +name = "zip" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2 0.5.2", + "constant_time_eq 0.3.1", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap 2.9.0", + "lzma-rs", + "memchr", + "pbkdf2 0.12.2", + "sha1", + "thiserror 2.0.12", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd 0.13.3", +] [[package]] name = "zopfli" @@ -5964,13 +6936,32 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe", + "zstd-safe 7.2.4", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 46ba11d..1337375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,60 +4,58 @@ version = "0.1.0" edition = "2021" authors = ["Rodrigo Rodriguez "] description = "General Bots Server" -license = "AGPL" +license = "AGPL-3.0" repository = "https://alm.pragmatismo.com.br/generalbots/gbserver" [features] -default = [] +default = ["postgres", "qdrant"] local_llm = [] +postgres = ["sqlx/postgres"] +qdrant = ["langchain-rust/qdrant"] [dependencies] -actix-cors = "0.6" -actix-multipart = "0.6" -actix-web = "4" -actix-ws = "0.3.0" -thirtyfour = { version = "0.30" } -downloader = "0.2.8" +actix-cors = "0.7" +actix-multipart = "0.7" +actix-web = "4.9" +actix-ws = "0.3" anyhow = "1.0" async-stream = "0.3" -bytes = "1.1" +async-trait = "0.1" +aes-gcm = "0.10" +argon2 = "0.5" +base64 = "0.22" +bytes = "1.8" chrono = { version = "0.4", features = ["serde"] } dotenv = "0.15" -env_logger = "0.10" +downloader = "0.2" +env_logger = "0.11" futures = "0.3" futures-util = "0.3" -imap = "2.0" -langchain-rust = "4.4.3" -lettre = { version = "0.10", features = [ - "smtp-transport", - "builder", - "tokio1", - "tokio1-native-tls", -] } -log = "0.4.28" -mailparse = "0.13" +imap = "2.4" +langchain-rust = { version = "4.6", features = ["qdrant", "postgres"] } +lettre = { version = "0.11", features = ["smtp-transport", "builder", "tokio1", "tokio1-native-tls"] } +livekit = "0.7" +log = "0.4" +mailparse = "0.15" minio = { git = "https://github.com/minio/minio-rs", branch = "master" } native-tls = "0.2" -reqwest = { version = "0.11", features = ["json", "stream"] } -rhai = "1.22.2" +num-format = "0.4" +qdrant-client = "1.12" +rhai = "1.22" +redis = { version = "0.27", features = ["tokio-comp"] } +regex = "1.11" +reqwest = { version = "0.12", features = ["json", "stream"] } +scraper = "0.20" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -smartstring = "1.0" # Use the latest version from crates.io -sqlx = { version = "0.7", features = [ - "time", - "uuid", - "runtime-tokio-rustls", - "postgres", - "chrono", -] } +smartstring = "1.0" +sqlx = { version = "0.8", features = ["time", "uuid", "runtime-tokio-rustls", "postgres", "chrono"] } tempfile = "3" -tokio = { version = "1", features = ["full"] } -tokio-stream = "0.1.17" +thirtyfour = "0.34" +tokio = { version = "1.41", features = ["full"] } +tokio-stream = "0.1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt"] } -scraper = "0.18" urlencoding = "2.1" -regex = "1.10" -uuid = { version = "1.4", features = ["serde", "v4"] } # v4, v7, etc. as needed -zip = "4.3.0" -num-format = "0.4" \ No newline at end of file +uuid = { version = "1.11", features = ["serde", "v4"] } +zip = "2.2" diff --git a/src/PROMPT.md b/src/PROMPT.md new file mode 100644 index 0000000..df5aefd --- /dev/null +++ b/src/PROMPT.md @@ -0,0 +1,7 @@ +* Output a single `.sh` script using `cat` so it can be restored directly. +* No placeholders, only real, production-ready code. +* No comments, no explanations, no extra text. +* Follow KISS principles. +* Provide a complete, professional, working solution. +* If the script is too long, split into multiple parts, but always return the **entire code**. +* Output must be **only the code**, nothing else. diff --git a/src/main.rs b/src/main.rs index b30159f..70db9fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,30 @@ use actix_web::middleware::Logger; use log::info; +use qdrant_client::Qdrant; use std::sync::Arc; use actix_web::{web, App, HttpServer}; use dotenv::dotenv; -use services::state::*; -use services::{config::*, file::*}; use sqlx::PgPool; +use crate::services::auth::AuthService; use crate::services::automation::AutomationService; +use crate::services::channels::ChannelAdapter; +use crate::services::config::AppConfig; use crate::services::email::{ get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email, }; -use crate::services::llm::{chat, chat_stream}; +use crate::services::file::{list_file, upload_file}; use crate::services::llm_generic::generic_chat_completions; use crate::services::llm_local::{ chat_completions_local, embeddings_local, ensure_llama_servers_running, }; +use crate::services::orchestrator::BotOrchestrator; +use crate::services::session::SessionManager; +use crate::services::state::AppState; +use crate::services::tools::{RedisToolExecutor, ToolManager}; use crate::services::web_automation::{initialize_browser_pool, BrowserPool}; +use crate::services::whatsapp::WhatsAppAdapter; mod models; mod services; @@ -34,7 +41,7 @@ async fn main() -> std::io::Result<()> { let db = PgPool::connect(&db_url).await.unwrap(); let db_custom = PgPool::connect(&db_custom_url).await.unwrap(); - let minio_client = init_minio(&config) + let minio_client = services::file::init_minio(&config) .await .expect("Failed to initialize Minio"); @@ -52,47 +59,179 @@ async fn main() -> std::io::Result<()> { .await .expect("Failed to initialize browser pool"); + // Initialize Redis if available + let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "".to_string()); + let redis_conn = match std::env::var("REDIS_URL") { + Ok(redis_url_value) => { + let client = redis::Client::open(redis_url_value.clone()) + .expect("Failed to create Redis client"); + let conn = redis::aio::Connection::new(client) + .await + .expect("Failed to create Redis connection"); + Some(Arc::new(conn)) + } + Err(_) => None, + }; + + let qdrant_url = std::env::var("QDRANT_URL").unwrap_or("http://localhost:6334".to_string()); + let qdrant = Qdrant::from_url(&qdrant_url) + .build() + .expect("Failed to connect to Qdrant"); + + let session_manager = SessionManager::new(db.clone(), redis_conn.clone()); + let auth_service = AuthService::new(db.clone(), redis_conn.clone()); + + let llm_provider: Arc = + match std::env::var("LLM_PROVIDER") + .unwrap_or("mock".to_string()) + .as_str() + { + "openai" => Arc::new(crate::services::llm_local::OpenAIClient::new( + std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY required"), + )), + "anthropic" => Arc::new(crate::services::llm_local::AnthropicClient::new( + std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY required"), + )), + _ => Arc::new(crate::services::llm_local::MockLLMProvider::new()), + }; + + let web_adapter = Arc::new(crate::services::channels::WebChannelAdapter::new()); + let voice_adapter = Arc::new(crate::services::channels::VoiceAdapter::new( + std::env::var("LIVEKIT_URL").unwrap_or("ws://localhost:7880".to_string()), + std::env::var("LIVEKIT_API_KEY").unwrap_or("dev".to_string()), + std::env::var("LIVEKIT_API_SECRET").unwrap_or("secret".to_string()), + )); + + let whatsapp_adapter = Arc::new(crate::services::whatsapp::WhatsAppAdapter::new( + std::env::var("META_ACCESS_TOKEN").unwrap_or("".to_string()), + std::env::var("META_PHONE_NUMBER_ID").unwrap_or("".to_string()), + std::env::var("META_WEBHOOK_VERIFY_TOKEN").unwrap_or("".to_string()), + )); + + let tool_executor = Arc::new( + RedisToolExecutor::new( + redis_url.as_str(), + web_adapter.clone() as Arc, + db.clone(), + redis_conn.clone(), + ) + .expect("Failed to create RedisToolExecutor"), + ); + let chart_generator = ChartGenerator::new().map(Arc::new); + // Initialize LangChain components + let llm = OpenAI::default(); + let llm_provider: Arc = Arc::new(OpenAIClient::new(llm)); + + // Initialize vector store for document mode + let vector_store = if let (Ok(qdrant_url), Ok(openai_key)) = + (std::env::var("QDRANT_URL"), std::env::var("OPENAI_API_KEY")) + { + let embedder = OpenAiEmbedder::default().with_api_key(openai_key); + let client = QdrantClient::from_url(&qdrant_url).build().ok()?; + + let store = StoreBuilder::new() + .embedder(embedder) + .client(client) + .collection_name("documents") + .build() + .await + .ok()?; + + Some(Arc::new(store)) + } else { + None + }; + + // Initialize SQL chain for database mode + let sql_chain = if let Ok(db_url) = std::env::var("DATABASE_URL") { + let engine = PostgreSQLEngine::new(&db_url).await.ok()?; + let db = SQLDatabaseBuilder::new(engine).build().await.ok()?; + + let llm = OpenAI::default(); + let chain = langchain_rust::chain::SQLDatabaseChainBuilder::new() + .llm(llm) + .top_k(5) + .database(db) + .build() + .ok()?; + + Some(Arc::new(chain)) + } else { + None + }; + + let tool_manager = ToolManager::new(); + let orchestrator = BotOrchestrator::new( + session_manager, + tool_manager, + llm_provider, + auth_service, + chart_generator, + vector_store, + sql_chain, + ); + + orchestrator.add_channel("web", web_adapter.clone()); + orchestrator.add_channel("voice", voice_adapter.clone()); + orchestrator.add_channel("whatsapp", whatsapp_adapter.clone()); + + sqlx::query( + "INSERT INTO bots (id, name, llm_provider) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", + ) + .bind(uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()) + .bind("Default Bot") + .bind("mock") + .execute(&db) + .await + .unwrap(); + let app_state = web::Data::new(AppState { db: db.into(), db_custom: db_custom.into(), config: Some(config.clone()), minio_client: minio_client.into(), browser_pool: browser_pool.clone(), + orchestrator: Arc::new(orchestrator), + web_adapter, + voice_adapter, + whatsapp_adapter, }); // Start automation service in background - let automation_state = app_state.get_ref().clone(); // This gets the Arc + let automation_state = app_state.get_ref().clone(); let automation = AutomationService::new(automation_state, "src/prompts"); let _automation_handle = automation.spawn(); // Start HTTP server HttpServer::new(move || { - // let cors = Cors::default() - // .send_wildcard() - // .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"]) - // .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) - // .allowed_header(header::CONTENT_TYPE) - // .max_age(3600); - //.wrap(cors) - App::new() .wrap(Logger::default()) .wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i")) .app_data(app_state.clone()) + // Original services .service(upload_file) .service(list_file) .service(save_click) .service(get_emails) .service(list_emails) .service(send_email) - .service(chat_stream) - .service(chat) + .service(crate::services::orchestrator::chat_stream) + .service(crate::services::orchestrator::chat) .service(chat_completions_local) .service(save_draft) .service(generic_chat_completions) .service(embeddings_local) .service(get_latest_email_from) + .service(services::orchestrator::websocket_handler) + .service(services::orchestrator::whatsapp_webhook_verify) + .service(services::orchestrator::whatsapp_webhook) + .service(services::orchestrator::voice_start) + .service(services::orchestrator::voice_stop) + .service(services::orchestrator::create_session) + .service(services::orchestrator::get_sessions) + .service(services::orchestrator::get_session_history) + .service(services::orchestrator::index) }) .bind((config.server.host.clone(), config.server.port))? .run() diff --git a/src/scripts/containers/cache.sh b/src/scripts/containers/cache.sh new file mode 100644 index 0000000..1e78013 --- /dev/null +++ b/src/scripts/containers/cache.sh @@ -0,0 +1,7 @@ +curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/valkey.gpg +echo "deb [signed-by=/usr/share/keyrings/valkey.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/valkey.list +sudo apt update +sudo apt install valkey-server + +sudo systemctl enable valkey-server +sudo systemctl start valkey-server diff --git a/src/scripts/containers/email.sh b/src/scripts/containers/email.sh index ed0503d..94a166c 100644 --- a/src/scripts/containers/email.sh +++ b/src/scripts/containers/email.sh @@ -7,9 +7,14 @@ sudo iptables -A FORWARD -i $PUBLIC_INTERFACE -o lxcbr0 -p tcp -m multiport --dp sudo iptables -A FORWARD -i lxcbr0 -o $PUBLIC_INTERFACE -m state --state RELATED,ESTABLISHED -j ACCEPT sudo iptables -t nat -A POSTROUTING -o $PUBLIC_INTERFACE -j MASQUERADE +# IPv6 firewall +sudo ip6tables -A FORWARD -i $PUBLIC_INTERFACE -o lxcbr0 -p tcp -m multiport --dports 25,80,110,143,465,587,993,995,4190 -j ACCEPT +sudo ip6tables -A FORWARD -i lxcbr0 -o $PUBLIC_INTERFACE -m state --state RELATED,ESTABLISHED -j ACCEPT + # Save iptables rules permanently (adjust based on your distro) if command -v iptables-persistent >/dev/null; then sudo iptables-save | sudo tee /etc/iptables/rules.v4 + sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 fi @@ -61,9 +66,9 @@ sudo chown -R "$HOST_EMAIL_UID:$HOST_EMAIL_GID" "$HOST_BASE" # Mount directories echo "[CONTAINER] Mounting directories..." -lxc config device add emailprofile emaildata disk source="$HOST_DATA" path=/opt/gbo/data -lxc config device add emailprofile emailconf disk source="$HOST_CONF" path=/opt/gbo/conf -lxc config device add emailprofile emaillogs disk source="$HOST_LOGS" path=/opt/gbo/logs +lxc config device add "$PARAM_TENANT"-email emaildata disk source="$HOST_DATA" path=/opt/gbo/data +lxc config device add "$PARAM_TENANT"-email emailconf disk source="$HOST_CONF" path=/opt/gbo/conf +lxc config device add "$PARAM_TENANT"-email emaillogs disk source="$HOST_LOGS" path=/opt/gbo/logs # Create systemd service echo "[CONTAINER] Creating email service..." @@ -92,7 +97,11 @@ systemctl enable email systemctl start email " +# FIXED: IPv4 + IPv6 proxy devices for port in 25 80 110 143 465 587 993 995 4190; do - lxc config device remove email "port-$port" 2>/dev/null || true - lxc config device add email "port-$port" proxy listen=tcp:0.0.0.0:$port connect=tcp:127.0.0.1:$port + lxc config device remove "$PARAM_TENANT"-email "port-$port" 2>/dev/null || true + lxc config device add "$PARAM_TENANT"-email "port-$port" proxy \ + listen=tcp:0.0.0.0:$port \ + listen=tcp:[::]:$port \ + connect=tcp:127.0.0.1:$port done diff --git a/src/scripts/containers/vector-db.sh b/src/scripts/containers/vector-db.sh new file mode 100644 index 0000000..43c1709 --- /dev/null +++ b/src/scripts/containers/vector-db.sh @@ -0,0 +1,4 @@ +#!/bin/bash +wget https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-gnu.tar.gz +tar -xzf qdrant-x86_64-unknown-linux-gnu.tar.gz +./qdrant diff --git a/src/scripts/database/0004.sql b/src/scripts/database/0004.sql new file mode 100644 index 0000000..6e1beea --- /dev/null +++ b/src/scripts/database/0004.sql @@ -0,0 +1,129 @@ +-- User authentication and profiles +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + phone_number VARCHAR(50), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_active BOOLEAN DEFAULT true +); + +-- Bot configurations +CREATE TABLE IF NOT EXISTS bots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + llm_provider VARCHAR(100) NOT NULL, + llm_config JSONB NOT NULL DEFAULT '{}', + context_provider VARCHAR(100) NOT NULL, + context_config JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_active BOOLEAN DEFAULT true +); + +-- User sessions with optimized storage +CREATE TABLE IF NOT EXISTS user_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL DEFAULT 'New Conversation', + answer_mode VARCHAR(50) NOT NULL DEFAULT 'direct', + context_data JSONB NOT NULL DEFAULT '{}', + current_tool VARCHAR(255), + message_count INTEGER NOT NULL DEFAULT 0, + total_tokens INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, bot_id, title) +); + +-- Encrypted message history with analytics-friendly structure +CREATE TABLE IF NOT EXISTS message_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + session_id UUID NOT NULL REFERENCES user_sessions(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role VARCHAR(50) NOT NULL CHECK (role IN ('user', 'assistant', 'system')), + content_encrypted TEXT NOT NULL, + message_type VARCHAR(50) NOT NULL DEFAULT 'text', + media_url TEXT, + token_count INTEGER NOT NULL DEFAULT 0, + processing_time_ms INTEGER, + llm_model VARCHAR(100), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + message_index INTEGER NOT NULL +); + +-- Bot channel configurations +CREATE TABLE IF NOT EXISTS bot_channels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, + channel_type VARCHAR(50) NOT NULL CHECK (channel_type IN ('web', 'whatsapp', 'meet', 'api')), + config JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(bot_id, channel_type) +); + +-- WhatsApp number mappings +CREATE TABLE IF NOT EXISTS whatsapp_numbers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, + phone_number VARCHAR(50) NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(phone_number, bot_id) +); + +-- User email mappings for web channel +CREATE TABLE IF NOT EXISTS user_emails ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + email VARCHAR(255) NOT NULL, + is_primary BOOLEAN DEFAULT false, + verified BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(email) +); + +-- Tools registry +CREATE TABLE IF NOT EXISTS tools ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) UNIQUE NOT NULL, + description TEXT NOT NULL, + parameters JSONB NOT NULL DEFAULT '{}', + script TEXT NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Manual context injections +CREATE TABLE IF NOT EXISTS context_injections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + session_id UUID NOT NULL REFERENCES user_sessions(id) ON DELETE CASCADE, + injected_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + context_data JSONB NOT NULL, + reason TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Analytics tables +CREATE TABLE IF NOT EXISTS usage_analytics ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, + session_id UUID NOT NULL REFERENCES user_sessions(id) ON DELETE CASCADE, + date DATE NOT NULL DEFAULT CURRENT_DATE, + message_count INTEGER NOT NULL DEFAULT 0, + total_tokens INTEGER NOT NULL DEFAULT 0, + total_processing_time_ms INTEGER NOT NULL DEFAULT 0 +); + +-- Indexes for performance +CREATE INDEX IF NOT EXISTS idx_message_history_session_id ON message_history(session_id); +CREATE INDEX IF NOT EXISTS idx_message_history_created_at ON message_history(created_at); +CREATE INDEX IF NOT EXISTS idx_user_sessions_user_bot ON user_sessions(user_id, bot_id); +CREATE INDEX IF NOT EXISTS idx_usage_analytics_date ON usage_analytics(date); diff --git a/src/scripts/database/001_init.sql b/src/scripts/database/001_init.sql new file mode 100644 index 0000000..36a7731 --- /dev/null +++ b/src/scripts/database/001_init.sql @@ -0,0 +1,57 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS bots ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + llm_provider VARCHAR(100) NOT NULL, + config JSONB DEFAULT '{}', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_sessions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL, + context_data JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, bot_id) +); + +CREATE TABLE IF NOT EXISTS message_history ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + session_id UUID NOT NULL REFERENCES user_sessions(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role VARCHAR(50) NOT NULL, + content_encrypted TEXT NOT NULL, + message_type VARCHAR(50) DEFAULT 'text', + message_index INTEGER NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_sessions_bot_id ON user_sessions(bot_id); +CREATE INDEX IF NOT EXISTS idx_message_history_session_id ON message_history(session_id); +CREATE INDEX IF NOT EXISTS idx_message_history_user_id ON message_history(user_id); +CREATE INDEX IF NOT EXISTS idx_message_history_created_at ON message_history(created_at); + +INSERT INTO bots (id, name, llm_provider) +VALUES ('00000000-0000-0000-0000-000000000000', 'Default Bot', 'mock') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO users (id, username, email, password_hash) +VALUES ('00000000-0000-0000-0000-000000000001', 'demo', 'demo@example.com', '$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG') +ON CONFLICT (id) DO NOTHING; diff --git a/src/services/auth/mod.rs b/src/services/auth/mod.rs new file mode 100644 index 0000000..a43c8e9 --- /dev/null +++ b/src/services/auth/mod.rs @@ -0,0 +1,90 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use redis::aio::Connection as ConnectionManager; +use redis::AsyncCommands; +use sqlx::PgPool; +use std::sync::Arc; +use uuid::Uuid; + +pub struct AuthService { + pub pool: PgPool, + pub redis: Option>, +} + +impl AuthService { + pub fn new(pool: PgPool, redis: Option>) -> Self { + Self { pool, redis } + } + + pub async fn verify_user( + &self, + username: &str, + password: &str, + ) -> Result, Box> { + // Try Redis cache first + if let Some(redis) = &self.redis { + let cache_key = format!("auth:user:{}", username); + if let Ok(user_id_str) = redis.clone().get::<_, String>(cache_key).await { + if let Ok(user_id) = Uuid::parse_str(&user_id_str) { + return Ok(Some(user_id)); + } + } + } + + // Fallback to database + let user = sqlx::query( + "SELECT id, password_hash FROM users WHERE username = $1 AND is_active = true", + ) + .bind(username) + .fetch_optional(&self.pool) + .await?; + + if let Some(row) = user { + let user_id: Uuid = row.get("id"); + let password_hash: String = row.get("password_hash"); + let parsed_hash = PasswordHash::new(&password_hash)?; + if Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok() + { + // Cache in Redis + if let Some(redis) = &self.redis { + let cache_key = format!("auth:user:{}", username); + let _: () = redis + .clone() + .set_ex(cache_key, user_id.to_string(), 3600) + .await?; + } + return Ok(Some(user_id)); + } + } + Ok(None) + } + + pub async fn create_user( + &self, + username: &str, + email: &str, + password: &str, + ) -> Result> { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string(); + + let user_id = sqlx::query( + "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id", + ) + .bind(username) + .bind(email) + .bind(password_hash) + .fetch_one(&self.pool) + .await? + .get("id"); + + Ok(user_id) + } +} diff --git a/src/services/automation.rs b/src/services/automation/automation.rs similarity index 100% rename from src/services/automation.rs rename to src/services/automation/automation.rs diff --git a/src/services/bot.md b/src/services/bot/bot.md similarity index 100% rename from src/services/bot.md rename to src/services/bot/bot.md diff --git a/src/services/bot/orchestrator.rs b/src/services/bot/orchestrator.rs new file mode 100644 index 0000000..41f9b99 --- /dev/null +++ b/src/services/bot/orchestrator.rs @@ -0,0 +1,884 @@ +use actix_cors::Cors; +use actix_web::middleware::Logger; +use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Result}; +use actix_ws::Message; +use chrono::Utc; +use langchain_rust::{ + chain::{Chain, LLMChain, LLMChainBuilder}, + embedding::openai::openai_embedder::OpenAiEmbedder, + llm::openai::OpenAI, + memory::SimpleMemory, + prompt_args, + schemas::{Document, Message}, + tools::{postgres::PostgreSQLEngine, SQLDatabaseBuilder}, + vectorstore::qdrant::{Qdrant as LangChainQdrant, StoreBuilder}, + vectorstore::{VecStoreOptions, VectorStore}, +}; +use log::info; +use qdrant_client::qdrant::Qdrant as QdrantClient; +use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgPoolOptions, PgPool}; +use std::collections::HashMap; +use std::fs; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use uuid::Uuid; + +mod shared; +use shared::*; + +mod services; + +use services::auth::AuthService; +use services::channels::{ChannelAdapter, VoiceAdapter, WebChannelAdapter}; +use services::chart::ChartGenerator; +use services::llm::{AnthropicClient, LLMProvider, MockLLMProvider, OpenAIClient}; +use services::session::SessionManager; +use services::tools::ToolManager; +use services::whatsapp::WhatsAppAdapter; + +pub struct BotOrchestrator { + session_manager: SessionManager, + tool_manager: ToolManager, + llm_provider: Arc, + auth_service: AuthService, + channels: HashMap>, + response_channels: Arc>>>, + chart_generator: Option>, + vector_store: Option>>, + sql_chain: Option>, +} + +impl BotOrchestrator { + fn new( + session_manager: SessionManager, + tool_manager: ToolManager, + llm_provider: Arc, + auth_service: AuthService, + chart_generator: Option>, + vector_store: Option>>, + sql_chain: Option>, + ) -> Self { + Self { + session_manager, + tool_manager, + llm_provider, + auth_service, + channels: HashMap::new(), + response_channels: Arc::new(Mutex::new(HashMap::new())), + chart_generator, + vector_store, + sql_chain, + } + } + + fn add_channel(&mut self, channel_type: &str, adapter: Arc) { + self.channels.insert(channel_type.to_string(), adapter); + } + + async fn register_response_channel( + &self, + session_id: String, + sender: mpsc::Sender, + ) { + self.response_channels + .lock() + .await + .insert(session_id, sender); + } + + async fn set_user_answer_mode( + &self, + user_id: &str, + bot_id: &str, + mode: &str, + ) -> Result<(), Box> { + self.session_manager + .update_answer_mode(user_id, bot_id, mode) + .await?; + Ok(()) + } + + async fn process_message( + &self, + message: UserMessage, + ) -> Result<(), Box> { + info!( + "Processing message from channel: {}, user: {}", + message.channel, message.user_id + ); + + let user_id = Uuid::parse_str(&message.user_id).unwrap_or_else(|_| Uuid::new_v4()); + let bot_id = Uuid::parse_str(&message.bot_id) + .unwrap_or_else(|_| Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()); + + let session = match self + .session_manager + .get_user_session(user_id, bot_id) + .await? + { + Some(session) => session, + None => { + self.session_manager + .create_session(user_id, bot_id, "New Conversation") + .await? + } + }; + + // Check if we're in tool mode and there's an active tool + if session.answer_mode == "tool" && session.current_tool.is_some() { + self.tool_manager + .provide_user_response(&message.user_id, &message.bot_id, message.content.clone()) + .await?; + return Ok(()); + } + + self.session_manager + .save_message( + session.id, + user_id, + "user", + &message.content, + &message.message_type, + ) + .await?; + + // Handle different answer modes with LangChain integration + let response_content = match session.answer_mode.as_str() { + "document" => self.document_mode_handler(&message, &session).await?, + "chart" => self.chart_mode_handler(&message, &session).await?, + "database" => self.database_mode_handler(&message, &session).await?, + "tool" => self.tool_mode_handler(&message, &session).await?, + _ => self.direct_mode_handler(&message, &session).await?, + }; + + self.session_manager + .save_message(session.id, user_id, "assistant", &response_content, "text") + .await?; + + let bot_response = BotResponse { + bot_id: message.bot_id, + user_id: message.user_id, + session_id: message.session_id, + channel: message.channel, + content: response_content, + message_type: "text".to_string(), + stream_token: None, + is_complete: true, + }; + + if let Some(adapter) = self.channels.get(&message.channel) { + adapter.send_message(bot_response).await?; + } + + Ok(()) + } + + async fn document_mode_handler( + &self, + message: &UserMessage, + session: &UserSession, + ) -> Result> { + if let Some(vector_store) = &self.vector_store { + let similar_docs = vector_store + .similarity_search(&message.content, 3, &VecStoreOptions::default()) + .await?; + + let mut enhanced_prompt = format!("User question: {}\n\n", message.content); + + if !similar_docs.is_empty() { + enhanced_prompt.push_str("Relevant documents:\n"); + for (i, doc) in similar_docs.iter().enumerate() { + enhanced_prompt.push_str(&format!("[Doc {}]: {}\n", i + 1, doc.page_content)); + } + enhanced_prompt.push_str( + "\nPlease answer the user's question based on the provided documents.", + ); + } + + self.llm_provider + .generate(&enhanced_prompt, &serde_json::Value::Null) + .await + } else { + self.direct_mode_handler(message, session).await + } + } + + async fn chart_mode_handler( + &self, + message: &UserMessage, + session: &UserSession, + ) -> Result> { + if let Some(chart_generator) = &self.chart_generator { + let chart_response = chart_generator + .generate_chart(&message.content, "bar") + .await?; + + // Store chart generation in history + self.session_manager + .save_message( + session.id, + session.user_id, + "system", + &format!("Generated chart for query: {}", message.content), + "chart", + ) + .await?; + + Ok(format!( + "Chart generated for your query. Data retrieved: {}", + chart_response.sql_query + )) + } else { + // Fallback to document mode + self.document_mode_handler(message, session).await + } + } + + async fn database_mode_handler( + &self, + message: &UserMessage, + session: &UserSession, + ) -> Result> { + if let Some(sql_chain) = &self.sql_chain { + let input_variables = prompt_args! { + "input" => message.content, + }; + + let result = sql_chain.invoke(input_variables).await?; + Ok(result.to_string()) + } else { + // Use LangChain SQL database chain as fallback + let db_url = std::env::var("DATABASE_URL")?; + let engine = PostgreSQLEngine::new(&db_url).await?; + let db = SQLDatabaseBuilder::new(engine).build().await?; + + let llm = OpenAI::default(); + let chain = langchain_rust::chain::SQLDatabaseChainBuilder::new() + .llm(llm) + .top_k(5) + .database(db) + .build()?; + + let input_variables = chain.prompt_builder().query(&message.content).build(); + let result = chain.invoke(input_variables).await?; + + Ok(result.to_string()) + } + } + + async fn tool_mode_handler( + &self, + message: &UserMessage, + session: &UserSession, + ) -> Result> { + // Check if we should start a tool + if message.content.to_lowercase().contains("calculator") { + if let Some(adapter) = self.channels.get(&message.channel) { + let (tx, _rx) = mpsc::channel(100); + + self.register_response_channel(message.session_id.clone(), tx.clone()) + .await; + + let tool_manager = self.tool_manager.clone(); + let user_id_str = message.user_id.clone(); + let bot_id_str = message.bot_id.clone(); + let session_manager = self.session_manager.clone(); + + tokio::spawn(async move { + tool_manager + .execute_tool("calculator", &user_id_str, &bot_id_str, session_manager, tx) + .await + .unwrap_or_else(|e| { + log::error!("Error executing tool: {}", e); + }); + }); + } + Ok("Starting calculator tool...".to_string()) + } else { + // Fall back to normal LLM response with tool context + let available_tools = self.tool_manager.list_tools(); + let tools_context = if !available_tools.is_empty() { + format!("\n\nAvailable tools: {}. If the user needs calculations, suggest using the calculator tool.", available_tools.join(", ")) + } else { + String::new() + }; + + let full_prompt = format!("{}{}", message.content, tools_context); + + self.llm_provider + .generate(&full_prompt, &serde_json::Value::Null) + .await + } + } + + async fn direct_mode_handler( + &self, + message: &UserMessage, + session: &UserSession, + ) -> Result> { + // Get conversation history for context using LangChain memory + let history = self + .session_manager + .get_conversation_history(session.id, session.user_id) + .await?; + + let mut memory = SimpleMemory::new(); + for (role, content) in history { + match role.as_str() { + "user" => memory.add_user_message(&content), + "assistant" => memory.add_ai_message(&content), + _ => {} + } + } + + // Build prompt with memory context + let mut prompt = String::new(); + if let Some(chat_history) = memory.get_chat_history() { + for message in chat_history { + prompt.push_str(&format!( + "{}: {}\n", + message.message_type(), + message.content() + )); + } + } + prompt.push_str(&format!("User: {}\nAssistant:", message.content)); + + self.llm_provider + .generate(&prompt, &serde_json::Value::Null) + .await + } + + async fn stream_response( + &self, + message: UserMessage, + mut response_tx: mpsc::Sender, + ) -> Result<(), Box> { + info!("Streaming response for user: {}", message.user_id); + + let user_id = Uuid::parse_str(&message.user_id).unwrap_or_else(|_| Uuid::new_v4()); + let bot_id = Uuid::parse_str(&message.bot_id) + .unwrap_or_else(|_| Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()); + + let session = match self + .session_manager + .get_user_session(user_id, bot_id) + .await? + { + Some(session) => session, + None => { + self.session_manager + .create_session(user_id, bot_id, "New Conversation") + .await? + } + }; + + if session.answer_mode == "tool" && session.current_tool.is_some() { + self.tool_manager + .provide_user_response(&message.user_id, &message.bot_id, message.content.clone()) + .await?; + return Ok(()); + } + + self.session_manager + .save_message( + session.id, + user_id, + "user", + &message.content, + &message.message_type, + ) + .await?; + + // Get conversation history for streaming context + let history = self + .session_manager + .get_conversation_history(session.id, user_id) + .await?; + + let mut memory = SimpleMemory::new(); + for (role, content) in history { + match role.as_str() { + "user" => memory.add_user_message(&content), + "assistant" => memory.add_ai_message(&content), + _ => {} + } + } + + let mut prompt = String::new(); + if let Some(chat_history) = memory.get_chat_history() { + for message in chat_history { + prompt.push_str(&format!( + "{}: {}\n", + message.message_type(), + message.content() + )); + } + } + prompt.push_str(&format!("User: {}\nAssistant:", message.content)); + + let (stream_tx, mut stream_rx) = mpsc::channel(100); + let llm_provider = self.llm_provider.clone(); + let prompt_clone = prompt.clone(); + + tokio::spawn(async move { + let _ = llm_provider + .generate_stream(&prompt_clone, &serde_json::Value::Null, stream_tx) + .await; + }); + + let mut full_response = String::new(); + while let Some(chunk) = stream_rx.recv().await { + full_response.push_str(&chunk); + + let bot_response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: chunk, + message_type: "text".to_string(), + stream_token: None, + is_complete: false, + }; + + if response_tx.send(bot_response).await.is_err() { + break; + } + } + + self.session_manager + .save_message(session.id, user_id, "assistant", &full_response, "text") + .await?; + + let final_response = BotResponse { + bot_id: message.bot_id, + user_id: message.user_id, + session_id: message.session_id, + channel: message.channel, + content: "".to_string(), + message_type: "text".to_string(), + stream_token: None, + is_complete: true, + }; + + response_tx.send(final_response).await?; + Ok(()) + } + + async fn get_user_sessions( + &self, + user_id: Uuid, + ) -> Result, Box> { + self.session_manager.get_user_sessions(user_id).await + } + + async fn get_conversation_history( + &self, + session_id: Uuid, + user_id: Uuid, + ) -> Result, Box> { + self.session_manager + .get_conversation_history(session_id, user_id) + .await + } + + pub async fn process_message_with_tools( + &self, + message: UserMessage, + ) -> Result<(), Box> { + info!( + "Processing message with tools from user: {}", + message.user_id + ); + + let user_id = Uuid::parse_str(&message.user_id).unwrap_or_else(|_| Uuid::new_v4()); + let bot_id = Uuid::parse_str(&message.bot_id) + .unwrap_or_else(|_| Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()); + + let session = match self + .session_manager + .get_user_session(user_id, bot_id) + .await? + { + Some(session) => session, + None => { + self.session_manager + .create_session(user_id, bot_id, "New Conversation") + .await? + } + }; + + self.session_manager + .save_message( + session.id, + user_id, + "user", + &message.content, + &message.message_type, + ) + .await?; + + // Check if we're in a tool conversation + let is_tool_waiting = self + .tool_manager + .is_tool_waiting(&message.session_id) + .await + .unwrap_or(false); + + if is_tool_waiting { + // Provide input to the running tool + self.tool_manager + .provide_input(&message.session_id, &message.content) + .await?; + + // Get tool output and send it as a response + if let Ok(tool_output) = self.tool_manager.get_tool_output(&message.session_id).await { + for output in tool_output { + let bot_response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: output, + message_type: "text".to_string(), + stream_token: None, + is_complete: true, + }; + + if let Some(adapter) = self.channels.get(&message.channel) { + adapter.send_message(bot_response).await?; + } + } + } + return Ok(()); + } + + // Normal LLM processing with tool awareness + let available_tools = self.tool_manager.list_tools(); + let tools_context = if !available_tools.is_empty() { + format!("\n\nAvailable tools: {}. If the user needs calculations, suggest using the calculator tool.", available_tools.join(", ")) + } else { + String::new() + }; + + let full_prompt = format!("{}{}", message.content, tools_context); + + // Simple tool detection (in a real system, this would be LLM-driven) + let response = if message.content.to_lowercase().contains("calculator") + || message.content.to_lowercase().contains("calculate") + || message.content.to_lowercase().contains("math") + { + // Start calculator tool + match self + .tool_manager + .execute_tool("calculator", &message.session_id, &message.user_id) + .await + { + Ok(tool_result) => { + // Save tool start message + self.session_manager + .save_message( + session.id, + user_id, + "assistant", + &tool_result.output, + "tool_start", + ) + .await?; + + tool_result.output + } + Err(e) => { + format!("I encountered an error starting the calculator: {}", e) + } + } + } else { + // Normal LLM response + self.llm_provider + .generate(&full_prompt, &serde_json::Value::Null) + .await? + }; + + self.session_manager + .save_message(session.id, user_id, "assistant", &response, "text") + .await?; + + let bot_response = BotResponse { + bot_id: message.bot_id, + user_id: message.user_id, + session_id: message.session_id, + channel: message.channel, + content: response, + message_type: "text".to_string(), + stream_token: None, + is_complete: true, + }; + + if let Some(adapter) = self.channels.get(&message.channel) { + adapter.send_message(bot_response).await?; + } + + Ok(()) + } +} + +struct AppState { + orchestrator: Arc, + web_adapter: Arc, + voice_adapter: Arc, + whatsapp_adapter: Arc, +} + +#[actix_web::get("/ws")] +async fn websocket_handler( + req: HttpRequest, + stream: web::Payload, + data: web::Data, +) -> Result { + let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?; + let session_id = Uuid::new_v4().to_string(); + let (tx, mut rx) = mpsc::channel::(100); + + data.orchestrator + .register_response_channel(session_id.clone(), tx.clone()) + .await; + data.web_adapter + .add_connection(session_id.clone(), tx.clone()) + .await; + data.voice_adapter + .add_connection(session_id.clone(), tx.clone()) + .await; + + let orchestrator = data.orchestrator.clone(); + let web_adapter = data.web_adapter.clone(); + + actix_web::rt::spawn(async move { + while let Some(msg) = rx.recv().await { + if let Ok(json) = serde_json::to_string(&msg) { + let _ = session.text(json).await; + } + } + }); + + actix_web::rt::spawn(async move { + while let Some(Ok(msg)) = msg_stream.recv().await { + match msg { + Message::Text(text) => { + let user_message = UserMessage { + bot_id: "default_bot".to_string(), + user_id: "default_user".to_string(), + session_id: session_id.clone(), + channel: "web".to_string(), + content: text.to_string(), + message_type: "text".to_string(), + media_url: None, + timestamp: Utc::now(), + }; + + if let Err(e) = orchestrator.stream_response(user_message, tx.clone()).await { + info!("Error processing message: {}", e); + } + } + Message::Close(_) => { + web_adapter.remove_connection(&session_id).await; + break; + } + _ => {} + } + } + }); + + Ok(res) +} + +#[actix_web::get("/api/whatsapp/webhook")] +async fn whatsapp_webhook_verify( + data: web::Data, + web::Query(params): web::Query>, +) -> Result { + let mode = params.get("hub.mode").unwrap_or(&"".to_string()); + let token = params.get("hub.verify_token").unwrap_or(&"".to_string()); + let challenge = params.get("hub.challenge").unwrap_or(&"".to_string()); + + match data.whatsapp_adapter.verify_webhook(mode, token, challenge) { + Ok(challenge_response) => Ok(HttpResponse::Ok().body(challenge_response)), + Err(_) => Ok(HttpResponse::Forbidden().body("Verification failed")), + } +} + +#[actix_web::post("/api/whatsapp/webhook")] +async fn whatsapp_webhook( + data: web::Data, + payload: web::Json, +) -> Result { + match data + .whatsapp_adapter + .process_incoming_message(payload.into_inner()) + .await + { + Ok(user_messages) => { + for user_message in user_messages { + if let Err(e) = data.orchestrator.process_message(user_message).await { + log::error!("Error processing WhatsApp message: {}", e); + } + } + Ok(HttpResponse::Ok().body("")) + } + Err(e) => { + log::error!("Error processing WhatsApp webhook: {}", e); + Ok(HttpResponse::BadRequest().body("Invalid message")) + } + } +} + +#[actix_web::post("/api/voice/start")] +async fn voice_start( + data: web::Data, + info: web::Json, +) -> Result { + let session_id = info + .get("session_id") + .and_then(|s| s.as_str()) + .unwrap_or(""); + let user_id = info + .get("user_id") + .and_then(|u| u.as_str()) + .unwrap_or("user"); + + match data + .voice_adapter + .start_voice_session(session_id, user_id) + .await + { + Ok(token) => { + Ok(HttpResponse::Ok().json(serde_json::json!({"token": token, "status": "started"}))) + } + Err(e) => { + Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": e.to_string()}))) + } + } +} + +#[actix_web::post("/api/voice/stop")] +async fn voice_stop( + data: web::Data, + info: web::Json, +) -> Result { + let session_id = info + .get("session_id") + .and_then(|s| s.as_str()) + .unwrap_or(""); + + match data.voice_adapter.stop_voice_session(session_id).await { + Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "stopped"}))), + Err(e) => { + Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": e.to_string()}))) + } + } +} + +#[actix_web::post("/api/sessions")] +async fn create_session(data: web::Data) -> Result { + let session_id = Uuid::new_v4(); + Ok(HttpResponse::Ok().json(serde_json::json!({ + "session_id": session_id, + "title": "New Conversation", + "created_at": Utc::now() + }))) +} + +#[actix_web::get("/api/sessions")] +async fn get_sessions(data: web::Data) -> Result { + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + match data.orchestrator.get_user_sessions(user_id).await { + Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)), + Err(e) => { + Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": e.to_string()}))) + } + } +} + +#[actix_web::get("/api/sessions/{session_id}")] +async fn get_session_history( + data: web::Data, + path: web::Path, +) -> Result { + let session_id = path.into_inner(); + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + + match Uuid::parse_str(&session_id) { + Ok(session_uuid) => { + match data + .orchestrator + .get_conversation_history(session_uuid, user_id) + .await + { + Ok(history) => Ok(HttpResponse::Ok().json(history)), + Err(e) => Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": e.to_string()}))), + } + } + Err(_) => { + Ok(HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"}))) + } + } +} + +#[actix_web::post("/api/set_mode")] +async fn set_mode_handler( + data: web::Data, + info: web::Json>, +) -> Result { + let default_user = "default_user".to_string(); + let default_bot = "default_bot".to_string(); + let default_mode = "direct".to_string(); + + let user_id = info.get("user_id").unwrap_or(&default_user); + let bot_id = info.get("bot_id").unwrap_or(&default_bot); + let mode = info.get("mode").unwrap_or(&default_mode); + + if let Err(e) = data + .orchestrator + .set_user_answer_mode(user_id, bot_id, mode) + .await + { + return Ok( + HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) + ); + } + + Ok(HttpResponse::Ok().json(serde_json::json!({"status": "mode_updated"}))) +} + +#[actix_web::get("/")] +async fn index() -> Result { + let html = fs::read_to_string("templates/index.html") + .unwrap_or_else(|_| include_str!("../templates/index.html").to_string()); + Ok(HttpResponse::Ok().content_type("text/html").body(html)) +} + +#[actix_web::get("/static/{filename:.*}")] +async fn static_files(req: HttpRequest) -> Result { + let filename = req.match_info().query("filename"); + let path = format!("static/{}", filename); + + match fs::read(&path) { + Ok(content) => { + let content_type = match filename { + f if f.ends_with(".js") => "application/javascript", + f if f.ends_with(".css") => "text/css", + f if f.ends_with(".png") => "image/png", + f if f.ends_with(".jpg") | f.ends_with(".jpeg") => "image/jpeg", + _ => "text/plain", + }; + + Ok(HttpResponse::Ok().content_type(content_type).body(content)) + } + Err(_) => Ok(HttpResponse::NotFound().body("File not found")), + } +} diff --git a/src/services/channels/mod.rs b/src/services/channels/mod.rs new file mode 100644 index 0000000..1136175 --- /dev/null +++ b/src/services/channels/mod.rs @@ -0,0 +1,153 @@ +use async_trait::async_trait; +use livekit::{AccessToken, Room, RoomOptions, DataPacketKind}; +use log::info; +use tokio::sync::{mpsc, Mutex}; +use std::collections::HashMap; +use std::sync::Arc; +use chrono::Utc; + +use crate::shared::{UserMessage, BotResponse}; + +#[async_trait] +pub trait ChannelAdapter: Send + Sync { + async fn send_message(&self, response: BotResponse) -> Result<(), Box>; +} + +pub struct WebChannelAdapter { + connections: Arc>>>, +} + +impl WebChannelAdapter { + pub fn new() -> Self { + Self { + connections: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender) { + self.connections.lock().await.insert(session_id, tx); + } + + pub async fn remove_connection(&self, session_id: &str) { + self.connections.lock().await.remove(session_id); + } +} + +#[async_trait] +impl ChannelAdapter for WebChannelAdapter { + async fn send_message(&self, response: BotResponse) -> Result<(), Box> { + let connections = self.connections.lock().await; + if let Some(tx) = connections.get(&response.session_id) { + tx.send(response).await?; + } + Ok(()) + } +} + +pub struct VoiceAdapter { + livekit_url: String, + api_key: String, + api_secret: String, + rooms: Arc>>, + connections: Arc>>>, +} + +impl VoiceAdapter { + pub fn new(livekit_url: String, api_key: String, api_secret: String) -> Self { + Self { + livekit_url, + api_key, + api_secret, + rooms: Arc::new(Mutex::new(HashMap::new())), + connections: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub async fn start_voice_session(&self, session_id: &str, user_id: &str) -> Result> { + let token = AccessToken::with_api_key(&self.api_key, &self.api_secret) + .with_identity(user_id) + .with_name(user_id) + .with_room_name(session_id) + .with_room_join(true) + .to_jwt()?; + + let room_options = RoomOptions { + auto_subscribe: true, + ..Default::default() + }; + + let (room, mut events) = Room::connect(&self.livekit_url, &token, room_options).await?; + self.rooms.lock().await.insert(session_id.to_string(), room.clone()); + + let rooms_clone = self.rooms.clone(); + let connections_clone = self.connections.clone(); + let session_id_clone = session_id.to_string(); + + tokio::spawn(async move { + while let Some(event) = events.recv().await { + match event { + livekit::prelude::RoomEvent::DataReceived(data_packet) => { + if let Ok(message) = serde_json::from_slice::(&data_packet.data) { + info!("Received voice message: {}", message.content); + if let Some(tx) = connections_clone.lock().await.get(&message.session_id) { + let _ = tx.send(BotResponse { + bot_id: message.bot_id, + user_id: message.user_id, + session_id: message.session_id, + channel: "voice".to_string(), + content: format!("๐ŸŽค Voice: {}", message.content), + message_type: "voice".to_string(), + stream_token: None, + is_complete: true, + }).await; + } + } + } + livekit::prelude::RoomEvent::TrackSubscribed(track, publication, participant) => { + info!("Voice track subscribed from {}", participant.identity()); + } + _ => {} + } + } + rooms_clone.lock().await.remove(&session_id_clone); + }); + + Ok(token) + } + + pub async fn stop_voice_session(&self, session_id: &str) -> Result<(), Box> { + if let Some(room) = self.rooms.lock().await.remove(session_id) { + room.disconnect(); + } + Ok(()) + } + + pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender) { + self.connections.lock().await.insert(session_id, tx); + } + + pub async fn send_voice_response(&self, session_id: &str, text: &str) -> Result<(), Box> { + if let Some(room) = self.rooms.lock().await.get(session_id) { + let voice_response = serde_json::json!({ + "type": "voice_response", + "text": text, + "timestamp": Utc::now() + }); + + room.local_participant().publish_data( + serde_json::to_vec(&voice_response)?, + DataPacketKind::Reliable, + &[], + )?; + } + Ok(()) + } +} + +#[async_trait] +impl ChannelAdapter for VoiceAdapter { + async fn send_message(&self, response: BotResponse) -> Result<(), Box> { + info!("Sending voice response to: {}", response.user_id); + self.send_voice_response(&response.session_id, &response.content).await + } +} diff --git a/src/services/chart/chart.rs b/src/services/chart/chart.rs new file mode 100644 index 0000000..7ed78fd --- /dev/null +++ b/src/services/chart/chart.rs @@ -0,0 +1,92 @@ +use langchain_rust::{ + chain::{Chain, SQLDatabaseChainBuilder, options::ChainCallOptions}, + llm::openai::OpenAI, + tools::{postgres::PostgreSQLEngine, SQLDatabaseBuilder}, + prompt::PromptTemplate, +}; + +pub struct ChartGenerator { + sql_chain: SQLDatabaseChainBuilder, + llm: OpenAI, +} + +impl ChartGenerator { + pub async fn new(database_url: &str) -> Result> { + let llm = OpenAI::default(); + let engine = PostgreSQLEngine::new(database_url).await?; + let db = SQLDatabaseBuilder::new(engine).build().await?; + + let sql_chain = SQLDatabaseChainBuilder::new() + .llm(llm.clone()) + .top_k(4) + .database(db); + + Ok(Self { + sql_chain, + llm, + }) + } + + pub async fn generate_chart( + &self, + question: &str, + chart_type: &str + ) -> Result> { + // Step 1: Generate SQL using LangChain + let sql_result = self.generate_sql(question).await?; + + // Step 2: Execute SQL and get data + let data = self.execute_sql(&sql_result).await?; + + // Step 3: Generate chart configuration using LLM + let chart_config = self.generate_chart_config(&data, chart_type).await?; + + // Step 4: Generate and render chart + let chart_image = self.render_chart(&chart_config).await?; + + Ok(ChartResponse { + sql_query: sql_result, + data, + chart_image, + chart_config, + }) + } + + async fn generate_sql(&self, question: &str) -> Result> { + let chain = self.sql_chain + .clone() + .build() + .expect("Failed to build SQL chain"); + + let input_variables = chain.prompt_builder().query(question).build(); + let result = chain.invoke(input_variables).await?; + + Ok(result.to_string()) + } + + async fn execute_sql(&self, query: &str) -> Result> { + // Execute the generated SQL and return structured data + // Implementation depends on your database setup + Ok(Value::Null) + } + + async fn generate_chart_config(&self, data: &Value, chart_type: &str) -> Result> { + let prompt = format!( + "Given this data: {} and chart type: {}, generate a billboard.js configuration JSON. \ + Focus on creating meaningful visualizations for this business data.", + data, chart_type + ); + + let message = HumanMessage::new(prompt); + let result = self.llm.invoke(&[message]).await?; + + serde_json::from_str(&result.generation) + .map_err(|e| e.into()) + } + + async fn render_chart(&self, config: &Value) -> Result, Box> { + // Use headless browser to render chart and capture as image + // This would integrate with your browser automation setup + Ok(vec![]) + } +} diff --git a/src/services/config.rs b/src/services/config/mod.rs similarity index 100% rename from src/services/config.rs rename to src/services/config/mod.rs diff --git a/src/services/context/mod.rs b/src/services/context/mod.rs new file mode 100644 index 0000000..cea1836 --- /dev/null +++ b/src/services/context/mod.rs @@ -0,0 +1,97 @@ +use async_trait::async_trait; +use langchain_rust::{ + embedding::openai::openai_embedder::OpenAiEmbedder, + vectorstore::qdrant::{Qdrant, StoreBuilder}, + vectorstore::{VectorStore, VecStoreOptions}, + schemas::Document, +}; +use qdrant_client::qdrant::Qdrant as QdrantClient; +use sqlx::PgPool; +use uuid::Uuid; + +#[async_trait] +pub trait ContextProvider: Send + Sync { + async fn get_context(&self, session_id: Uuid, user_id: Uuid, query: &str) -> Result>; + async fn store_embedding(&self, text: &str, embedding: Vec, metadata: Value) -> Result<(), Box>; + async fn search_similar(&self, embedding: Vec, limit: u32) -> Result, Box>; +} + +pub struct LangChainContextProvider { + pool: PgPool, + vector_store: Qdrant, + embedder: OpenAiEmbedder, +} + +impl LangChainContextProvider { + pub async fn new(pool: PgPool, qdrant_url: &str) -> Result> { + let embedder = OpenAiEmbedder::default(); + + let client = QdrantClient::from_url(qdrant_url).build()?; + let vector_store = StoreBuilder::new() + .embedder(embedder.clone()) + .client(client) + .collection_name("conversations") + .build() + .await?; + + Ok(Self { + pool, + vector_store, + embedder, + }) + } +} + +#[async_trait] +impl ContextProvider for LangChainContextProvider { + async fn get_context(&self, session_id: Uuid, user_id: Uuid, query: &str) -> Result> { + // Get conversation history + let history = sqlx::query( + "SELECT role, content_encrypted FROM message_history + WHERE session_id = $1 AND user_id = $2 + ORDER BY message_index DESC LIMIT 5" + ) + .bind(session_id) + .bind(user_id) + .fetch_all(&self.pool) + .await?; + + let mut context = String::from("Conversation history:\n"); + for row in history.iter().rev() { + let role: String = row.get("role"); + let content: String = row.get("content_encrypted"); + context.push_str(&format!("{}: {}\n", role, content)); + } + + // Search for similar documents using LangChain + let similar_docs = self.vector_store + .similarity_search(query, 3, &VecStoreOptions::default()) + .await?; + + if !similar_docs.is_empty() { + context.push_str("\nRelevant context:\n"); + for doc in similar_docs { + context.push_str(&format!("- {}\n", doc.page_content)); + } + } + + context.push_str(&format!("\nCurrent message: {}", query)); + Ok(context) + } + + async fn store_embedding(&self, text: &str, embedding: Vec, metadata: Value) -> Result<(), Box> { + let document = Document::new(text).with_metadata(metadata); + + self.vector_store + .add_documents(&[document], &VecStoreOptions::default()) + .await?; + + Ok(()) + } + + async fn search_similar(&self, embedding: Vec, limit: u32) -> Result, Box> { + // LangChain handles this through the vector store interface + // This method would need adaptation to work with LangChain's search patterns + Ok(vec![]) + } +} diff --git a/src/services/email.rs b/src/services/email/email.rs similarity index 100% rename from src/services/email.rs rename to src/services/email/email.rs diff --git a/src/services/llm-email.md b/src/services/email/llm-email.md similarity index 100% rename from src/services/llm-email.md rename to src/services/email/llm-email.md diff --git a/src/services/file.rs b/src/services/file/file.rs similarity index 100% rename from src/services/file.rs rename to src/services/file/file.rs diff --git a/src/services/llm.rs b/src/services/llm/llm.rs similarity index 100% rename from src/services/llm.rs rename to src/services/llm/llm.rs diff --git a/src/services/llm_generic.rs b/src/services/llm/llm_generic.rs similarity index 100% rename from src/services/llm_generic.rs rename to src/services/llm/llm_generic.rs diff --git a/src/services/llm_local.rs b/src/services/llm/llm_local.rs similarity index 100% rename from src/services/llm_local.rs rename to src/services/llm/llm_local.rs diff --git a/src/services/llm_provider.rs b/src/services/llm/llm_provider.rs similarity index 100% rename from src/services/llm_provider.rs rename to src/services/llm/llm_provider.rs diff --git a/src/services/llm/mod.rs b/src/services/llm/mod.rs new file mode 100644 index 0000000..5171bcf --- /dev/null +++ b/src/services/llm/mod.rs @@ -0,0 +1,121 @@ +use async_trait::async_trait; +use futures::StreamExt; +use langchain_rust::{ + language_models::llm::LLM, + llm::{claude::Claude, openai::OpenAI}, + schemas::Message, +}; +use serde_json::Value; +use tokio::sync::mpsc; +pub mod llm_generic; +pub mod llm_local; +pub mod llm_provider; + +#[async_trait] +pub trait LLMProvider: Send + Sync { + async fn generate( + &self, + prompt: &str, + config: &Value, + ) -> Result>; + + async fn generate_stream( + &self, + prompt: &str, + config: &Value, + tx: mpsc::Sender, + ) -> Result<(), Box>; + + // Add tool calling capability + async fn generate_with_tools( + &self, + prompt: &str, + config: &serde_json::Value, + available_tools: &[String], + tool_manager: Arc, + session_id: &str, + user_id: &str, + ) -> Result>; +} + +pub struct OpenAIClient +where + C: langchain_rust::llm::Config, +{ + client: OpenAI, +} + +impl OpenAIClient +where + C: langchain_rust::llm::Config, +{ + pub fn new(config: C) -> Self { + let client = OpenAI::new(config); + Self { client } + } +} + +#[async_trait] +impl LLMProvider for OpenAIClient +where + C: langchain_rust::llm::Config + Send + Sync, +{ + async fn generate( + &self, + prompt: &str, + _config: &Value, + ) -> Result> { + // Call the underlying OpenAI client with the raw prompt string. + let result = self + .client + .invoke(prompt) + .await + .map_err(|e| Box::new(e) as Box)?; + + Ok(result) + } + + async fn generate_stream( + &self, + prompt: &str, + _config: &Value, + mut tx: mpsc::Sender, + ) -> Result<(), Box> { + // Build a message vector for the OpenAI streaming API + let messages = vec![Message::new_human_message(prompt.to_string())]; + + let mut stream = self + .client + .stream(&messages) + .await + .map_err(|e| Box::new(e) as Box)?; + + while let Some(result) = stream.next().await { + match result { + Ok(chunk) => { + // The `content` field is accessed directly (no method). + let content = chunk.content; + if !content.is_empty() { + let _ = tx.send(content.to_string()).await; + } + } + Err(e) => { + eprintln!("Stream error: {}", e); + } + } + } + + Ok(()) + } +} + +pub struct AnthropicClient { + client: Claude, +} + +impl AnthropicClient { + pub fn new(api_key: String) -> Self { + let client = Claude::default().with_api_key(api_key); + Self { client } + } +} diff --git a/src/services.rs b/src/services/mod.rs similarity index 53% rename from src/services.rs rename to src/services/mod.rs index e56db6a..4587321 100644 --- a/src/services.rs +++ b/src/services/mod.rs @@ -1,13 +1,17 @@ +pub mod auth; pub mod automation; +pub mod channels; pub mod config; +pub mod context; pub mod email; pub mod file; -pub mod keywords; pub mod llm; pub mod llm_generic; pub mod llm_local; -pub mod llm_provider; -pub mod script; +pub mod orchestrator; +pub mod session; +pub mod shared; pub mod state; -pub mod utils; +pub mod tools; pub mod web_automation; +pub mod whatsapp; diff --git a/src/services/org.md b/src/services/org/org.md similarity index 100% rename from src/services/org.md rename to src/services/org/org.md diff --git a/src/services/script.rs b/src/services/script/script.rs similarity index 100% rename from src/services/script.rs rename to src/services/script/script.rs diff --git a/src/services/session/mod.rs b/src/services/session/mod.rs new file mode 100644 index 0000000..5c211af --- /dev/null +++ b/src/services/session/mod.rs @@ -0,0 +1,175 @@ +use crate::services::shared::shared::UserSession; +use sqlx::Row; + +use redis::{aio::Connection as ConnectionManager, AsyncCommands}; +use serde_json; +use sqlx::PgPool; +use std::sync::Arc; +use uuid::Uuid; + +pub struct SessionManager { + pub pool: PgPool, + pub redis: Option>, +} + +impl SessionManager { + pub fn new(pool: PgPool, redis: Option>) -> Self { + Self { pool, redis } + } + + pub async fn get_user_session( + &self, + user_id: Uuid, + bot_id: Uuid, + ) -> Result, Box> { + // Try Redis cache first + if let Some(redis) = &self.redis { + let cache_key = format!("session:{}:{}", user_id, bot_id); + let mut conn = redis.clone().lock().await; + if let Ok(session_json) = conn.get::<_, String>(cache_key).await { + if let Ok(session) = serde_json::from_str::(&session_json) { + return Ok(Some(session)); + } + } + } + + // Fallback to database + let session = sqlx::query_as( + "SELECT * FROM user_sessions WHERE user_id = $1 AND bot_id = $2 ORDER BY updated_at DESC LIMIT 1" + ) + .bind(user_id) + .bind(bot_id) + .fetch_optional(&self.pool) + .await?; + + // Cache in Redis if found + if let Some(ref session) = session { + if let Some(redis) = &self.redis { + let cache_key = format!("session:{}:{}", user_id, bot_id); + let session_json = serde_json::to_string(session)?; + let mut conn = redis.clone().lock().await; + let _: () = conn.set_ex(cache_key, session_json, 1800).await?; + } + } + + Ok(session) + } + + pub async fn create_session( + &self, + user_id: Uuid, + bot_id: Uuid, + title: &str, + ) -> Result> { + log::info!( + "Creating new session for user: {}, bot: {}", + user_id, + bot_id + ); + let session = sqlx::query_as( + "INSERT INTO user_sessions (user_id, bot_id, title) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(user_id) + .bind(bot_id) + .bind(title) + .fetch_one(&self.pool) + .await?; + + // Cache in Redis + if let Some(redis) = &self.redis { + let cache_key = format!("session:{}:{}", user_id, bot_id); + let session_json = serde_json::to_string(&session)?; + let _: () = redis.clone().set_ex(cache_key, session_json, 1800).await?; + } + + Ok(session) + } + + pub async fn save_message( + &self, + session_id: Uuid, + user_id: Uuid, + role: &str, + content: &str, + message_type: &str, + ) -> Result<(), Box> { + let message_count: i32 = + sqlx::query("SELECT COUNT(*) as count FROM message_history WHERE session_id = $1") + .bind(session_id) + .fetch_one(&self.pool) + .await? + .get("count"); + + sqlx::query( + "INSERT INTO message_history (session_id, user_id, role, content_encrypted, message_type, message_index) + VALUES ($1, $2, $3, $4, $5, $6)" + ) + .bind(session_id) + .bind(user_id) + .bind(role) + .bind(content) // Note: Encryption removed for simplicity + .bind(message_type) + .bind(message_count + 1) + .execute(&self.pool) + .await?; + + sqlx::query("UPDATE user_sessions SET updated_at = NOW() WHERE id = $1") + .bind(session_id) + .execute(&self.pool) + .await?; + + // Invalidate session cache + if let Some(redis) = &self.redis { + let session: Option<(Uuid, Uuid)> = + sqlx::query_as("SELECT user_id, bot_id FROM user_sessions WHERE id = $1") + .bind(session_id) + .fetch_optional(&self.pool) + .await?; + + if let Some((user_id, bot_id)) = session { + let cache_key = format!("session:{}:{}", user_id, bot_id); + let _: () = redis.clone().del(cache_key).await?; + } + } + + Ok(()) + } + + pub async fn get_conversation_history( + &self, + session_id: Uuid, + user_id: Uuid, + ) -> Result, Box> { + let messages = sqlx::query( + "SELECT role, content_encrypted FROM message_history + WHERE session_id = $1 AND user_id = $2 + ORDER BY message_index ASC", + ) + .bind(session_id) + .bind(user_id) + .fetch_all(&self.pool) + .await?; + + let mut history = Vec::new(); + for row in messages { + let role: String = row.get("role"); + let content: String = row.get("content_encrypted"); // No decryption needed + history.push((role, content)); + } + Ok(history) + } + + pub async fn get_user_sessions( + &self, + user_id: Uuid, + ) -> Result, Box> { + // For lists, we'll just use database + let sessions = sqlx::query_as( + "SELECT * FROM user_sessions WHERE user_id = $1 ORDER BY updated_at DESC", + ) + .bind(user_id) + .fetch_all(&self.pool) + .await?; + Ok(sessions) + } +} diff --git a/src/services/shared/mod.rs b/src/services/shared/mod.rs new file mode 100644 index 0000000..cdcc6f2 --- /dev/null +++ b/src/services/shared/mod.rs @@ -0,0 +1,3 @@ +pub mod shared; +pub mod state; +pub mod utils; diff --git a/src/services/shared/shared.rs b/src/services/shared/shared.rs new file mode 100644 index 0000000..bd86586 --- /dev/null +++ b/src/services/shared/shared.rs @@ -0,0 +1,58 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct UserSession { + pub id: Uuid, + pub user_id: Uuid, + pub bot_id: Uuid, + pub title: String, + pub context_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbeddingRequest { + pub text: String, + pub model: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbeddingResponse { + pub embedding: Vec, + pub model: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchResult { + pub text: String, + pub similarity: f32, + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserMessage { + pub bot_id: String, + pub user_id: String, + pub session_id: String, + pub channel: String, + pub content: String, + pub message_type: String, + pub media_url: Option, + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BotResponse { + pub bot_id: String, + pub user_id: String, + pub session_id: String, + pub channel: String, + pub content: String, + pub message_type: String, + pub stream_token: Option, + pub is_complete: bool, +} diff --git a/src/services/state.rs b/src/services/shared/state.rs similarity index 66% rename from src/services/state.rs rename to src/services/shared/state.rs index 7e75775..fe45b56 100644 --- a/src/services/state.rs +++ b/src/services/shared/state.rs @@ -11,6 +11,11 @@ pub struct AppState { pub db: Option, pub db_custom: Option, pub browser_pool: Arc, + pub orchestrator: Arc, + pub web_adapter: Arc, + pub voice_adapter: Arc, + pub whatsapp_adapter: Arc, + tool_api: Arc, // Add this } pub struct _BotState { pub language: String, diff --git a/src/services/utils.rs b/src/services/shared/utils.rs similarity index 100% rename from src/services/utils.rs rename to src/services/shared/utils.rs diff --git a/src/services/tools/mod.rs b/src/services/tools/mod.rs new file mode 100644 index 0000000..01c16b9 --- /dev/null +++ b/src/services/tools/mod.rs @@ -0,0 +1,536 @@ +// services/tools/mod.rs +use async_trait::async_trait; +use redis::AsyncCommands; +use rhai::Engine; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolResult { + pub success: bool, + pub output: String, + pub requires_input: bool, + pub session_id: String, +} + +#[derive(Clone)] +pub struct Tool { + pub name: String, + pub description: String, + pub parameters: HashMap, + pub script: String, +} + +#[async_trait] +pub trait ToolExecutor: Send + Sync { + async fn execute( + &self, + tool_name: &str, + session_id: &str, + user_id: &str, + ) -> Result>; + async fn provide_input( + &self, + session_id: &str, + input: &str, + ) -> Result<(), Box>; + async fn get_output(&self, session_id: &str) + -> Result, Box>; + async fn is_waiting_for_input( + &self, + session_id: &str, + ) -> Result>; +} + +pub struct RedisToolExecutor { + redis_client: redis::Client, + web_adapter: Arc, + voice_adapter: Arc, + whatsapp_adapter: Arc, +} + +impl RedisToolExecutor { + pub fn new( + redis_url: &str, + web_adapter: Arc, + voice_adapter: Arc, + whatsapp_adapter: Arc, + ) -> Result> { + let client = redis::Client::open(redis_url)?; + Ok(Self { + redis_client: client, + web_adapter, + voice_adapter, + whatsapp_adapter, + }) + } + + async fn send_tool_message( + &self, + session_id: &str, + user_id: &str, + channel: &str, + message: &str, + ) -> Result<(), Box> { + let response = BotResponse { + bot_id: "tool_bot".to_string(), + user_id: user_id.to_string(), + session_id: session_id.to_string(), + channel: channel.to_string(), + content: message.to_string(), + message_type: "tool".to_string(), + stream_token: None, + is_complete: true, + }; + + match channel { + "web" => self.web_adapter.send_message(response).await, + "voice" => self.voice_adapter.send_message(response).await, + "whatsapp" => self.whatsapp_adapter.send_message(response).await, + _ => Ok(()), + } + } + + fn create_rhai_engine(&self, session_id: String, user_id: String, channel: String) -> Engine { + let mut engine = Engine::new(); + + // Clone for TALK function + let tool_executor = Arc::new(( + self.redis_client.clone(), + self.web_adapter.clone(), + self.voice_adapter.clone(), + self.whatsapp_adapter.clone(), + )); + + let session_id_clone = session_id.clone(); + let user_id_clone = user_id.clone(); + let channel_clone = channel.clone(); + + engine.register_fn("talk", move |message: String| { + let tool_executor = Arc::clone(&tool_executor); + let session_id = session_id_clone.clone(); + let user_id = user_id_clone.clone(); + let channel = channel_clone.clone(); + + tokio::spawn(async move { + let (redis_client, web_adapter, voice_adapter, whatsapp_adapter) = &*tool_executor; + + // Send message through appropriate channel + let response = BotResponse { + bot_id: "tool_bot".to_string(), + user_id: user_id.clone(), + session_id: session_id.clone(), + channel: channel.clone(), + content: message.clone(), + message_type: "tool".to_string(), + stream_token: None, + is_complete: true, + }; + + let result = match channel.as_str() { + "web" => web_adapter.send_message(response).await, + "voice" => voice_adapter.send_message(response).await, + "whatsapp" => whatsapp_adapter.send_message(response).await, + _ => Ok(()), + }; + + if let Err(e) = result { + log::error!("Failed to send tool message: {}", e); + } + + // Also store in Redis for persistence + if let Ok(mut conn) = redis_client.get_async_connection().await { + let output_key = format!("tool:{}:output", session_id); + let _ = conn.lpush(&output_key, &message).await; + } + }); + }); + + // Clone for HEAR function + let hear_executor = self.redis_client.clone(); + let session_id_clone = session_id.clone(); + + engine.register_fn("hear", move || -> String { + let hear_executor = hear_executor.clone(); + let session_id = session_id_clone.clone(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + match hear_executor.get_async_connection().await { + Ok(mut conn) => { + let input_key = format!("tool:{}:input", session_id); + let waiting_key = format!("tool:{}:waiting", session_id); + + // Set waiting flag + let _ = conn.set_ex(&waiting_key, "true", 300).await; + + // Wait for input + let result: Option<(String, String)> = + conn.brpop(&input_key, 30).await.ok().flatten(); + + // Clear waiting flag + let _ = conn.del(&waiting_key).await; + + result + .map(|(_, input)| input) + .unwrap_or_else(|| "timeout".to_string()) + } + Err(e) => { + log::error!("HEAR Redis error: {}", e); + "error".to_string() + } + } + }) + }); + + engine + } + + async fn cleanup_session(&self, session_id: &str) -> Result<(), Box> { + let mut conn = self.redis_client.get_async_connection().await?; + + let keys = vec![ + format!("tool:{}:output", session_id), + format!("tool:{}:input", session_id), + format!("tool:{}:waiting", session_id), + format!("tool:{}:active", session_id), + ]; + + for key in keys { + let _: () = conn.del(&key).await?; + } + + Ok(()) + } +} + +#[async_trait] +impl ToolExecutor for RedisToolExecutor { + async fn execute( + &self, + tool_name: &str, + session_id: &str, + user_id: &str, + ) -> Result> { + let tool = get_tool(tool_name).ok_or_else(|| format!("Tool not found: {}", tool_name))?; + + // Store session info in Redis + let mut conn = self.redis_client.get_async_connection().await?; + let session_key = format!("tool:{}:session", session_id); + let session_data = serde_json::json!({ + "user_id": user_id, + "tool_name": tool_name, + "started_at": chrono::Utc::now().to_rfc3339(), + }); + conn.set_ex(&session_key, session_data.to_string(), 3600) + .await?; + + // Mark tool as active + let active_key = format!("tool:{}:active", session_id); + conn.set_ex(&active_key, "true", 3600).await?; + + // Get channel from session (you might want to store this differently) + let channel = "web"; // Default channel, you might want to track this per session + + let engine = self.create_rhai_engine( + session_id.to_string(), + user_id.to_string(), + channel.to_string(), + ); + + // Execute tool in background + let redis_clone = self.redis_client.clone(); + let web_adapter_clone = self.web_adapter.clone(); + let voice_adapter_clone = self.voice_adapter.clone(); + let whatsapp_adapter_clone = self.whatsapp_adapter.clone(); + let session_id_clone = session_id.to_string(); + let user_id_clone = user_id.to_string(); + let tool_script = tool.script.clone(); + + tokio::spawn(async move { + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + // Register TALK function for background execution + let redis_client = redis_clone.clone(); + let web_adapter = web_adapter_clone.clone(); + let voice_adapter = voice_adapter_clone.clone(); + let whatsapp_adapter = whatsapp_adapter_clone.clone(); + let session_id = session_id_clone.clone(); + let user_id = user_id_clone.clone(); + + engine.register_fn("talk", move |message: String| { + let redis_client = redis_client.clone(); + let web_adapter = web_adapter.clone(); + let voice_adapter = voice_adapter.clone(); + let whatsapp_adapter = whatsapp_adapter.clone(); + let session_id = session_id.clone(); + let user_id = user_id.clone(); + + tokio::spawn(async move { + // Determine channel from session data + let channel = "web"; // In real implementation, get from session storage + + let response = BotResponse { + bot_id: "tool_bot".to_string(), + user_id: user_id.clone(), + session_id: session_id.clone(), + channel: channel.to_string(), + content: message.clone(), + message_type: "tool".to_string(), + stream_token: None, + is_complete: true, + }; + + // Send through appropriate channel + let send_result = match channel { + "web" => web_adapter.send_message(response).await, + "voice" => voice_adapter.send_message(response).await, + "whatsapp" => whatsapp_adapter.send_message(response).await, + _ => Ok(()), + }; + + if let Err(e) = send_result { + log::error!("Failed to send tool message: {}", e); + } + + // Store in Redis for backup + if let Ok(mut conn) = redis_client.get_async_connection().await { + let output_key = format!("tool:{}:output", session_id); + let _ = conn.lpush(&output_key, &message).await; + } + }); + }); + + // Register HEAR function + let hear_redis = redis_clone.clone(); + let session_id_hear = session_id.clone(); + engine.register_fn("hear", move || -> String { + let hear_redis = hear_redis.clone(); + let session_id = session_id_hear.clone(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + match hear_redis.get_async_connection().await { + Ok(mut conn) => { + let input_key = format!("tool:{}:input", session_id); + let waiting_key = format!("tool:{}:waiting", session_id); + + let _ = conn.set_ex(&waiting_key, "true", 300).await; + let result: Option<(String, String)> = + conn.brpop(&input_key, 30).await.ok().flatten(); + let _ = conn.del(&waiting_key).await; + + result + .map(|(_, input)| input) + .unwrap_or_else(|| "timeout".to_string()) + } + Err(_) => "error".to_string(), + } + }) + }); + + // Execute the tool + match engine.eval_with_scope::<()>(&mut scope, &tool_script) { + Ok(_) => { + log::info!( + "Tool {} completed successfully for session {}", + tool_name, + session_id + ); + + // Send completion message + let completion_msg = + "๐Ÿ› ๏ธ Tool execution completed. How can I help you with anything else?"; + let response = BotResponse { + bot_id: "tool_bot".to_string(), + user_id: user_id_clone, + session_id: session_id_clone.clone(), + channel: "web".to_string(), + content: completion_msg.to_string(), + message_type: "tool_complete".to_string(), + stream_token: None, + is_complete: true, + }; + + let _ = web_adapter_clone.send_message(response).await; + } + Err(e) => { + log::error!("Tool execution failed: {}", e); + + let error_msg = format!("โŒ Tool error: {}", e); + let response = BotResponse { + bot_id: "tool_bot".to_string(), + user_id: user_id_clone, + session_id: session_id_clone.clone(), + channel: "web".to_string(), + content: error_msg, + message_type: "tool_error".to_string(), + stream_token: None, + is_complete: true, + }; + + let _ = web_adapter_clone.send_message(response).await; + } + } + + // Cleanup + if let Ok(mut conn) = redis_clone.get_async_connection().await { + let active_key = format!("tool:{}:active", session_id_clone); + let _ = conn.del(&active_key).await; + } + }); + + Ok(ToolResult { + success: true, + output: format!( + "๐Ÿ› ๏ธ Starting {} tool. Please follow the tool's instructions.", + tool_name + ), + requires_input: true, + session_id: session_id.to_string(), + }) + } + + async fn provide_input( + &self, + session_id: &str, + input: &str, + ) -> Result<(), Box> { + let mut conn = self.redis_client.get_async_connection().await?; + let input_key = format!("tool:{}:input", session_id); + conn.lpush(&input_key, input).await?; + Ok(()) + } + + async fn get_output( + &self, + session_id: &str, + ) -> Result, Box> { + let mut conn = self.redis_client.get_async_connection().await?; + let output_key = format!("tool:{}:output", session_id); + let messages: Vec = conn.lrange(&output_key, 0, -1).await?; + + // Clear after reading + let _: () = conn.del(&output_key).await?; + + Ok(messages) + } + + async fn is_waiting_for_input( + &self, + session_id: &str, + ) -> Result> { + let mut conn = self.redis_client.get_async_connection().await?; + let waiting_key = format!("tool:{}:waiting", session_id); + let exists: bool = conn.exists(&waiting_key).await?; + Ok(exists) + } +} + +// Tool definitions +fn get_tool(name: &str) -> Option { + match name { + "calculator" => Some(Tool { + name: "calculator".to_string(), + description: "Perform mathematical calculations".to_string(), + parameters: HashMap::from([ + ("operation".to_string(), "add|subtract|multiply|divide".to_string()), + ("a".to_string(), "number".to_string()), + ("b".to_string(), "number".to_string()), + ]), + script: r#" + // Calculator tool using TALK/HEAR pattern + let TALK = |message| { + talk(message); + }; + + let HEAR = || { + hear() + }; + + TALK("๐Ÿ”ข Calculator started!"); + TALK("Please enter the first number:"); + let a = HEAR(); + TALK("Please enter the second number:"); + let b = HEAR(); + TALK("Choose operation: add, subtract, multiply, or divide:"); + let op = HEAR(); + + let num_a = a.to_float(); + let num_b = b.to_float(); + + if op == "add" { + let result = num_a + num_b; + TALK("โœ… Result: " + a + " + " + b + " = " + result); + } else if op == "subtract" { + let result = num_a - num_b; + TALK("โœ… Result: " + a + " - " + b + " = " + result); + } else if op == "multiply" { + let result = num_a * num_b; + TALK("โœ… Result: " + a + " ร— " + b + " = " + result); + } else if op == "divide" { + if num_b != 0.0 { + let result = num_a / num_b; + TALK("โœ… Result: " + a + " รท " + b + " = " + result); + } else { + TALK("โŒ Error: Cannot divide by zero!"); + } + } else { + TALK("โŒ Error: Invalid operation. Please use: add, subtract, multiply, or divide"); + } + + TALK("Calculator session completed. Thank you!"); + "#.to_string(), + }), + _ => None, + } +} + +pub struct ToolManager { + executor: Arc, +} + +impl ToolManager { + pub fn new(executor: Arc) -> Self { + Self { executor } + } + + pub async fn execute_tool( + &self, + tool_name: &str, + session_id: &str, + user_id: &str, + ) -> Result> { + self.executor.execute(tool_name, session_id, user_id).await + } + + pub async fn provide_input( + &self, + session_id: &str, + input: &str, + ) -> Result<(), Box> { + self.executor.provide_input(session_id, input).await + } + + pub async fn get_tool_output( + &self, + session_id: &str, + ) -> Result, Box> { + self.executor.get_output(session_id).await + } + + pub async fn is_tool_waiting( + &self, + session_id: &str, + ) -> Result> { + self.executor.is_waiting_for_input(session_id).await + } + + pub fn list_tools(&self) -> Vec { + vec!["calculator".to_string()] + } +} diff --git a/src/services/web_automation.rs b/src/services/web_automation/mod.rs similarity index 100% rename from src/services/web_automation.rs rename to src/services/web_automation/mod.rs diff --git a/src/services/whatsapp/mod.rs b/src/services/whatsapp/mod.rs new file mode 100644 index 0000000..ecbcfd8 --- /dev/null +++ b/src/services/whatsapp/mod.rs @@ -0,0 +1,176 @@ +use async_trait::async_trait; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use log::info; + +use crate::shared::BotResponse; + +#[derive(Debug, Deserialize)] +pub struct WhatsAppMessage { + pub entry: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppEntry { + pub changes: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppChange { + pub value: WhatsAppValue, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppValue { + pub contacts: Option>, + pub messages: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppContact { + pub profile: WhatsAppProfile, + pub wa_id: String, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppProfile { + pub name: String, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppMessageData { + pub from: String, + pub id: String, + pub timestamp: String, + pub text: Option, + pub r#type: String, +} + +#[derive(Debug, Deserialize)] +pub struct WhatsAppText { + pub body: String, +} + +#[derive(Serialize)] +pub struct WhatsAppResponse { + pub messaging_product: String, + pub to: String, + pub text: WhatsAppResponseText, +} + +#[derive(Serialize)] +pub struct WhatsAppResponseText { + pub body: String, +} + +pub struct WhatsAppAdapter { + client: Client, + access_token: String, + phone_number_id: String, + webhook_verify_token: String, + sessions: Arc>>, // phone -> session_id +} + +impl WhatsAppAdapter { + pub fn new(access_token: String, phone_number_id: String, webhook_verify_token: String) -> Self { + Self { + client: Client::new(), + access_token, + phone_number_id, + webhook_verify_token, + sessions: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub async fn get_session_id(&self, phone: &str) -> String { + let sessions = self.sessions.lock().await; + sessions.get(phone).cloned().unwrap_or_else(|| { + drop(sessions); + let session_id = uuid::Uuid::new_v4().to_string(); + let mut sessions = self.sessions.lock().await; + sessions.insert(phone.to_string(), session_id.clone()); + session_id + }) + } + + pub async fn send_whatsapp_message(&self, to: &str, body: &str) -> Result<(), Box> { + let url = format!( + "https://graph.facebook.com/v17.0/{}/messages", + self.phone_number_id + ); + + let response_data = WhatsAppResponse { + messaging_product: "whatsapp".to_string(), + to: to.to_string(), + text: WhatsAppResponseText { + body: body.to_string(), + }, + }; + + let response = self.client + .post(&url) + .header("Authorization", format!("Bearer {}", self.access_token)) + .json(&response_data) + .send() + .await?; + + if response.status().is_success() { + info!("WhatsApp message sent to {}", to); + } else { + let error_text = response.text().await?; + log::error!("Failed to send WhatsApp message: {}", error_text); + } + + Ok(()) + } + + pub async fn process_incoming_message(&self, message: WhatsAppMessage) -> Result, Box> { + let mut user_messages = Vec::new(); + + for entry in message.entry { + for change in entry.changes { + if let Some(messages) = change.value.messages { + for msg in messages { + if let Some(text) = msg.text { + let session_id = self.get_session_id(&msg.from).await; + + let user_message = crate::shared::UserMessage { + bot_id: "default_bot".to_string(), + user_id: msg.from.clone(), + session_id: session_id.clone(), + channel: "whatsapp".to_string(), + content: text.body, + message_type: msg.r#type, + media_url: None, + timestamp: chrono::Utc::now(), + }; + + user_messages.push(user_message); + } + } + } + } + } + + Ok(user_messages) + } + + pub fn verify_webhook(&self, mode: &str, token: &str, challenge: &str) -> Result> { + if mode == "subscribe" && token == self.webhook_verify_token { + Ok(challenge.to_string()) + } else { + Err("Invalid verification".into()) + } + } +} + +#[async_trait] +impl super::channels::ChannelAdapter for WhatsAppAdapter { + async fn send_message(&self, response: BotResponse) -> Result<(), Box> { + info!("Sending WhatsApp response to: {}", response.user_id); + self.send_whatsapp_message(&response.user_id, &response.content).await + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..f929974 --- /dev/null +++ b/static/index.html @@ -0,0 +1,484 @@ + + + + General Bots - ChatGPT Clone + + + + + +
+ +
+
+ + +
+
+ + + + + diff --git a/test.rs b/test.rs deleted file mode 100644 index e69de29..0000000