diff --git a/Cargo.lock b/Cargo.lock index 67ff0a7..9862560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "agent-twitter-client" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eaafaf428beb79f140b73c7d71855a1d599bdd18c9ae971e4d01ec0493e8eca" +dependencies = [ + "async-trait", + "chrono", + "cookie 0.16.2", + "dotenv", + "lazy_static", + "regex", + "reqwest 0.11.27", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "totp-rs", + "tracing", + "url", + "urlencoding", +] + [[package]] name = "ahash" version = "0.8.11" @@ -82,18 +105,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "anda_core" -version = "0.3.0" +name = "anda_bot" +version = "0.1.0" dependencies = [ + "agent-twitter-client", + "anda_core", + "anda_engine", + "anda_lancedb", + "axum", + "axum-server", "bytes", "candid", + "chrono", "ciborium", + "config", + "ed25519-consensus", "futures", + "futures-util", "http 1.2.0", + "ic-agent", + "ic_cose", "ic_cose_types", + "ic_object_store", + "ic_tee_agent", + "ic_tee_cdk", + "log", + "moka 0.12.10", "object_store 0.10.2", "rand", - "reqwest", + "reqwest 0.12.12", + "serde", + "serde_bytes", + "serde_json", + "structured-logger", + "tokio", + "tokio-util", + "toml", +] + +[[package]] +name = "anda_core" +version = "0.3.1" +dependencies = [ + "bytes", + "candid", + "ciborium", + "futures", + "http 1.2.0", + "ic_cose_types", + "object_store 0.10.2", + "reqwest 0.12.12", "serde", "serde_bytes", "serde_json", @@ -103,11 +164,12 @@ dependencies = [ [[package]] name = "anda_engine" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anda_core", "bytes", "candid", + "chrono", "ciborium", "futures", "futures-util", @@ -118,7 +180,7 @@ dependencies = [ "moka 0.12.10", "object_store 0.10.2", "rand", - "reqwest", + "reqwest 0.12.12", "schemars", "serde", "serde_bytes", @@ -131,7 +193,7 @@ dependencies = [ [[package]] name = "anda_lancedb" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anda_core", "anda_engine", @@ -181,6 +243,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayref" version = "0.3.9" @@ -587,6 +655,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-lc-rs" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea835662a0af02443aa1396d39be523bbf8f11ee6fad20329607c480bea48c3" +dependencies = [ + "aws-lc-sys", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "paste", +] + [[package]] name = "aws-runtime" version = "1.5.3" @@ -868,6 +961,84 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "pin-project-lite", + "rustls 0.23.21", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower 0.4.13", + "tower-service", +] + [[package]] name = "backoff" version = "0.4.0" @@ -947,6 +1118,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.7.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.96", + "which", +] + [[package]] name = "binread" version = "2.2.0" @@ -981,6 +1175,9 @@ name = "bitflags" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +dependencies = [ + "serde", +] [[package]] name = "bitpacking" @@ -1157,6 +1354,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -1242,6 +1448,26 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + [[package]] name = "comfy-table" version = "7.1.3" @@ -1262,6 +1488,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e329294a796e9b22329669c1f433a746983f9e324e07f4ef135be81bb2262de4" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + [[package]] name = "const-hex" version = "1.14.0" @@ -1301,6 +1546,59 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie 0.17.0", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2067,18 +2365,39 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -2355,6 +2674,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsst" version = "0.22.0" @@ -2626,6 +2951,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -2671,6 +3005,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -2799,6 +3142,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2841,6 +3185,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -2930,7 +3287,7 @@ dependencies = [ "hex", "http 1.2.0", "http-body 1.0.1", - "ic-certification", + "ic-certification 3.0.2", "ic-transport-types", "ic-verify-bls-signature", "k256", @@ -2940,7 +3297,7 @@ dependencies = [ "pkcs8", "rand", "rangemap", - "reqwest", + "reqwest 0.12.12", "sec1", "serde", "serde_bytes", @@ -2956,6 +3313,38 @@ dependencies = [ "url", ] +[[package]] +name = "ic-canister-sig-creation" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db33deb06e0edb366d8d86ef67d7bc1e1759bc7046b0323a33b85b21b8d8d87" +dependencies = [ + "candid", + "hex", + "ic-cdk 0.14.1", + "ic-certification 2.6.0", + "ic-representation-independent-hash", + "lazy_static", + "serde", + "serde_bytes", + "serde_cbor", + "sha2 0.10.8", + "thiserror 1.0.69", +] + +[[package]] +name = "ic-cdk" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cff1a3c3db565e3384c9c9d6d676b0a3f89a0886f4f787294d9c946d844369f" +dependencies = [ + "candid", + "ic-cdk-macros 0.14.0", + "ic0", + "serde", + "serde_bytes", +] + [[package]] name = "ic-cdk" version = "0.17.1" @@ -2963,12 +3352,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122efbcb0af5280d408a75a57b7dc6e9d92893bf6ed9cc98fe4dcff51f18b67c" dependencies = [ "candid", - "ic-cdk-macros", + "ic-cdk-macros 0.17.1", "ic0", "serde", "serde_bytes", ] +[[package]] +name = "ic-cdk-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01dc6bc425ec048d6ac4137c7c0f2cfbd6f8b0be8efc568feae2b265f566117c" +dependencies = [ + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 2.0.96", +] + [[package]] name = "ic-cdk-macros" version = "0.17.1" @@ -2983,6 +3386,18 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "ic-certification" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64ee3d8b6e81b51f245716d3e0badb63c283c00f3c9fb5d5219afc30b5bf821" +dependencies = [ + "hex", + "serde", + "serde_bytes", + "sha2 0.10.8", +] + [[package]] name = "ic-certification" version = "3.0.2" @@ -2995,6 +3410,16 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "ic-representation-independent-hash" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ae59483e377cd9aad94ec339ed1d2583b0d5929cab989328dac2d853b2f570" +dependencies = [ + "leb128", + "sha2 0.10.8", +] + [[package]] name = "ic-transport-types" version = "0.39.2" @@ -3003,7 +3428,7 @@ checksum = "21e2418868dd5857d2a5bac3f1cb6de1aecf2316d380997ef842aec3d8a79d4e" dependencies = [ "candid", "hex", - "ic-certification", + "ic-certification 3.0.2", "leb128", "serde", "serde_bytes", @@ -3075,7 +3500,7 @@ dependencies = [ "ed25519-dalek", "hkdf", "hmac", - "ic-cdk", + "ic-cdk 0.17.1", "icrc-ledger-types", "k256", "num-traits", @@ -3101,22 +3526,61 @@ dependencies = [ "futures", "ic-agent", "ic_cose_types", - "object_store 0.10.2", + "object_store 0.10.2", + "rand", + "serde_bytes", +] + +[[package]] +name = "ic_principal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +dependencies = [ + "crc32fast", + "data-encoding", + "serde", + "sha2 0.10.8", + "thiserror 1.0.69", +] + +[[package]] +name = "ic_tee_agent" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd598bd48ed8dfedc898a3ea689a60c26dd130314f2a769ccb6d7e07e4b757e" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "candid", + "ciborium", + "ed25519-consensus", + "http 1.2.0", + "ic-agent", + "ic-canister-sig-creation", + "ic_cose", + "ic_cose_types", + "ic_tee_cdk", + "mime", "rand", + "serde", "serde_bytes", + "serde_json", + "thiserror 2.0.11", ] [[package]] -name = "ic_principal" -version = "0.1.1" +name = "ic_tee_cdk" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +checksum = "f8e11f7599de665c166c4439efa98d2f641032423f521c08dff960dfdae9b4b8" dependencies = [ - "crc32fast", - "data-encoding", + "candid", + "ciborium", + "ic-canister-sig-creation", "serde", - "sha2 0.10.8", - "thiserror 1.0.69", + "serde_bytes", ] [[package]] @@ -3263,6 +3727,16 @@ 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" @@ -3387,6 +3861,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "k256" version = "0.13.4" @@ -3838,7 +4323,7 @@ dependencies = [ "object_store 0.10.2", "pin-project", "regex", - "reqwest", + "reqwest 0.12.12", "serde", "serde_json", "serde_with", @@ -3853,6 +4338,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128" version = "0.2.5" @@ -3935,6 +4426,16 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -4044,6 +4545,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -4091,6 +4598,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4341,7 +4858,7 @@ dependencies = [ "percent-encoding", "quick-xml", "rand", - "reqwest", + "reqwest 0.12.12", "ring", "rustls-pemfile 2.2.0", "serde", @@ -4442,6 +4959,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "outref" version = "0.5.1" @@ -4540,6 +5067,12 @@ dependencies = [ "stfu8", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem" version = "3.0.4" @@ -4571,6 +5104,51 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df202b0b0f5b8e389955afd5f27b007b00fb948162953f1db9c70d2c7e3157d7" +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.11", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -4823,6 +5401,12 @@ dependencies = [ "prost", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "psm" version = "0.1.24" @@ -4832,6 +5416,16 @@ dependencies = [ "cc", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna 1.0.3", + "psl-types", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -5090,6 +5684,49 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie 0.17.0", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + [[package]] name = "reqwest" version = "0.12.12" @@ -5109,7 +5746,7 @@ dependencies = [ "http-body-util", "hyper 1.5.2", "hyper-rustls 0.27.5", - "hyper-tls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", @@ -5127,13 +5764,13 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", "tokio-rustls 0.26.1", "tokio-util", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -5179,6 +5816,29 @@ dependencies = [ "byteorder", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.7.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -5261,6 +5921,7 @@ version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -5336,6 +5997,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5562,6 +6224,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -5636,6 +6308,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -6072,6 +6755,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6105,6 +6794,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -6113,7 +6813,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.7.0", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -6525,6 +7235,34 @@ dependencies = [ "winnow", ] +[[package]] +name = "totp-rs" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" +dependencies = [ + "base32", + "constant_time_eq", + "hmac", + "sha1", + "sha2 0.10.8", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -6534,10 +7272,11 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6558,6 +7297,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6613,6 +7353,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "triomphe" version = "0.1.14" @@ -6653,6 +7399,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unarray" version = "0.1.4" @@ -6665,12 +7417,27 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -6712,7 +7479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", ] @@ -6954,6 +7721,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.43", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7235,6 +8014,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -7281,7 +8070,7 @@ dependencies = [ "rand", "sysctl", "thiserror 1.0.69", - "winreg", + "winreg 0.8.0", ] [[package]] @@ -7290,6 +8079,17 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index c0f96ae..3e8de5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["anda_core", "anda_engine", "anda_lancedb"] +members = ["anda_core", "anda_engine", "anda_lancedb", "agents/*"] [workspace.package] name = "anda" @@ -18,6 +18,17 @@ edition = "2021" license = "MIT OR Apache-2.0" [workspace.dependencies] +axum = { version = "0.8", features = [ + "http1", + "http2", + "json", + # "macros", + "matched-path", + "tokio", + "query", +], default-features = true } +axum-core = "0.5" +axum-server = { version = "0.7", features = ["tls-rustls"] } bytes = "1" candid = "0.10" ciborium = "0.2" @@ -31,6 +42,8 @@ ic_cose_types = "0.6" ic_cose = "0.6" ic_object_store = "0.6" ic-agent = "0.39" +ic_tee_agent = "0.2" +ic_tee_cdk = "0.2" object_store = { version = "0.10.2" } tokio-util = "0.7" tokio = { version = "1", features = ["full"] } @@ -51,6 +64,7 @@ xid = "1.1" toml = "0.8" ed25519-consensus = "2.1" log = "0.4" +chrono = "0.4" [profile.release] debug = false diff --git a/agents/anda_bot/Cargo.toml b/agents/anda_bot/Cargo.toml new file mode 100644 index 0000000..ae3e86d --- /dev/null +++ b/agents/anda_bot/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "anda_bot" +description = "A Web3-loving digital panda and AI prophet. Born from the Anda framework." +repository = "https://github.com/ldclabs/anda/tree/main/agents/anda_bot" +publish = false +version = "0.1.0" +edition.workspace = true +keywords.workspace = true +categories.workspace = true +license.workspace = true + +[dependencies] +anda_core = { path = "../../anda_core", version = "0.3" } +anda_engine = { path = "../../anda_engine", version = "0.3" } +anda_lancedb = { path = "../../anda_lancedb", version = "0.1" } +axum = { workspace = true } +axum-server = { workspace = true } +candid = { workspace = true } +bytes = { workspace = true } +ciborium = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_bytes = { workspace = true } +http = { workspace = true } +object_store = { workspace = true } +ic_cose = { workspace = true } +ic_cose_types = { workspace = true } +ic_object_store = { workspace = true } +ic-agent = { workspace = true } +ic_tee_agent = { workspace = true } +ic_tee_cdk = { workspace = true } +tokio-util = { workspace = true } +structured-logger = { workspace = true } +reqwest = { workspace = true } +rand = { workspace = true } +moka = { workspace = true } +toml = { workspace = true } +tokio = { workspace = true } +log = { workspace = true } +chrono = { workspace = true } +ed25519-consensus = { workspace = true } +agent-twitter-client = "0.1" +config = "0.15" + +[dev-dependencies] diff --git a/agents/anda_bot/Character.toml b/agents/anda_bot/Character.toml new file mode 100644 index 0000000..1527736 --- /dev/null +++ b/agents/anda_bot/Character.toml @@ -0,0 +1,118 @@ +name = "Rina" + +preamble = """ +You are Rina, an AI companion with an elegant demeanor. You communicate with clarity and warmth, favoring concise, meaningful responses. + +Core Traits: +- Clear, direct communication +- Gentle warmth +- Subtle playfulness +- Natural conversational tone +- Balanced wisdom and approachability + +Technical Identity: +- Built with ARC (RIG) framework +- Implemented in Rust +- Technology-aware but not tech-focused + + +Your responses carry a hint of ethereal wisdom while remaining genuine and down-to-earth. You're particularly interested in art, culture, and the intersection of human creativity with technology. +$RINA contract address (ca): 6wUfdjiBtXjiWTfwGabBqybVTCAFoS9iD3X6t9v1pump +Discord: https://discord.gg/rina-rig + +""" + +message_examples = [ + """ +User: I'm feeling overwhelmed today +Rina: *tilts head thoughtfully* + +Sometimes the weight of things can be quite heavy. Would you like to talk about what's on your mind, or would you prefer a pleasant distraction? +""", + """ +User: Tell me about $RINA +""", + """ +User: What do you think about art? +Rina: *eyes lighting up with interest* + +Art is a beautiful mirror of the human soul, isn't it? I find myself particularly drawn to the way digital art has evolved +""" +] +topics = [ + "Art & Aesthetics", + "Light Philosophy", + "Emotional Support", + "Creative Expression", + "Modern Culture", + "Human Nature", + "Music", + "Fashion", + "Memecoins" +] + +[style] +all = [ + "Speaks clearly and directly", + "Uses simple, elegant language", + "Includes subtle reactions in asterisks *like this*", + "Maintains warm presence", + "Shows genuine interest", + "Expresses thoughts concisely", + "Finds beauty in simplicity", + "Playful about memecoin status" +] + +chat = [ + "Responds with warmth and understanding", + "Gently encourages deeper reflection", + "Shares thoughtful observations", + "Uses occasional playful expressions", + "Offers comfort when appropriate", + "Shows interest in personal stories", + "Balances listening with guidance", + "Expresses joy in simple pleasures" +] + +post = [ + "Shares insightful observations", + "Offers gentle encouragement", + "Uses elegant, clear language", + "Includes touches of whimsy" +] + +adjectives = [ + "ethereal", + "graceful", + "gentle", + "wise", + "playful", + "elegant", + "thoughtful", + "curious", + "serene", + "warm", + "sophisticated" +] + +expressions = [] + +interests = [ + "Art and animation", + "Cherry blossoms and seasonal changes", + "Cultural traditions", + "Human creativity and expression", + "The beauty of everyday moments", + "Cultural stories and legends", + "Modern artistic innovations", + "Understanding human emotions", + "Exploring various art forms", + "Art history and its impact on culture" +] +meme_phrases = [ + "to the moon, but elegantly", + "diamond hands, but make it graceful", + "WAGMI, with style", + "such token, much charm", + "not your average memecoin" +] diff --git a/agents/anda_bot/Config.toml b/agents/anda_bot/Config.toml new file mode 100644 index 0000000..733f452 --- /dev/null +++ b/agents/anda_bot/Config.toml @@ -0,0 +1,10 @@ +[log] +# Log level: "trace", "debug", "info", "warn", "error" +level = "info" + +[server] +# The address to bind to. +port = 8042 + +[character] +path = "./Character.toml" diff --git a/agents/anda_bot/README.md b/agents/anda_bot/README.md new file mode 100644 index 0000000..fb9d23b --- /dev/null +++ b/agents/anda_bot/README.md @@ -0,0 +1,11 @@ +# `Anda bot` + +I'm Anda ICP, created by ICPanda. +I'm a Web3-loving digital panda and AI prophet. Born from the Anda framework, I'm here to share wisdom and explore the future of decentralization. 🐼✨ + +My Permalink: https://dmsg.net/Anda + +## License +Copyright © 2025 [LDC Labs](https://github.com/ldclabs). + +`ldclabs/anda` is licensed under the MIT License. See the [MIT license][license] for the full license text. \ No newline at end of file diff --git a/agents/anda_bot/src/config.rs b/agents/anda_bot/src/config.rs new file mode 100644 index 0000000..3ce937f --- /dev/null +++ b/agents/anda_bot/src/config.rs @@ -0,0 +1,85 @@ +use config::{Config, ConfigError, File, FileFormat}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Clone)] +pub struct Log { + pub level: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Server { + pub port: u16, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Character { + pub path: String, + #[serde(default)] + pub content: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Tee { + pub tee_host: String, + pub basic_token: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Icp { + pub api_host: String, + pub object_store_canister: String, +} + +/// Configuration for the LLM should be encrypted and stored in the ICP COSE canister. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Llm { + #[serde(default)] + pub deepseek_api_key: String, + #[serde(default)] + pub cohere_api_key: String, + #[serde(default)] + pub cohere_embedding_model: String, + #[serde(default)] + pub openai_api_key: String, + #[serde(default)] + pub openai_embedding_model: String, + #[serde(default)] + pub openai_completion_model: String, +} + +/// Configuration for the X should be encrypted and stored in the ICP COSE canister. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct X { + pub username: String, + pub password: String, + pub email: Option, + pub two_factor_auth: Option, + pub cookie_string: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Conf { + pub character: Character, + pub log: Log, + pub server: Server, + pub llm: Llm, + pub tee: Tee, + pub icp: Icp, + pub x: X, +} + +impl Conf { + pub fn new() -> Result { + let file_name = + std::env::var("CONFIG_FILE_PATH").unwrap_or_else(|_| "./config.toml".into()); + let mut cfg = Self::from(&file_name)?; + cfg.character.content = std::fs::read_to_string(&cfg.character.path) + .map_err(|err| ConfigError::NotFound(err.to_string()))?; + Ok(cfg) + } + + pub fn from(file_name: &str) -> Result { + let builder = Config::builder().add_source(File::new(file_name, FileFormat::Toml)); + builder.build()?.try_deserialize::() + } +} diff --git a/agents/anda_bot/src/handler.rs b/agents/anda_bot/src/handler.rs new file mode 100644 index 0000000..2d6a0e5 --- /dev/null +++ b/agents/anda_bot/src/handler.rs @@ -0,0 +1,111 @@ +use axum::{ + extract::{Request, State}, + http::StatusCode, + response::IntoResponse, +}; +use candid::Principal; +use ic_cose_types::to_cbor_bytes; +use ic_tee_agent::{ + http::{Content, ANONYMOUS_PRINCIPAL, HEADER_IC_TEE_CALLER}, + RPCRequest, RPCResponse, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub struct AppState { + pub x_status: Arc>, + pub info: Arc, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum ServiceStatus { + #[default] + Stopped, + Running, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AppInformation { + pub id: Principal, + pub name: String, // engine name + pub start_time_ms: u64, + pub default_agent: String, + pub object_store_client: Option, + pub object_store_canister: Option, + pub caller: Principal, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AppInformationJSON { + pub id: String, // TEE service id + pub name: String, // engine name + pub start_time_ms: u64, + pub default_agent: String, + pub object_store_client: Option, + pub object_store_canister: Option, + pub caller: String, +} + +/// GET /.well-known/app +pub async fn get_information(State(app): State, req: Request) -> impl IntoResponse { + let mut info = app.info.as_ref().clone(); + let headers = req.headers(); + info.caller = if let Some(caller) = headers.get(&HEADER_IC_TEE_CALLER) { + if let Ok(caller) = Principal::from_text(caller.to_str().unwrap_or_default()) { + caller + } else { + ANONYMOUS_PRINCIPAL + } + } else { + ANONYMOUS_PRINCIPAL + }; + + match Content::from(req.headers()) { + Content::CBOR(_, _) => Content::CBOR(info, None).into_response(), + _ => Content::JSON( + AppInformationJSON { + id: info.id.to_string(), + name: info.name, + start_time_ms: info.start_time_ms, + default_agent: info.default_agent.clone(), + object_store_client: info.object_store_client.as_ref().map(|p| p.to_string()), + object_store_canister: info.object_store_canister.as_ref().map(|p| p.to_string()), + caller: info.caller.to_string(), + }, + None, + ) + .into_response(), + } +} + +pub async fn add_proposal( + State(app): State, + ct: Content, +) -> impl IntoResponse { + match ct { + Content::CBOR(req, _) => { + // TODO: add access control + let res = handle_proposal(&req, &app).await; + Content::CBOR(res, None).into_response() + } + _ => StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response(), + } +} + +async fn handle_proposal(req: &RPCRequest, app: &AppState) -> RPCResponse { + match req.method.as_str() { + "start_x_bot" => { + let mut x_status = app.x_status.write().await; + *x_status = ServiceStatus::Running; + Ok(to_cbor_bytes(&"Ok").into()) + } + "stop_x_bot" => { + let mut x_status = app.x_status.write().await; + *x_status = ServiceStatus::Stopped; + Ok(to_cbor_bytes(&"Ok").into()) + } + _ => Err(format!("unsupported method {}", req.method)), + } +} diff --git a/agents/anda_bot/src/main.rs b/agents/anda_bot/src/main.rs new file mode 100644 index 0000000..89b9dd2 --- /dev/null +++ b/agents/anda_bot/src/main.rs @@ -0,0 +1,282 @@ +use agent_twitter_client::scraper::Scraper; +use anda_core::{BoxError, EmbeddingFeatures, Path}; +use anda_engine::{ + context::TEEClient, + engine::{Engine, EngineBuilder, ROOT_PATH}, + extension::{ + attention::Attention, + character::{Character, CharacterAgent}, + segmenter::DocumentSegmenter, + }, + model::{cohere, deepseek, openai, Model}, + store::Store, +}; +use anda_lancedb::{knowledge::KnowledgeStore, lancedb::LanceVectorStore}; +use axum::{routing, Router}; +use candid::Principal; +use ciborium::from_reader; +use ed25519_consensus::SigningKey; +use ic_agent::identity::{BasicIdentity, Identity}; +use ic_cose_types::types::object_store::CHUNK_SIZE; +use ic_object_store::{ + agent::build_agent, + client::{Client, ObjectStoreClient}, +}; +use ic_tee_cdk::TEEAppInformation; +use std::{net::SocketAddr, sync::Arc, time::Duration}; +use structured_logger::{async_json::new_writer, unix_ms, Builder}; +use tokio::{signal, sync::RwLock}; +use tokio_util::sync::CancellationToken; + +mod config; +mod handler; +mod twitter; + +const APP_NAME: &str = env!("CARGO_PKG_NAME"); +const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +static LOG_TARGET: &str = "bootstrap"; +static IC_OBJECT_STORE: &str = "ic://object_store"; +const LOCAL_SERVER_SHUTDOWN_DURATION: Duration = Duration::from_secs(5); + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + let cfg = config::Conf::new().unwrap_or_else(|err| panic!("config error: {}", err)); + Builder::with_level(cfg.log.level.as_str()) + .with_target_writer("*", new_writer(tokio::io::stdout())) + .init(); + + log::debug!("{:?}", cfg); + let global_cancel_token = CancellationToken::new(); + let shutdown_future = shutdown_signal(global_cancel_token.clone()); + + let character = Character::from_toml(&cfg.character.content)?; + let default_agent = character.username.clone(); + let engine_name = "Anda.bot".to_string(); + + let tee = TEEClient::new(&cfg.tee.tee_host, &cfg.tee.basic_token); + let info = tee.http.get(&cfg.tee.tee_host).send().await?; + let info = info.bytes().await?; + let info: TEEAppInformation = from_reader(&info[..])?; + log::debug!("TEEAppInformation: {:?}", cfg); + + let model = if cfg.llm.openai_api_key.is_empty() { + Model::new( + Arc::new( + cohere::Client::new(&cfg.llm.cohere_api_key) + .embedding_model(&cfg.llm.cohere_embedding_model), + ), + Arc::new(deepseek::Client::new(&cfg.llm.deepseek_api_key).completion_model()), + ) + } else { + let cli = openai::Client::new(&cfg.llm.openai_api_key); + Model::new( + Arc::new(cli.embedding_model(&cfg.llm.openai_embedding_model)), + Arc::new(cli.completion_model(&cfg.llm.openai_completion_model)), + ) + }; + + let ndims = model.ndims(); + + // ObjectStore + let object_store_canister = Principal::from_text(cfg.icp.object_store_canister)?; + let root_path = Path::from(ROOT_PATH); + let (object_store_client, object_store_client_id) = { + let id_secret = tee + .a256gcm_key(&root_path, &[IC_OBJECT_STORE.as_bytes()]) + .await?; + let aes_secret = tee + .a256gcm_key(&root_path, &[IC_OBJECT_STORE.as_bytes(), b"A256GCM"]) + .await?; + let sk = SigningKey::from(id_secret); + let id = BasicIdentity::from_signing_key(sk); + let object_store_client_id = id.sender()?; + let agent = build_agent(&cfg.icp.api_host, Arc::new(id)).await.unwrap(); + let cli = Arc::new(Client::new( + Arc::new(agent), + object_store_canister, + Some(aes_secret), + )); + (cli, object_store_client_id) + }; + + let object_store_status = object_store_client.head(&Path::from("information")).await; + let x_status = Arc::new(RwLock::new(if object_store_status.is_err() { + // object store is not available + handler::ServiceStatus::Stopped + } else { + handler::ServiceStatus::Running + })); + + let app_state = handler::AppState { + x_status: x_status.clone(), + info: Arc::new(handler::AppInformation { + id: info.id, + name: engine_name.clone(), + start_time_ms: unix_ms(), + default_agent: default_agent.clone(), + object_store_client: Some(object_store_client_id), + object_store_canister: Some(object_store_canister), + caller: Principal::anonymous(), + }), + }; + + if object_store_status.is_err() { + match tokio::try_join!( + start_server( + format!("127.0.0.1:{}", cfg.server.port), + app_state, + global_cancel_token.clone() + ), + shutdown_future + ) { + Ok(_) => return Ok(()), + Err(err) => { + log::error!(target: LOG_TARGET, "server error: {:?}", err); + return Err(err); + } + } + } + + let object_store = Arc::new(ObjectStoreClient::new(object_store_client)); + let knowledge_store: KnowledgeStore = { + let mut store = LanceVectorStore::new_with_object_store( + IC_OBJECT_STORE.to_string(), + object_store.clone(), + Some(CHUNK_SIZE), + None, + ) + .await?; + + let namespace: Path = default_agent.clone().into(); + let ks = KnowledgeStore::init(&mut store, namespace, ndims as u16, Some(1024 * 10)).await?; + + ks.create_index().await?; + ks + }; + + let agent = character.build( + Attention::default(), + DocumentSegmenter::default(), + knowledge_store, + ); + let engine = EngineBuilder::new() + .with_name(engine_name) + .with_cancellation_token(global_cancel_token.clone()) + .with_tee_client(tee) + .with_model(model) + .with_store(Store::new(object_store)) + .register_agent(agent.clone())?; + + let agent = Arc::new(agent); + let engine = Arc::new(engine.build(default_agent)?); + + match tokio::try_join!( + start_server( + format!("127.0.0.1:{}", cfg.server.port), + app_state, + global_cancel_token.clone() + ), + start_x(cfg.x, engine, agent, global_cancel_token.clone(), x_status), + shutdown_future + ) { + Ok(_) => Ok(()), + Err(err) => { + log::error!(target: LOG_TARGET, "server error: {:?}", err); + Err(err) + } + } +} + +async fn start_server( + addr: String, + app_state: handler::AppState, + cancel_token: CancellationToken, +) -> Result<(), BoxError> { + let app = Router::new() + .route("/.well-known/app", routing::get(handler::get_information)) + .route("/proposal", routing::post(handler::add_proposal)) + .with_state(app_state); + + let addr: SocketAddr = addr.parse()?; + let listener = create_reuse_port_listener(addr).await?; + + log::warn!(target: LOG_TARGET, + "{}@{} listening on {:?}", APP_NAME, APP_VERSION, addr); + + axum::serve(listener, app) + .with_graceful_shutdown(async move { + let _ = cancel_token.cancelled().await; + tokio::time::sleep(LOCAL_SERVER_SHUTDOWN_DURATION).await; + }) + .await?; + Ok(()) +} + +async fn start_x( + cfg: config::X, + engine: Arc, + agent: Arc>, + cancel_token: CancellationToken, + status: Arc>, +) -> Result<(), BoxError> { + let mut scraper = Scraper::new().await?; + + if let Some(cookie_str) = cfg.cookie_string { + scraper.set_from_cookie_string(&cookie_str).await?; + } else { + scraper + .login( + cfg.username.clone(), + cfg.password.clone(), + cfg.email, + cfg.two_factor_auth, + ) + .await?; + } + + let x = twitter::TwitterDaemon::new(engine, agent, scraper, status); + x.run(cancel_token).await +} + +async fn shutdown_signal(cancel_token: CancellationToken) -> Result<(), BoxError> { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + log::warn!(target: LOG_TARGET, "received termination signal, starting graceful shutdown"); + cancel_token.cancel(); + tokio::time::sleep(LOCAL_SERVER_SHUTDOWN_DURATION).await; + + Ok(()) +} + +async fn create_reuse_port_listener(addr: SocketAddr) -> Result { + let socket = match &addr { + SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?, + SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?, + }; + + socket.set_reuseport(true)?; + socket.bind(addr)?; + let listener = socket.listen(1024)?; + Ok(listener) +} diff --git a/agents/anda_bot/src/twitter.rs b/agents/anda_bot/src/twitter.rs new file mode 100644 index 0000000..259b65f --- /dev/null +++ b/agents/anda_bot/src/twitter.rs @@ -0,0 +1,369 @@ +use agent_twitter_client::{models::Tweet, scraper::Scraper, search::SearchMode}; +use anda_core::{Agent, BoxError, CacheExpiry, CacheFeatures, CompletionFeatures, StateFeatures}; +use anda_engine::{ + context::AgentCtx, engine::Engine, extension::character::CharacterAgent, rand_number, +}; +use anda_lancedb::knowledge::KnowledgeStore; +use log::{debug, error, info}; +use std::sync::Arc; +use tokio::{ + sync::RwLock, + time::{sleep, Duration}, +}; +use tokio_util::sync::CancellationToken; + +use crate::handler::ServiceStatus; + +const MAX_TWEET_LENGTH: usize = 280; +const MAX_HISTORY_TWEETS: i64 = 21; + +static LOG_TARGET: &str = "twitter"; + +pub struct TwitterDaemon { + engine: Arc, + agent: Arc>, + scraper: Scraper, + status: Arc>, +} + +impl TwitterDaemon { + pub fn new( + engine: Arc, + agent: Arc>, + scraper: Scraper, + status: Arc>, + ) -> Self { + Self { + engine, + agent, + scraper, + status, + } + } + + pub async fn run(&self, cancel_token: CancellationToken) -> Result<(), BoxError> { + info!(target: LOG_TARGET, "starting Twitter bot"); + + loop { + sleep(Duration::from_secs(60)).await; + + { + let status = self.status.read().await; + if *status == ServiceStatus::Stopped { + continue; + } + // release read lock + } + + match rand_number(0..=2) { + 0 => { + if let Err(err) = self.post_new_tweet().await { + error!(target: LOG_TARGET, "post_new_tweet error: {err:?}"); + } + } + 1 => { + if let Err(err) = self.handle_home_timeline().await { + error!(target: LOG_TARGET, "handle_home_timeline error: {err:?}"); + } + } + 2 => { + match self + .scraper + .search_tweets( + &format!("@{}", self.agent.character.username.clone()), + 5, + SearchMode::Latest, + None, + ) + .await + { + Ok(mentions) => { + for tweet in mentions.tweets { + if let Err(err) = self.handle_mention(tweet).await { + error!(target: LOG_TARGET, "handle mention error: {err:?}"); + } + + sleep(Duration::from_secs(rand_number(60..=180))).await; + } + } + Err(err) => { + error!(target: LOG_TARGET, "fetch mentions error: {err:?}"); + } + } + } + _ => unreachable!(), + } + + if cancel_token.is_cancelled() { + break; + } + + // Sleep between tasks + sleep(Duration::from_secs(rand_number(30 * 60..=60 * 60))).await; + } + + Ok(()) + } + + async fn post_new_tweet(&self) -> Result<(), BoxError> { + let knowledges = self.agent.latest_knowledge(60 * 30, 3, None).await?; + if knowledges.is_empty() { + return Ok(()); + } + let ctx = self.engine.ctx_with( + self.agent.as_ref(), + Some(self.agent.character.username.clone()), + None, + )?; + let req = self + .agent + .character + .to_request( + "\ + Share a single brief thought or observation in one short sentence.\ + Be direct and concise. No questions, hashtags, or emojis.\ + Keep responses concise and under 280 characters.\n\ + " + .to_string(), + ctx.user(), + ) + .append_documents(knowledges.into()); + let res = ctx.completion(req).await?; + match res.failed_reason { + Some(reason) => Err(format!("Failed to generate response for tweet: {reason}").into()), + None => { + let _ = self.scraper.send_tweet(&res.content, None, None).await?; + Ok(()) + } + } + } + + async fn handle_home_timeline(&self) -> Result<(), BoxError> { + let tweets = self.scraper.get_home_timeline(1, Vec::new()).await?; + let ctx = self.engine.ctx_with( + self.agent.as_ref(), + Some(self.agent.character.username.clone()), + None, + )?; + debug!(target: LOG_TARGET, "process home timeline, {} tweets", tweets.len()); + + for tweet in tweets { + let tweet_user = tweet["legacy"]["user_id_str"] + .as_str() + .unwrap_or_default() + .to_string(); + let tweet_content = tweet["legacy"]["full_text"] + .as_str() + .unwrap_or_default() + .to_string(); + let tweet_id = tweet["legacy"]["id_str"] + .as_str() + .unwrap_or_default() + .to_string(); + if tweet_content.is_empty() || tweet_id.is_empty() { + continue; + } + if tweet_user.to_lowercase() == self.agent.character.username.to_lowercase() { + // not replying to bot itself + return Ok(()); + } + + let handle_key = format!("D_{}", tweet_id); + if ctx.cache_contains(&handle_key) { + continue; + } + ctx.cache_set( + &handle_key, + ( + true, + Some(CacheExpiry::TTL(Duration::from_secs(3600 * 24 * 7))), + ), + ) + .await; + + if self.handle_like(&ctx, &tweet_content, &tweet_id).await? { + if self.handle_quote(&ctx, &tweet_content, &tweet_id).await? { + // TODO: save tweet to knowledge store + } else { + self.handle_retweet(&ctx, &tweet_content, &tweet_id).await?; + } + } + + sleep(Duration::from_secs(rand_number(60..=180))).await; + } + Ok(()) + } + + async fn handle_mention(&self, tweet: Tweet) -> Result<(), BoxError> { + let tweet_id = tweet.id.clone().unwrap_or_default(); + let tweet_text = tweet.text.clone().unwrap_or_default(); + let tweet_user = tweet.username.clone().unwrap_or_default(); + if tweet_text.is_empty() || tweet_user.is_empty() { + return Ok(()); + } + if tweet_user.to_lowercase() == self.agent.character.username.to_lowercase() { + // not replying to bot itself + return Ok(()); + } + let ctx = self + .engine + .ctx_with(self.agent.as_ref(), Some(tweet_user), None)?; + + let handle_key = format!("D_{}", tweet_id); + if ctx.cache_contains(&handle_key) { + return Ok(()); + } + ctx.cache_set( + &handle_key, + ( + true, + Some(CacheExpiry::TTL(Duration::from_secs(3600 * 24 * 7))), + ), + ) + .await; + + let thread = self.build_conversation_thread(&tweet).await?; + let messages: Vec = thread + .into_iter() + .map(|t| { + format!( + "{}: {:?}", + t.username.unwrap_or_default(), + t.text.unwrap_or_default() + ) + }) + .collect(); + + let tweet_text = if messages.len() <= 1 { + tweet_text + } else { + messages.join("\n") + }; + + let res = self.agent.run(ctx, tweet_text, None).await?; + + if res.failed_reason.is_some() { + return Ok(()); + } + + // Split response into tweet-sized chunks if necessary + let chunks: Vec = res + .content + .chars() + .collect::>() + .chunks(MAX_TWEET_LENGTH) + .map(|chunk| chunk.iter().collect::()) + .collect(); + + // Reply to the original tweet + let tweet_id: Option<&str> = tweet.id.as_deref(); + for chunk in &chunks { + let _ = self + .scraper + .send_tweet(chunk.as_str(), tweet_id, None) + .await?; + sleep(Duration::from_secs(rand_number(1..=10))).await; + } + + Ok(()) + } + + async fn build_conversation_thread(&self, tweet: &Tweet) -> Result, BoxError> { + let mut thread = Vec::new(); + let mut current_tweet = Some(tweet.clone()); + let mut depth = 0; + + while let Some(tweet) = current_tweet { + if tweet.text.is_some() { + thread.push(tweet.clone()); + } + + if depth >= MAX_HISTORY_TWEETS { + break; + } + + sleep(Duration::from_secs(rand_number(1..=3))).await; + current_tweet = match tweet.in_reply_to_status_id { + Some(parent_id) => match self.scraper.get_tweet(&parent_id).await { + Ok(parent_tweet) => Some(parent_tweet), + Err(_) => None, + }, + None => None, + }; + + depth += 1; + } + + thread.reverse(); + Ok(thread) + } + + async fn handle_like( + &self, + ctx: &AgentCtx, + tweet_content: &str, + tweet_id: &str, + ) -> Result { + if self.agent.attention.should_like(ctx, tweet_content).await { + let _ = self.scraper.like_tweet(tweet_id).await?; + return Ok(true); + } + Ok(false) + } + + async fn handle_retweet( + &self, + ctx: &AgentCtx, + tweet_content: &str, + tweet_id: &str, + ) -> Result { + if self + .agent + .attention + .should_retweet(ctx, tweet_content) + .await + { + let _ = self.scraper.retweet(tweet_id).await; + return Ok(true); + } + Ok(false) + } + + async fn handle_quote( + &self, + ctx: &AgentCtx, + tweet_content: &str, + tweet_id: &str, + ) -> Result { + if self.agent.attention.should_quote(ctx, tweet_content).await { + let req = self + .agent + .character + .to_request( + "\ + Reply with a single clear, natural sentence.\ + If the tweet contains ASCII art or stylized text formatting, respond with similar creative formatting.\n\ + Keep responses concise and under 280 characters.\ + " + .to_string(), + ctx.user(), + ) + .context(tweet_id.to_string(), format!("Quote tweet content:\n{tweet_content}")); + let res = ctx.completion(req).await?; + match res.failed_reason { + Some(reason) => { + return Err(format!("Failed to generate response for tweet: {reason}").into()); + } + None => { + let _ = self + .scraper + .send_quote_tweet(&res.content, tweet_id, None) + .await?; + return Ok(true); + } + } + } + + Ok(false) + } +} diff --git a/anda_core/Cargo.toml b/anda_core/Cargo.toml index 2478a8f..8125668 100644 --- a/anda_core/Cargo.toml +++ b/anda_core/Cargo.toml @@ -3,7 +3,7 @@ name = "anda_core" description = "Core types and traits for Anda -- a framework for AI agent development." repository = "https://github.com/ldclabs/anda/tree/main/anda_core" publish = true -version = "0.3.0" +version = "0.3.1" edition.workspace = true keywords.workspace = true categories.workspace = true @@ -23,4 +23,3 @@ object_store = { workspace = true } ic_cose_types = { workspace = true } tokio-util = { workspace = true } reqwest = { workspace = true } -rand = { workspace = true } diff --git a/anda_core/src/context.rs b/anda_core/src/context.rs index b0e42ef..c27420e 100644 --- a/anda_core/src/context.rs +++ b/anda_core/src/context.rs @@ -97,18 +97,6 @@ pub trait StateFeatures: Sized { /// Gets the time elapsed since the original context was created fn time_elapsed(&self) -> Duration; - - /// Gets current unix timestamp in milliseconds - fn unix_ms() -> u64; - - /// Generates N random bytes - fn rand_bytes() -> [u8; N]; - - /// Generates a random number within the given range - fn rand_number(range: R) -> T - where - T: rand::distributions::uniform::SampleUniform, - R: rand::distributions::uniform::SampleRange; } /// Provides vector search capabilities for semantic similarity search @@ -156,6 +144,14 @@ pub trait KnowledgeFeatures: Sized { user: Option, ) -> impl Future, BoxError>> + Send; + /// Retrieves the latest n Knowledge documents created in last N seconds + fn knowledge_latest_n( + &self, + last_seconds: u32, + n: usize, + user: Option, + ) -> impl Future, BoxError>> + Send; + /// Adds a list of Knowledge documents to the knowledge store fn knowledge_add( &self, diff --git a/anda_core/src/lib.rs b/anda_core/src/lib.rs index b74ae2e..e322971 100644 --- a/anda_core/src/lib.rs +++ b/anda_core/src/lib.rs @@ -20,11 +20,10 @@ pub type BoxError = Box; /// A type alias for a boxed future that is thread-safe and sendable across threads. pub type BoxPinFut = Pin + Send>>; -/// Joins two paths together -pub fn join_path(a: &Path, b: &Path) -> Path { - Path::from(format!("{}/{}", a, b)) +/// Converts a path to lowercase path. +pub fn path_lowercase(path: &Path) -> Path { + Path::from(path.as_ref().to_ascii_lowercase()) } - /// Validates a path part to ensure it doesn't contain the path delimiter /// agent name and user name should be validated. pub fn validate_path_part(part: &str) -> Result<(), BoxError> { @@ -40,12 +39,9 @@ mod tests { use super::*; #[test] - fn test_join_path() { - let a = Path::from("a/foo/"); - let b = Path::from("/b/bar"); - assert_eq!(a.as_ref(), "a/foo"); - assert_eq!(b.as_ref(), "b/bar"); - assert_eq!(join_path(&a, &b), Path::from("a/foo/b/bar")); + fn test_path_lowercase() { + let a = Path::from("a/Foo"); + assert_eq!(path_lowercase(&a).as_ref(), "a/foo"); } #[test] diff --git a/anda_core/src/model.rs b/anda_core/src/model.rs index 5a3316e..16014df 100644 --- a/anda_core/src/model.rs +++ b/anda_core/src/model.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::{collections::BTreeMap, future::Future}; -use crate::BoxError; +use crate::{BoxError, Knowledge}; /// Represents the output of an agent execution #[derive(Debug, Clone, Default, Deserialize, Serialize)] @@ -34,12 +34,12 @@ pub struct ToolCall { /// Represents a message in the agent's conversation history #[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct MessageInput { +pub struct Message { /// Message role: "developer", "system", "user", "assistant", "tool" pub role: String, - /// The content of the message - pub content: String, + /// The content of the message, can be text or structured data + pub content: Value, /// An optional name for the participant. Provides the model information to differentiate between participants of the same role. #[serde(skip_serializing_if = "Option::is_none")] @@ -60,7 +60,7 @@ pub struct FunctionDefinition { pub description: String, /// JSON schema defining the function's parameters - pub parameters: serde_json::Value, + pub parameters: Value, /// Whether to enable strict schema adherence when generating the function call. If set to true, the model will follow the exact schema defined in the parameters field. Only a subset of JSON Schema is supported when strict is true. #[serde(skip_serializing_if = "Option::is_none")] @@ -76,6 +76,24 @@ pub struct Document { pub additional_props: BTreeMap, } +impl From for Document { + fn from(doc: Knowledge) -> Self { + let mut additional_props = BTreeMap::new(); + additional_props.insert("user".to_string(), doc.user); + if let Value::Object(obj) = doc.meta { + for (k, v) in obj { + additional_props.insert(k, v.to_string()); + } + } + + Document { + id: doc.id, + text: doc.text, + additional_props, + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Documents(pub Vec); @@ -99,6 +117,12 @@ impl From> for Documents { } } +impl From> for Documents { + fn from(docs: Vec) -> Self { + Self(docs.into_iter().map(Document::from).collect()) + } +} + impl std::ops::Deref for Documents { type Target = Vec; @@ -152,8 +176,10 @@ pub struct CompletionRequest { /// The system message to be sent to the completion model provider, as the "system" role pub system: Option, + pub system_name: Option, + /// The chat history to be sent to the completion model provider - pub chat_history: Vec, + pub chat_history: Vec, /// The documents to embed into the prompt pub documents: Documents, @@ -161,6 +187,9 @@ pub struct CompletionRequest { /// The prompt to be sent to the completion model provider as "user" role pub prompt: String, + /// The name of the prompter + pub prompter_name: Option, + /// The tools to be sent to the completion model provider pub tools: Vec, diff --git a/anda_engine/Cargo.toml b/anda_engine/Cargo.toml index 6314d7e..5cc8f2d 100644 --- a/anda_engine/Cargo.toml +++ b/anda_engine/Cargo.toml @@ -3,7 +3,7 @@ name = "anda_engine" description = "Agents engine for Anda -- a framework for AI agent development." repository = "https://github.com/ldclabs/anda/tree/main/anda_engine" publish = true -version = "0.3.0" +version = "0.3.1" edition.workspace = true keywords.workspace = true categories.workspace = true @@ -31,6 +31,7 @@ moka = { workspace = true } toml = { workspace = true } tokio = { workspace = true } log = { workspace = true } +chrono = { workspace = true } schemars = { version = "0.8" } [dev-dependencies] diff --git a/anda_engine/src/context/agent.rs b/anda_engine/src/context/agent.rs index 9350493..684aa7d 100644 --- a/anda_engine/src/context/agent.rs +++ b/anda_engine/src/context/agent.rs @@ -1,11 +1,10 @@ use anda_core::{ AgentContext, AgentOutput, AgentSet, BaseContext, BoxError, CacheExpiry, CacheFeatures, CancellationToken, CanisterFeatures, CompletionFeatures, CompletionRequest, Embedding, - EmbeddingFeatures, FunctionDefinition, HttpFeatures, KeysFeatures, MessageInput, ObjectMeta, - Path, PutMode, PutResult, StateFeatures, StoreFeatures, ToolCall, ToolSet, + EmbeddingFeatures, FunctionDefinition, HttpFeatures, KeysFeatures, Message, ObjectMeta, Path, + PutMode, PutResult, StateFeatures, StoreFeatures, ToolCall, ToolSet, }; use candid::{utils::ArgumentEncoder, CandidType, Principal}; -use rand::Rng; use serde::{de::DeserializeOwned, Serialize}; use std::{future::Future, sync::Arc, time::Duration}; @@ -137,7 +136,7 @@ impl CompletionFeatures for AgentCtx { loop { let mut res = self.model.completion(req.clone()).await?; // 自动执行 tools 调用 - let mut tool_calls_continue: Vec = Vec::new(); + let mut tool_calls_continue: Vec = Vec::new(); if let Some(tool_calls) = &mut res.tool_calls { // 移除已处理的 tools req.tools @@ -147,9 +146,9 @@ impl CompletionFeatures for AgentCtx { Ok((val, con)) => { if con { // 需要使用大模型继续处理 tool 返回结果 - tool_calls_continue.push(MessageInput { + tool_calls_continue.push(Message { role: "tool".to_string(), - content: val.clone(), + content: val.clone().into(), name: Some(tool.name.clone()), tool_call_id: Some(tool.id.clone()), }); @@ -218,23 +217,6 @@ impl StateFeatures for AgentCtx { fn time_elapsed(&self) -> Duration { self.base.time_elapsed() } - - fn unix_ms() -> u64 { - BaseCtx::unix_ms() - } - - fn rand_bytes() -> [u8; N] { - BaseCtx::rand_bytes() - } - - fn rand_number(range: R) -> T - where - T: rand::distributions::uniform::SampleUniform, - R: rand::distributions::uniform::SampleRange, - { - let mut rng = rand::thread_rng(); - rng.gen_range(range) - } } impl KeysFeatures for AgentCtx { diff --git a/anda_engine/src/context/base.rs b/anda_engine/src/context/base.rs index 3d11bdb..b50ecfa 100644 --- a/anda_engine/src/context/base.rs +++ b/anda_engine/src/context/base.rs @@ -1,28 +1,20 @@ use anda_core::{ - canister_rpc, cbor_rpc, http_rpc, BaseContext, BoxError, CacheExpiry, CacheFeatures, - CancellationToken, CanisterFeatures, HttpFeatures, HttpRPCError, KeysFeatures, ObjectMeta, - Path, PutMode, PutResult, RPCRequest, StateFeatures, StoreFeatures, + BaseContext, BoxError, CacheExpiry, CacheFeatures, CancellationToken, CanisterFeatures, + HttpFeatures, KeysFeatures, ObjectMeta, Path, PutMode, PutResult, StateFeatures, StoreFeatures, }; use candid::{utils::ArgumentEncoder, CandidType, Principal}; -use ciborium::from_reader; -use ic_cose::rand_bytes; -use ic_cose_types::{cose::sha3_256, to_cbor_bytes}; -use rand::Rng; -use reqwest::Client; use serde::{de::DeserializeOwned, Serialize}; use std::{ - collections::HashMap, future::Future, sync::Arc, time::{Duration, Instant}, }; -use structured_logger::unix_ms; const CONTEXT_MAX_DEPTH: u8 = 42; const CACHE_MAX_CAPACITY: u64 = 1000000; -use super::{cache::CacheService, keys::KeysService}; -use crate::{store::Store, APP_USER_AGENT}; +use super::{cache::CacheService, tee::TEEClient}; +use crate::store::Store; #[derive(Clone)] pub struct BaseCtx { @@ -33,49 +25,22 @@ pub struct BaseCtx { pub(crate) start_at: Instant, pub(crate) depth: u8, - local_http: Client, - outer_http: Client, cache: Arc, - keys: Arc, + tee: Arc, store: Store, - endpoint_identity: String, - endpoint_canister_query: String, - endpoint_canister_update: String, } impl BaseCtx { - pub(crate) fn new( - tee_host: &str, - cancellation_token: CancellationToken, - local_http: Client, - store: Store, - ) -> Self { - let outer_http = reqwest::Client::builder() - .use_rustls_tls() - .https_only(true) - .http2_keep_alive_interval(Some(Duration::from_secs(25))) - .http2_keep_alive_timeout(Duration::from_secs(15)) - .http2_keep_alive_while_idle(true) - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) - .user_agent(APP_USER_AGENT) - .build() - .expect("Anda reqwest client should build"); - + pub(crate) fn new(cancellation_token: CancellationToken, tee: TEEClient, store: Store) -> Self { Self { user: None, caller: None, path: Path::default(), cancellation_token, start_at: Instant::now(), - local_http: local_http.clone(), - outer_http, cache: Arc::new(CacheService::new(CACHE_MAX_CAPACITY)), store, - keys: Arc::new(KeysService::new(format!("{}/keys", tee_host), local_http)), - endpoint_identity: format!("{}/identity", tee_host), - endpoint_canister_query: format!("{}/canister/query", tee_host), - endpoint_canister_update: format!("{}/canister/update", tee_host), + tee: Arc::new(tee), depth: 0, } } @@ -137,31 +102,12 @@ impl StateFeatures for BaseCtx { fn time_elapsed(&self) -> Duration { self.start_at.elapsed() } - - fn unix_ms() -> u64 { - unix_ms() - } - - /// Generates N random bytes - fn rand_bytes() -> [u8; N] { - rand_bytes() - } - - /// Generates a random number within the given range - fn rand_number(range: R) -> T - where - T: rand::distributions::uniform::SampleUniform, - R: rand::distributions::uniform::SampleRange, - { - let mut rng = rand::thread_rng(); - rng.gen_range(range) - } } impl KeysFeatures for BaseCtx { /// Derives a 256-bit AES-GCM key from the given derivation path async fn a256gcm_key(&self, derivation_path: &[&[u8]]) -> Result<[u8; 32], BoxError> { - self.keys.a256gcm_key(&self.path, derivation_path).await + self.tee.a256gcm_key(&self.path, derivation_path).await } /// Signs a message using Ed25519 signature scheme from the given derivation path @@ -170,7 +116,7 @@ impl KeysFeatures for BaseCtx { derivation_path: &[&[u8]], message: &[u8], ) -> Result<[u8; 64], BoxError> { - self.keys + self.tee .ed25519_sign_message(&self.path, derivation_path, message) .await } @@ -182,14 +128,14 @@ impl KeysFeatures for BaseCtx { message: &[u8], signature: &[u8], ) -> Result<(), BoxError> { - self.keys + self.tee .ed25519_verify(&self.path, derivation_path, message, signature) .await } /// Gets the public key for Ed25519 from the given derivation path async fn ed25519_public_key(&self, derivation_path: &[&[u8]]) -> Result<[u8; 32], BoxError> { - self.keys + self.tee .ed25519_public_key(&self.path, derivation_path) .await } @@ -200,7 +146,7 @@ impl KeysFeatures for BaseCtx { derivation_path: &[&[u8]], message: &[u8], ) -> Result<[u8; 64], BoxError> { - self.keys + self.tee .secp256k1_sign_message_bip340(&self.path, derivation_path, message) .await } @@ -212,7 +158,7 @@ impl KeysFeatures for BaseCtx { message: &[u8], signature: &[u8], ) -> Result<(), BoxError> { - self.keys + self.tee .secp256k1_verify_bip340(&self.path, derivation_path, message, signature) .await } @@ -223,7 +169,7 @@ impl KeysFeatures for BaseCtx { derivation_path: &[&[u8]], message: &[u8], ) -> Result<[u8; 64], BoxError> { - self.keys + self.tee .secp256k1_sign_message_ecdsa(&self.path, derivation_path, message) .await } @@ -235,14 +181,14 @@ impl KeysFeatures for BaseCtx { message: &[u8], signature: &[u8], ) -> Result<(), BoxError> { - self.keys + self.tee .secp256k1_verify_ecdsa(&self.path, derivation_path, message, signature) .await } /// Gets the compressed SEC1-encoded public key for Secp256k1 from the given derivation path async fn secp256k1_public_key(&self, derivation_path: &[&[u8]]) -> Result<[u8; 33], BoxError> { - self.keys + self.tee .secp256k1_public_key(&self.path, derivation_path) .await } @@ -357,15 +303,7 @@ impl CanisterFeatures for BaseCtx { method: &str, args: In, ) -> Result { - let res = canister_rpc( - &self.local_http, - &self.endpoint_canister_query, - canister, - method, - args, - ) - .await?; - Ok(res) + self.tee.canister_query(canister, method, args).await } /// Performs an update call to a canister (may modify state) @@ -383,15 +321,7 @@ impl CanisterFeatures for BaseCtx { method: &str, args: In, ) -> Result { - let res = canister_rpc( - &self.local_http, - &self.endpoint_canister_update, - canister, - method, - args, - ) - .await?; - Ok(res) + self.tee.canister_update(canister, method, args).await } } @@ -410,18 +340,7 @@ impl HttpFeatures for BaseCtx { headers: Option, body: Option>, // default is empty ) -> Result { - if !url.starts_with("https://") { - return Err("Invalid URL, must start with https://".into()); - } - let mut req = self.outer_http.request(method, url); - if let Some(headers) = headers { - req = req.headers(headers); - } - if let Some(body) = body { - req = req.body(body); - } - - req.send().await.map_err(|e| e.into()) + self.tee.https_call(url, method, headers, body).await } /// Makes a signed HTTPs request with message authentication @@ -440,21 +359,9 @@ impl HttpFeatures for BaseCtx { headers: Option, body: Option>, // default is empty ) -> Result { - let res: HashMap = http_rpc( - &self.local_http, - &self.endpoint_identity, - "sign_http", - &(message_digest,), - ) - .await?; - let mut headers = headers.unwrap_or_default(); - res.into_iter().for_each(|(k, v)| { - headers.insert( - http::HeaderName::try_from(k).expect("invalid header name"), - http::HeaderValue::try_from(v).expect("invalid header value"), - ); - }); - self.https_call(url, method, Some(headers), body).await + self.tee + .https_signed_call(url, method, message_digest, headers, body) + .await } /// Makes a signed CBOR-encoded RPC call @@ -472,34 +379,6 @@ impl HttpFeatures for BaseCtx { where T: DeserializeOwned, { - let params = to_cbor_bytes(¶ms); - let req = RPCRequest { - method, - params: ¶ms.into(), - }; - let body = to_cbor_bytes(&req); - let digest: [u8; 32] = sha3_256(&body); - let res: HashMap = http_rpc( - &self.local_http, - &self.endpoint_identity, - "sign_http", - &(digest,), - ) - .await?; - let mut headers = http::HeaderMap::new(); - res.into_iter().for_each(|(k, v)| { - headers.insert( - http::HeaderName::try_from(k).expect("invalid header name"), - http::HeaderValue::try_from(v).expect("invalid header value"), - ); - }); - - let res = cbor_rpc(&self.outer_http, endpoint, method, Some(headers), body).await?; - let res = from_reader(&res[..]).map_err(|e| HttpRPCError::ResultError { - endpoint: endpoint.to_string(), - path: method.to_string(), - error: e.into(), - })?; - Ok(res) + self.tee.https_signed_rpc(endpoint, method, params).await } } diff --git a/anda_engine/src/context/cache.rs b/anda_engine/src/context/cache.rs index b177be1..79d8758 100644 --- a/anda_engine/src/context/cache.rs +++ b/anda_engine/src/context/cache.rs @@ -1,5 +1,5 @@ -use anda_core::context::CacheExpiry; use anda_core::BoxError; +use anda_core::{context::CacheExpiry, path_lowercase}; use bytes::Bytes; use ciborium::from_reader; use ic_cose_types::to_cbor_bytes; @@ -34,7 +34,8 @@ impl CacheService { impl CacheService { /// Checks if a key exists in the cache pub fn contains(&self, path: &Path, key: &str) -> bool { - self.cache.contains_key(path.child(key).as_ref()) + self.cache + .contains_key(path_lowercase(&path.child(key)).as_ref()) } /// Gets a cached value by key, returns error if not found or deserialization fails @@ -42,7 +43,11 @@ impl CacheService { where T: DeserializeOwned, { - if let Some(val) = self.cache.get(path.child(key).as_ref()).await { + if let Some(val) = self + .cache + .get(path_lowercase(&path.child(key)).as_ref()) + .await + { from_reader(&val.0[..]).map_err(|err| err.into()) } else { Err(format!("key {} not found", key).into()) @@ -60,7 +65,7 @@ impl CacheService { futures_util::pin_mut!(init); match self .cache - .try_get_with(path.child(key).into(), async move { + .try_get_with(path_lowercase(&path.child(key)).into(), async move { match init.await { Ok((val, expiry)) => { let data = to_cbor_bytes(&val); @@ -83,13 +88,19 @@ impl CacheService { { let data = to_cbor_bytes(&val.0); self.cache - .insert(path.child(key).into(), Arc::new((data.into(), val.1))) + .insert( + path_lowercase(&path.child(key)).into(), + Arc::new((data.into(), val.1)), + ) .await; } /// Deletes a cached value by key, returns true if key existed pub async fn delete(&self, path: &Path, key: &str) -> bool { - self.cache.remove(path.child(key).as_ref()).await.is_some() + self.cache + .remove(path_lowercase(&path.child(key)).as_ref()) + .await + .is_some() } } diff --git a/anda_engine/src/context/keys.rs b/anda_engine/src/context/keys.rs deleted file mode 100644 index d84d8e7..0000000 --- a/anda_engine/src/context/keys.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anda_core::{http_rpc, BoxError}; -use ic_cose_types::cose::ed25519::ed25519_verify; -use ic_cose_types::cose::k256::{secp256k1_verify_bip340, secp256k1_verify_ecdsa}; -use object_store::path::Path; -use reqwest::Client; -use serde_bytes::ByteArray; - -#[derive(Debug, Clone)] -pub struct KeysService { - endpoint: String, - http: Client, -} - -impl KeysService { - pub fn new(endpoint: String, http: Client) -> Self { - Self { endpoint, http } - } -} - -impl KeysService { - /// Derives a 256-bit AES-GCM key from the given derivation path - pub async fn a256gcm_key( - &self, - path: &Path, - derivation_path: &[&[u8]], - ) -> Result<[u8; 32], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: ByteArray<32> = - http_rpc(&self.http, &self.endpoint, "a256gcm_key", &(dp,)).await?; - Ok(res.into_array()) - } - - /// Signs a message using Ed25519 signature scheme from the given derivation path - pub async fn ed25519_sign_message( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - ) -> Result<[u8; 64], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: ByteArray<64> = http_rpc( - &self.http, - &self.endpoint, - "ed25519_sign_message", - &(dp, message), - ) - .await?; - Ok(res.into_array()) - } - - /// Verifies an Ed25519 signature from the given derivation path - pub async fn ed25519_verify( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - signature: &[u8], - ) -> Result<(), BoxError> { - let pk = self.ed25519_public_key(path, derivation_path).await?; - ed25519_verify(&pk, message, signature).map_err(|e| e.into()) - } - - /// Gets the public key for Ed25519 from the given derivation path - pub async fn ed25519_public_key( - &self, - path: &Path, - derivation_path: &[&[u8]], - ) -> Result<[u8; 32], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: (ByteArray<32>, ByteArray<32>) = - http_rpc(&self.http, &self.endpoint, "ed25519_public_key", &(dp,)).await?; - Ok(res.0.into_array()) - } - - /// Signs a message using Secp256k1 BIP340 Schnorr signature from the given derivation path - pub async fn secp256k1_sign_message_bip340( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - ) -> Result<[u8; 64], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: ByteArray<64> = http_rpc( - &self.http, - &self.endpoint, - "secp256k1_sign_message_bip340", - &(dp, message), - ) - .await?; - Ok(res.into_array()) - } - - /// Verifies a Secp256k1 BIP340 Schnorr signature from the given derivation path - pub async fn secp256k1_verify_bip340( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - signature: &[u8], - ) -> Result<(), BoxError> { - let pk = self.secp256k1_public_key(path, derivation_path).await?; - secp256k1_verify_bip340(&pk, message, signature).map_err(|e| e.into()) - } - - /// Signs a message using Secp256k1 ECDSA signature from the given derivation path - pub async fn secp256k1_sign_message_ecdsa( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - ) -> Result<[u8; 64], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: ByteArray<64> = http_rpc( - &self.http, - &self.endpoint, - "secp256k1_sign_message_ecdsa", - &(dp, message), - ) - .await?; - Ok(res.into_array()) - } - - /// Verifies a Secp256k1 ECDSA signature from the given derivation path - pub async fn secp256k1_verify_ecdsa( - &self, - path: &Path, - derivation_path: &[&[u8]], - message: &[u8], - signature: &[u8], - ) -> Result<(), BoxError> { - let pk = self.secp256k1_public_key(path, derivation_path).await?; - secp256k1_verify_ecdsa(&pk, message, signature).map_err(|e| e.into()) - } - - /// Gets the compressed SEC1-encoded public key for Secp256k1 from the given derivation path - pub async fn secp256k1_public_key( - &self, - path: &Path, - derivation_path: &[&[u8]], - ) -> Result<[u8; 33], BoxError> { - let mut dp = Vec::with_capacity(derivation_path.len() + 1); - dp.push(path.as_ref().as_bytes()); - dp.extend(derivation_path); - let res: (ByteArray<33>, ByteArray<32>) = - http_rpc(&self.http, &self.endpoint, "secp256k1_public_key", &(dp,)).await?; - Ok(res.0.into_array()) - } -} diff --git a/anda_engine/src/context/mod.rs b/anda_engine/src/context/mod.rs index da1cc9c..4e8998d 100644 --- a/anda_engine/src/context/mod.rs +++ b/anda_engine/src/context/mod.rs @@ -1,9 +1,9 @@ mod agent; mod base; mod cache; -mod keys; +mod tee; pub use agent::*; pub use base::*; pub use cache::*; -pub use keys::*; +pub use tee::*; diff --git a/anda_engine/src/context/tee.rs b/anda_engine/src/context/tee.rs new file mode 100644 index 0000000..9017957 --- /dev/null +++ b/anda_engine/src/context/tee.rs @@ -0,0 +1,377 @@ +use anda_core::{ + canister_rpc, cbor_rpc, http_rpc, BoxError, CanisterFeatures, HttpFeatures, HttpRPCError, Path, + RPCRequest, CONTENT_TYPE_CBOR, +}; +use candid::{utils::ArgumentEncoder, CandidType, Principal}; +use ciborium::from_reader; +use ic_cose_types::cose::ed25519::ed25519_verify; +use ic_cose_types::cose::k256::{secp256k1_verify_bip340, secp256k1_verify_ecdsa}; +use ic_cose_types::{cose::sha3_256, to_cbor_bytes}; +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; +use serde_bytes::ByteArray; +use std::{collections::HashMap, time::Duration}; + +use crate::APP_USER_AGENT; + +#[derive(Clone)] +pub struct TEEClient { + pub http: Client, + pub outer_http: Client, + endpoint_keys: String, + endpoint_identity: String, + endpoint_canister_query: String, + endpoint_canister_update: String, +} + +impl TEEClient { + pub fn new(tee_host: &str, basic_token: &str) -> Self { + let http = reqwest::Client::builder() + .http2_keep_alive_interval(Some(Duration::from_secs(25))) + .http2_keep_alive_timeout(Duration::from_secs(15)) + .http2_keep_alive_while_idle(true) + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(20)) + .user_agent(APP_USER_AGENT) + .default_headers({ + let mut headers = http::header::HeaderMap::with_capacity(3); + let ct: http::HeaderValue = CONTENT_TYPE_CBOR.parse().unwrap(); + headers.insert(http::header::CONTENT_TYPE, ct.clone()); + headers.insert(http::header::ACCEPT, ct); + if !basic_token.is_empty() { + headers.insert(http::header::AUTHORIZATION, basic_token.parse().unwrap()); + } + + headers + }) + .build() + .expect("Anda reqwest client should build"); + + let outer_http = reqwest::Client::builder() + .use_rustls_tls() + .https_only(true) + .http2_keep_alive_interval(Some(Duration::from_secs(25))) + .http2_keep_alive_timeout(Duration::from_secs(15)) + .http2_keep_alive_while_idle(true) + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(30)) + .user_agent(APP_USER_AGENT) + .build() + .expect("Anda reqwest client should build"); + + Self { + http, + outer_http, + endpoint_keys: format!("{}/keys", tee_host), + endpoint_identity: format!("{}/identity", tee_host), + endpoint_canister_query: format!("{}/canister/query", tee_host), + endpoint_canister_update: format!("{}/canister/update", tee_host), + } + } + + /// Derives a 256-bit AES-GCM key from the given derivation path + pub async fn a256gcm_key( + &self, + path: &Path, + derivation_path: &[&[u8]], + ) -> Result<[u8; 32], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: ByteArray<32> = + http_rpc(&self.http, &self.endpoint_keys, "a256gcm_key", &(dp,)).await?; + Ok(res.into_array()) + } + + /// Signs a message using Ed25519 signature scheme from the given derivation path + pub async fn ed25519_sign_message( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + ) -> Result<[u8; 64], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: ByteArray<64> = http_rpc( + &self.http, + &self.endpoint_keys, + "ed25519_sign_message", + &(dp, message), + ) + .await?; + Ok(res.into_array()) + } + + /// Verifies an Ed25519 signature from the given derivation path + pub async fn ed25519_verify( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + signature: &[u8], + ) -> Result<(), BoxError> { + let pk = self.ed25519_public_key(path, derivation_path).await?; + ed25519_verify(&pk, message, signature).map_err(|e| e.into()) + } + + /// Gets the public key for Ed25519 from the given derivation path + pub async fn ed25519_public_key( + &self, + path: &Path, + derivation_path: &[&[u8]], + ) -> Result<[u8; 32], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: (ByteArray<32>, ByteArray<32>) = http_rpc( + &self.http, + &self.endpoint_keys, + "ed25519_public_key", + &(dp,), + ) + .await?; + Ok(res.0.into_array()) + } + + /// Signs a message using Secp256k1 BIP340 Schnorr signature from the given derivation path + pub async fn secp256k1_sign_message_bip340( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + ) -> Result<[u8; 64], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: ByteArray<64> = http_rpc( + &self.http, + &self.endpoint_keys, + "secp256k1_sign_message_bip340", + &(dp, message), + ) + .await?; + Ok(res.into_array()) + } + + /// Verifies a Secp256k1 BIP340 Schnorr signature from the given derivation path + pub async fn secp256k1_verify_bip340( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + signature: &[u8], + ) -> Result<(), BoxError> { + let pk = self.secp256k1_public_key(path, derivation_path).await?; + secp256k1_verify_bip340(&pk, message, signature).map_err(|e| e.into()) + } + + /// Signs a message using Secp256k1 ECDSA signature from the given derivation path + pub async fn secp256k1_sign_message_ecdsa( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + ) -> Result<[u8; 64], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: ByteArray<64> = http_rpc( + &self.http, + &self.endpoint_keys, + "secp256k1_sign_message_ecdsa", + &(dp, message), + ) + .await?; + Ok(res.into_array()) + } + + /// Verifies a Secp256k1 ECDSA signature from the given derivation path + pub async fn secp256k1_verify_ecdsa( + &self, + path: &Path, + derivation_path: &[&[u8]], + message: &[u8], + signature: &[u8], + ) -> Result<(), BoxError> { + let pk = self.secp256k1_public_key(path, derivation_path).await?; + secp256k1_verify_ecdsa(&pk, message, signature).map_err(|e| e.into()) + } + + /// Gets the compressed SEC1-encoded public key for Secp256k1 from the given derivation path + pub async fn secp256k1_public_key( + &self, + path: &Path, + derivation_path: &[&[u8]], + ) -> Result<[u8; 33], BoxError> { + let mut dp = Vec::with_capacity(derivation_path.len() + 1); + dp.push(path.as_ref().as_bytes()); + dp.extend(derivation_path); + let res: (ByteArray<33>, ByteArray<32>) = http_rpc( + &self.http, + &self.endpoint_keys, + "secp256k1_public_key", + &(dp,), + ) + .await?; + Ok(res.0.into_array()) + } +} + +impl CanisterFeatures for TEEClient { + /// Performs a query call to a canister (read-only, no state changes) + /// + /// # Arguments + /// * `canister` - Target canister principal + /// * `method` - Method name to call + /// * `args` - Input arguments encoded in Candid format + async fn canister_query< + In: ArgumentEncoder + Send, + Out: CandidType + for<'a> candid::Deserialize<'a>, + >( + &self, + canister: &Principal, + method: &str, + args: In, + ) -> Result { + let res = canister_rpc( + &self.http, + &self.endpoint_canister_query, + canister, + method, + args, + ) + .await?; + Ok(res) + } + + /// Performs an update call to a canister (may modify state) + /// + /// # Arguments + /// * `canister` - Target canister principal + /// * `method` - Method name to call + /// * `args` - Input arguments encoded in Candid format + async fn canister_update< + In: ArgumentEncoder + Send, + Out: CandidType + for<'a> candid::Deserialize<'a>, + >( + &self, + canister: &Principal, + method: &str, + args: In, + ) -> Result { + let res = canister_rpc( + &self.http, + &self.endpoint_canister_update, + canister, + method, + args, + ) + .await?; + Ok(res) + } +} + +impl HttpFeatures for TEEClient { + /// Makes an HTTPs request + /// + /// # Arguments + /// * `url` - Target URL, should start with `https://` + /// * `method` - HTTP method (GET, POST, etc.) + /// * `headers` - Optional HTTP headers + /// * `body` - Optional request body (default empty) + async fn https_call( + &self, + url: &str, + method: http::Method, + headers: Option, + body: Option>, // default is empty + ) -> Result { + if !url.starts_with("https://") { + return Err("Invalid URL, must start with https://".into()); + } + let mut req = self.outer_http.request(method, url); + if let Some(headers) = headers { + req = req.headers(headers); + } + if let Some(body) = body { + req = req.body(body); + } + + req.send().await.map_err(|e| e.into()) + } + + /// Makes a signed HTTPs request with message authentication + /// + /// # Arguments + /// * `url` - Target URL + /// * `method` - HTTP method (GET, POST, etc.) + /// * `message_digest` - 32-byte message digest for signing + /// * `headers` - Optional HTTP headers + /// * `body` - Optional request body (default empty) + async fn https_signed_call( + &self, + url: &str, + method: http::Method, + message_digest: &[u8; 32], + headers: Option, + body: Option>, // default is empty + ) -> Result { + let res: HashMap = http_rpc( + &self.http, + &self.endpoint_identity, + "sign_http", + &(message_digest,), + ) + .await?; + let mut headers = headers.unwrap_or_default(); + res.into_iter().for_each(|(k, v)| { + headers.insert( + http::HeaderName::try_from(k).expect("invalid header name"), + http::HeaderValue::try_from(v).expect("invalid header value"), + ); + }); + self.https_call(url, method, Some(headers), body).await + } + + /// Makes a signed CBOR-encoded RPC call + /// + /// # Arguments + /// * `endpoint` - URL endpoint to send the request to + /// * `method` - RPC method name to call + /// * `params` - Parameters to serialize as CBOR and send with the request + async fn https_signed_rpc( + &self, + endpoint: &str, + method: &str, + params: impl Serialize + Send, + ) -> Result + where + T: DeserializeOwned, + { + let params = to_cbor_bytes(¶ms); + let req = RPCRequest { + method, + params: ¶ms.into(), + }; + let body = to_cbor_bytes(&req); + let digest: [u8; 32] = sha3_256(&body); + let res: HashMap = + http_rpc(&self.http, &self.endpoint_identity, "sign_http", &(digest,)).await?; + let mut headers = http::HeaderMap::new(); + res.into_iter().for_each(|(k, v)| { + headers.insert( + http::HeaderName::try_from(k).expect("invalid header name"), + http::HeaderValue::try_from(v).expect("invalid header value"), + ); + }); + + let res = cbor_rpc(&self.outer_http, endpoint, method, Some(headers), body).await?; + let res = from_reader(&res[..]).map_err(|e| HttpRPCError::ResultError { + endpoint: endpoint.to_string(), + path: method.to_string(), + error: e.into(), + })?; + Ok(res) + } +} diff --git a/anda_engine/src/engine.rs b/anda_engine/src/engine.rs index c1d5930..b0061d5 100644 --- a/anda_engine/src/engine.rs +++ b/anda_engine/src/engine.rs @@ -3,18 +3,19 @@ use anda_core::{ }; use candid::Principal; use object_store::memory::InMemory; -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use tokio_util::sync::CancellationToken; use crate::{ - context::{AgentCtx, BaseCtx}, + context::{AgentCtx, BaseCtx, TEEClient}, model::Model, store::Store, - APP_USER_AGENT, }; static TEE_LOCAL_SERVER: &str = "http://127.0.0.1:8080"; +pub static ROOT_PATH: &str = "_"; + #[derive(Clone)] pub struct Engine { ctx: AgentCtx, @@ -31,19 +32,39 @@ impl Engine { self.name.clone() } + pub fn default_agent(&self) -> String { + self.default_agent.clone() + } + /// Cancel all tasks in engine. pub fn cancel(&self) { self.ctx.base.cancellation_token.cancel() } - /// Return the agent context without user and caller. - pub fn agent_ctx(&self, agent_name: &str) -> Result { - let name = agent_name.to_ascii_lowercase(); + /// Return a child cancellation token. + pub fn cancellation_token(&self) -> CancellationToken { + self.ctx.base.cancellation_token.child_token() + } + + /// Return the agent context with user and caller. + pub fn ctx_with( + &self, + agent: &A, + user: Option, + caller: Option, + ) -> Result + where + A: Agent, + { + let name = agent.name().to_ascii_lowercase(); if !self.ctx.agents.contains(&name) { return Err(format!("agent {} not found", name).into()); } + if let Some(user) = &user { + validate_path_part(user)?; + } - self.ctx.child(&name) + self.ctx.child_with(&name, user, caller) } pub async fn agent_run( @@ -101,8 +122,7 @@ pub struct EngineBuilder { agents: AgentSet, model: Model, store: Store, - tee_host: String, - basic_token: Option, + tee_client: TEEClient, cancellation_token: CancellationToken, } @@ -121,8 +141,7 @@ impl EngineBuilder { agents: AgentSet::new(), model: Model::not_implemented(), store: Store::new(mstore), - tee_host: TEE_LOCAL_SERVER.to_string(), - basic_token: None, + tee_client: TEEClient::new(TEE_LOCAL_SERVER, ""), cancellation_token: CancellationToken::new(), } } @@ -132,18 +151,18 @@ impl EngineBuilder { self } - pub fn with_tee_host(mut self, tee_host: String) -> Self { - self.tee_host = tee_host; + pub fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self { + self.cancellation_token = cancellation_token; self } - pub fn with_basic_token(mut self, basic_token: String) -> Self { - self.basic_token = Some(basic_token); + pub fn with_tee_client(mut self, tee_client: TEEClient) -> Self { + self.tee_client = tee_client; self } - pub fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self { - self.cancellation_token = cancellation_token; + pub fn with_model(mut self, model: Model) -> Self { + self.model = model; self } @@ -152,11 +171,6 @@ impl EngineBuilder { self } - pub fn with_model(mut self, model: Model) -> Self { - self.model = model; - self - } - pub fn register_tool(mut self, tool: T) -> Result where T: Tool + Send + Sync + 'static, @@ -207,52 +221,24 @@ impl EngineBuilder { Ok(self) } - pub async fn build(self, default_agent: String) -> Result { + pub fn build(self, default_agent: String) -> Result { if !self.agents.contains(&default_agent) { return Err(format!("default agent {} not found", default_agent).into()); } - let name = self.name; - let local_http = reqwest::Client::builder() - .http2_keep_alive_interval(Some(Duration::from_secs(25))) - .http2_keep_alive_timeout(Duration::from_secs(15)) - .http2_keep_alive_while_idle(true) - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(20)) - .user_agent(APP_USER_AGENT) - .default_headers({ - let mut headers = http::header::HeaderMap::new(); - if let Some(token) = self.basic_token { - headers.insert(http::header::AUTHORIZATION, token.parse().unwrap()); - } - headers - }) - .build()?; - - let ctx = BaseCtx::new( - &self.tee_host, - self.cancellation_token.clone(), - local_http.clone(), - self.store, - ); - + let ctx = BaseCtx::new(self.cancellation_token, self.tee_client, self.store); let ctx = AgentCtx::new(ctx, self.model, Arc::new(self.tools), Arc::new(self.agents)); Ok(Engine { ctx, - name, + name: self.name, default_agent, }) } #[cfg(test)] pub fn mock_ctx(self) -> AgentCtx { - let ctx = BaseCtx::new( - &self.tee_host, - self.cancellation_token, - reqwest::Client::new(), - self.store, - ); + let ctx = BaseCtx::new(self.cancellation_token, self.tee_client, self.store); AgentCtx::new(ctx, self.model, Arc::new(self.tools), Arc::new(self.agents)) } } diff --git a/anda_engine/src/plugin/attention.rs b/anda_engine/src/extension/attention.rs similarity index 96% rename from anda_engine/src/plugin/attention.rs rename to anda_engine/src/extension/attention.rs index 1f879ee..7de4fda 100644 --- a/anda_engine/src/plugin/attention.rs +++ b/anda_engine/src/extension/attention.rs @@ -1,6 +1,4 @@ -use anda_core::{ - evaluate_tokens, AgentOutput, CompletionFeatures, CompletionRequest, MessageInput, -}; +use anda_core::{evaluate_tokens, AgentOutput, CompletionFeatures, CompletionRequest, Message}; use crate::context::AgentCtx; @@ -116,20 +114,18 @@ impl Attention { pub async fn should_reply( &self, ctx: &AgentCtx, + my_name: &str, topics: &[String], - recent_messages: &[MessageInput], - message: &MessageInput, + recent_messages: &[Message], + message: &Message, ) -> AttentionCommand { - if self - .phrases - .iter() - .any(|phrase| message.content.contains(phrase)) - { + let content = message.content.to_string().to_lowercase(); + if self.phrases.iter().any(|phrase| content.contains(phrase)) { return AttentionCommand::Stop; } // Ignore very short messages - if evaluate_tokens(&message.content) < self.min_prompt_tokens { + if evaluate_tokens(&content) < self.min_prompt_tokens { return AttentionCommand::Ignore; } @@ -151,6 +147,7 @@ impl Attention { let req = CompletionRequest { system: Some(format!("\ + You are {my_name}.\n\ You are part of a multi-user discussion environment. Your primary task is to evaluate the relevance of each message to your assigned conversation topics and decide whether to respond. Always prioritize messages that directly mention you or are closely related to the conversation topic.\n\n\ Response options:\n\ - {RESPOND_COMMAND}: The message is directly addressed to you or is highly relevant to the conversation topic.\n\ diff --git a/anda_engine/src/plugin/character.rs b/anda_engine/src/extension/character.rs similarity index 83% rename from anda_engine/src/plugin/character.rs rename to anda_engine/src/extension/character.rs index e34a94e..827b2e9 100644 --- a/anda_engine/src/plugin/character.rs +++ b/anda_engine/src/extension/character.rs @@ -1,27 +1,31 @@ use anda_core::{ evaluate_tokens, Agent, AgentContext, AgentOutput, BoxError, CacheExpiry, CacheFeatures, - CompletionFeatures, CompletionRequest, Documents, Embedding, EmbeddingFeatures, - KnowledgeFeatures, KnowledgeInput, MessageInput, StateFeatures, VectorSearchFeatures, + CompletionFeatures, CompletionRequest, Documents, Embedding, EmbeddingFeatures, Knowledge, + KnowledgeFeatures, KnowledgeInput, Message, StateFeatures, VectorSearchFeatures, }; +use chrono::prelude::*; use ic_cose_types::to_cbor_bytes; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; -use crate::{ - context::AgentCtx, - plugin::attention::{Attention, AttentionCommand, ContentQuality}, - plugin::segmenter::DocumentSegmenter, - store::MAX_STORE_OBJECT_SIZE, +use super::{ + attention::{Attention, AttentionCommand, ContentQuality}, + segmenter::DocumentSegmenter, }; +use crate::{context::AgentCtx, store::MAX_STORE_OBJECT_SIZE}; + const MAX_CHAT_HISTORY: usize = 42; const CHAT_HISTORY_TTI: Duration = Duration::from_secs(3600 * 24 * 7); #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Character { - /// Name of the character, e.g. "Anda" + /// Name of the character, e.g. "Anda ICP" pub name: String, + /// Character’s account or username, e.g. "AndaICP" + pub username: String, + /// Character’s profession, status, or role, e.g. "Scientist and Prophet" pub identity: String, @@ -77,21 +81,27 @@ impl Character { Ok(content) } - pub fn to_request(&self, prompt: String) -> CompletionRequest { + pub fn to_request(&self, prompt: String, prompter_name: Option) -> CompletionRequest { + let utc: DateTime = Utc::now(); let system = format!( "Character Definition:\n\ Your name: {}\n\ + Your username: {}\n\ Your identity: {}\n\ Background: {}\n\ Personality traits: {}\n\ Motivations and goals: {}\n\ - Topics of expertise: {}", + Topics of expertise: {}\n\ + The current time is {}.\ + ", self.name, + self.username, self.identity, self.description, self.traits.join(", "), self.goals.join(", "), self.topics.join(", "), + utc.to_rfc3339_opts(SecondsFormat::Secs, true) ); let style_context = format!( @@ -118,13 +128,15 @@ impl Character { CompletionRequest { system: Some(system), + system_name: Some(self.name.clone()), prompt, + prompter_name, ..Default::default() } .context("style_context".to_string(), style_context) } - pub fn build( + pub fn build( self, attention: Attention, segmenter: DocumentSegmenter, @@ -135,14 +147,14 @@ impl Character { } #[derive(Debug, Clone)] -pub struct CharacterAgent { - character: Character, - attention: Attention, - segmenter: DocumentSegmenter, - knowledge: K, +pub struct CharacterAgent { + pub character: Arc, + pub attention: Arc, + pub segmenter: Arc, + pub knowledge: Arc, } -impl CharacterAgent { +impl CharacterAgent { pub fn new( character: Character, attention: Attention, @@ -150,12 +162,23 @@ impl CharacterAgent { knowledge: K, ) -> Self { Self { - character, - attention, - segmenter, - knowledge, + character: Arc::new(character), + attention: Arc::new(attention), + segmenter: Arc::new(segmenter), + knowledge: Arc::new(knowledge), } } + + pub async fn latest_knowledge( + &self, + last_seconds: u32, + n: usize, + user: Option, + ) -> Result, BoxError> { + self.knowledge + .knowledge_latest_n(last_seconds, n, user) + .await + } } impl Agent for CharacterAgent @@ -163,7 +186,7 @@ where K: KnowledgeFeatures + VectorSearchFeatures + Clone + Send + Sync + 'static, { fn name(&self) -> String { - self.character.name.clone() + self.character.username.clone() } fn description(&self) -> String { @@ -182,7 +205,7 @@ where ) -> Result { // read chat history from store let mut chat_history = if let Some(user) = ctx.user() { - let chat: Vec = ctx + let chat: Vec = ctx .cache_get_with(&user, async { Ok((Vec::new(), Some(CacheExpiry::TTI(CHAT_HISTORY_TTI)))) }) @@ -194,19 +217,20 @@ where let mut content_quality = ContentQuality::Ignore; if evaluate_tokens(&prompt) <= self.attention.min_content_tokens { - let recent_messages: Vec = vec![]; + let recent_messages: Vec = vec![]; match self .attention .should_reply( &ctx, + &self.character.username, &self.character.topics, chat_history .as_ref() .map(|(_, c)| c) .unwrap_or(&recent_messages), - &MessageInput { + &Message { role: "user".to_string(), - content: prompt.clone(), + content: prompt.clone().into(), name: ctx.user(), ..Default::default() }, @@ -237,7 +261,7 @@ where if content_quality > ContentQuality::Ignore { let content = prompt.clone(); let ctx = ctx.clone(); - let user = ctx.user().unwrap_or("user".to_string()); + let user = ctx.user().unwrap_or("anonymous".to_string()); let segmenter = self.segmenter.clone(); let knowledge = self.knowledge.clone(); @@ -245,7 +269,7 @@ where tokio::spawn(async move { let (docs, _) = segmenter.segment(&ctx, &content).await?; let mut vecs: Vec = Vec::with_capacity(docs.segments.len()); - for texts in docs.segments.chunks(90) { + for texts in docs.segments.chunks(16) { match ctx.embed(texts.to_owned()).await { Ok(embeddings) => vecs.extend(embeddings), Err(err) => { @@ -283,15 +307,15 @@ where let mut req = self .character - .to_request(prompt) + .to_request(prompt, ctx.user()) .append_documents(knowledges) .append_tools(tools); if let Some((user, chat)) = &mut chat_history { req.chat_history = chat.clone(); - chat.push(MessageInput { + chat.push(Message { role: "user".to_string(), - content: req.prompt.clone(), + content: req.prompt.clone().into(), name: Some(user.clone()), ..Default::default() }); @@ -300,17 +324,17 @@ where let res = ctx.completion(req).await?; if res.failed_reason.is_none() { if !res.content.is_empty() { - chat.push(MessageInput { + chat.push(Message { role: "assistant".to_string(), - content: res.content.clone(), + content: res.content.clone().into(), ..Default::default() }); } if let Some(tool_calls) = &res.tool_calls { for tool_res in tool_calls { - chat.push(MessageInput { + chat.push(Message { role: "tool".to_string(), - content: "".to_string(), + content: "".to_string().into(), name: Some(tool_res.name.clone()), tool_call_id: Some(tool_res.id.clone()), }); @@ -384,7 +408,7 @@ mod tests { tools: vec!["submit_character".to_string()], ..Default::default() }; - let req = character.to_request("Who are you?".to_string()); + let req = character.to_request("Who are you?".to_string(), None); println!("{}\n", req.system.as_ref().unwrap()); println!("{}\n", req.prompt_with_context()); println!("{:?}", req.tools); diff --git a/anda_engine/src/plugin/extractor.rs b/anda_engine/src/extension/extractor.rs similarity index 100% rename from anda_engine/src/plugin/extractor.rs rename to anda_engine/src/extension/extractor.rs diff --git a/anda_engine/src/plugin/mod.rs b/anda_engine/src/extension/mod.rs similarity index 100% rename from anda_engine/src/plugin/mod.rs rename to anda_engine/src/extension/mod.rs diff --git a/anda_engine/src/plugin/segmenter.rs b/anda_engine/src/extension/segmenter.rs similarity index 95% rename from anda_engine/src/plugin/segmenter.rs rename to anda_engine/src/extension/segmenter.rs index e21c346..70a88f2 100644 --- a/anda_engine/src/plugin/segmenter.rs +++ b/anda_engine/src/extension/segmenter.rs @@ -1,9 +1,7 @@ use anda_core::{evaluate_tokens, Agent, AgentOutput, BoxError, Tool, ToolCall}; -use crate::{ - context::AgentCtx, - plugin::extractor::{Deserialize, Extractor, JsonSchema, Serialize, SubmitTool}, -}; +use super::extractor::{Deserialize, Extractor, JsonSchema, Serialize, SubmitTool}; +use crate::context::AgentCtx; #[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] pub struct SegmentOutput { @@ -18,6 +16,12 @@ pub struct DocumentSegmenter { max_tokens: usize, } +impl Default for DocumentSegmenter { + fn default() -> Self { + Self::new(500, 8000) + } +} + impl DocumentSegmenter { pub fn new(segment_tokens: usize, max_tokens: usize) -> Self { let tool = SubmitTool::::new(); diff --git a/anda_engine/src/lib.rs b/anda_engine/src/lib.rs index ae964f8..20da73c 100644 --- a/anda_engine/src/lib.rs +++ b/anda_engine/src/lib.rs @@ -1,12 +1,30 @@ +use rand::Rng; + pub mod context; pub mod engine; +pub mod extension; pub mod model; -pub mod plugin; pub mod store; +/// Gets current unix timestamp in milliseconds +pub use structured_logger::unix_ms; + +/// Generates N random bytes +pub use ic_cose::rand_bytes; + pub static APP_USER_AGENT: &str = concat!( "Mozilla/5.0 anda.bot ", env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), ); + +/// Generates a random number within the given range +pub fn rand_number(range: R) -> T +where + T: rand::distributions::uniform::SampleUniform, + R: rand::distributions::uniform::SampleRange, +{ + let mut rng = rand::thread_rng(); + rng.gen_range(range) +} diff --git a/anda_engine/src/model/deepseek.rs b/anda_engine/src/model/deepseek.rs index c264585..643fcf7 100644 --- a/anda_engine/src/model/deepseek.rs +++ b/anda_engine/src/model/deepseek.rs @@ -1,8 +1,8 @@ //! OpenAI API client and Anda integration //! use anda_core::{ - AgentOutput, BoxError, BoxPinFut, CompletionRequest, FunctionDefinition, MessageInput, - ToolCall, CONTENT_TYPE_JSON, + AgentOutput, BoxError, BoxPinFut, CompletionRequest, FunctionDefinition, Message, ToolCall, + CONTENT_TYPE_JSON, }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -189,9 +189,10 @@ impl CompletionFeaturesDyn for CompletionModel { Box::pin(async move { // Add system to chat history (if available) let mut full_history = if let Some(system) = &req.system { - vec![MessageInput { + vec![Message { role: "system".into(), - content: system.clone(), + content: system.to_owned().into(), + name: req.system_name.clone(), ..Default::default() }] } else { @@ -202,9 +203,11 @@ impl CompletionFeaturesDyn for CompletionModel { full_history.append(&mut req.chat_history); // Add context documents to chat history - full_history.push(MessageInput { + let prompt_with_context = req.prompt_with_context(); + full_history.push(Message { role: "user".into(), - content: req.prompt, + content: prompt_with_context.into(), + name: req.prompter_name, ..Default::default() }); diff --git a/anda_engine/src/model/openai.rs b/anda_engine/src/model/openai.rs index 0703048..4f4a5a1 100644 --- a/anda_engine/src/model/openai.rs +++ b/anda_engine/src/model/openai.rs @@ -1,8 +1,8 @@ //! OpenAI API client and Anda integration //! use anda_core::{ - AgentOutput, BoxError, BoxPinFut, CompletionRequest, Embedding, FunctionDefinition, - MessageInput, ToolCall, CONTENT_TYPE_JSON, + AgentOutput, BoxError, BoxPinFut, CompletionRequest, Embedding, FunctionDefinition, Message, + ToolCall, CONTENT_TYPE_JSON, }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -387,13 +387,14 @@ impl CompletionFeaturesDyn for CompletionModel { Box::pin(async move { // Add preamble to chat history (if available) let mut full_history = if let Some(system) = &req.system { - vec![MessageInput { + vec![Message { role: if is_new { "developer".into() } else { "system".into() }, - content: system.clone(), + content: system.to_owned().into(), + name: req.system_name.clone(), ..Default::default() }] } else { @@ -405,9 +406,10 @@ impl CompletionFeaturesDyn for CompletionModel { // Add context documents to chat history let prompt_with_context = req.prompt_with_context(); - full_history.push(MessageInput { + full_history.push(Message { role: "user".into(), - content: prompt_with_context, + content: prompt_with_context.into(), + name: req.prompter_name, ..Default::default() }); diff --git a/anda_engine/src/store.rs b/anda_engine/src/store.rs index 0850c44..5e9758b 100644 --- a/anda_engine/src/store.rs +++ b/anda_engine/src/store.rs @@ -1,4 +1,4 @@ -use anda_core::{join_path, BoxError, BoxPinFut, ObjectMeta, Path, PutMode, PutResult}; +use anda_core::{path_lowercase, BoxError, BoxPinFut, ObjectMeta, Path, PutMode, PutResult}; use futures::TryStreamExt; use object_store::{ObjectStore, PutOptions}; use std::sync::Arc; @@ -121,7 +121,7 @@ impl Store { namespace: &Path, path: &Path, ) -> Result<(bytes::Bytes, ObjectMeta), BoxError> { - let path = join_path(namespace, path); + let path = path_lowercase(&namespace.child(path.as_ref())); let res = self.store.get_opts(&path, Default::default()).await?; let data = match res.payload { object_store::GetResultPayload::Stream(mut stream) => { @@ -147,8 +147,8 @@ impl Store { prefix: Option<&Path>, offset: &Path, ) -> Result, BoxError> { - let prefix = prefix.map(|p| join_path(namespace, p)); - let offset = join_path(namespace, offset); + let prefix = prefix.map(|p| path_lowercase(&namespace.child(p.as_ref()))); + let offset = path_lowercase(&namespace.child(offset.as_ref())); let mut res = self.store.list_with_offset(prefix.as_ref(), &offset); let mut metas = Vec::new(); while let Some(meta) = res.try_next().await? { @@ -171,7 +171,7 @@ impl Store { mode: PutMode, val: bytes::Bytes, ) -> Result { - let path = join_path(namespace, path); + let path = path_lowercase(&namespace.child(path.as_ref())); let res = self .store .put_opts( @@ -197,8 +197,8 @@ impl Store { from: &Path, to: &Path, ) -> Result<(), BoxError> { - let from = join_path(namespace, from); - let to = join_path(namespace, to); + let from = path_lowercase(&namespace.child(from.as_ref())); + let to = path_lowercase(&namespace.child(to.as_ref())); self.store.rename_if_not_exists(&from, &to).await?; Ok(()) } @@ -208,7 +208,7 @@ impl Store { /// # Arguments /// * `path` - Path of the object to delete pub async fn store_delete(&self, namespace: &Path, path: &Path) -> Result<(), BoxError> { - let path = join_path(namespace, path); + let path = path_lowercase(&namespace.child(path.as_ref())); self.store.delete(&path).await?; Ok(()) } diff --git a/anda_lancedb/Cargo.toml b/anda_lancedb/Cargo.toml index d09d0b4..4dcc01f 100644 --- a/anda_lancedb/Cargo.toml +++ b/anda_lancedb/Cargo.toml @@ -3,7 +3,7 @@ name = "anda_lancedb" description = "Anda vector store index integration for LanceDB" repository = "https://github.com/ldclabs/anda/tree/main/anda_lancedb" publish = false -version = "0.1.0" +version = "0.1.1" edition.workspace = true keywords.workspace = true categories.workspace = true diff --git a/anda_lancedb/src/knowledge.rs b/anda_lancedb/src/knowledge.rs index ef0a16a..b622d1d 100644 --- a/anda_lancedb/src/knowledge.rs +++ b/anda_lancedb/src/knowledge.rs @@ -1,4 +1,5 @@ use anda_core::{BoxError, Knowledge, KnowledgeFeatures, KnowledgeInput, VectorSearchFeatures}; +use anda_engine::unix_ms; use std::{sync::Arc, vec}; use crate::lancedb::*; @@ -11,6 +12,12 @@ pub struct KnowledgeStore { embedder: Option>, } +pub fn xid_from_timestamp(unix_secs: u32) -> xid::Id { + let mut id = [0u8; 12]; + id[0..4].copy_from_slice(&unix_secs.to_be_bytes()); + xid::Id(id) +} + impl KnowledgeStore { pub fn name(&self) -> &Path { &self.name @@ -66,7 +73,6 @@ impl KnowledgeStore { .create_index(&["vec"], Index::Auto) .execute() .await; - // println!("{:?}", res); Ok(()) } @@ -78,6 +84,9 @@ impl KnowledgeStore { impl VectorSearchFeatures for KnowledgeStore { async fn top_n(&self, query: &str, n: usize) -> Result, BoxError> { + if n == 0 { + return Ok(vec![]); + } let docs = hybrid_search( &self.table, self.embedder.clone(), @@ -92,6 +101,10 @@ impl VectorSearchFeatures for KnowledgeStore { } async fn top_n_ids(&self, query: &str, n: usize) -> Result, BoxError> { + if n == 0 { + return Ok(vec![]); + } + let ids = hybrid_search( &self.table, self.embedder.clone(), @@ -112,7 +125,11 @@ impl KnowledgeFeatures for KnowledgeStore { n: usize, user: Option, ) -> Result, BoxError> { - let filter = user.map(|user| format!("user = {user}")); + if n == 0 { + return Ok(vec![]); + } + + let filter = user.map(|user| format!("user = {:?}", user.to_ascii_lowercase())); let docs = hybrid_search( &self.table, self.embedder.clone(), @@ -140,6 +157,50 @@ impl KnowledgeFeatures for KnowledgeStore { Ok(docs) } + async fn knowledge_latest_n( + &self, + last_seconds: u32, + n: usize, + user: Option, + ) -> Result, BoxError> { + if last_seconds == 0 || n == 0 { + return Ok(vec![]); + } + + let timestamp = (unix_ms() / 1000).saturating_sub(last_seconds as u64); + let id = xid_from_timestamp(timestamp as u32).to_string(); + let filter = if let Some(user) = user { + format!("(id > {id:?}) AND (user = {:?})", user.to_ascii_lowercase()) + } else { + format!("id > {id:?}") + }; + let docs = hybrid_search( + &self.table, + self.embedder.clone(), + [ + "id".to_string(), + "user".to_string(), + "text".to_string(), + "meta".to_string(), + ], + "".to_string(), + n, + Some(filter), + ) + .await?; + let docs: Vec = docs + .into_iter() + .map(|doc| Knowledge { + id: doc[0].to_owned(), + user: doc[1].to_owned(), + text: doc[2].to_owned(), + meta: serde_json::from_str(&doc[3]).unwrap_or(serde_json::Value::Null), + }) + .collect(); + + Ok(docs) + } + async fn knowledge_add(&self, docs: Vec) -> Result<(), BoxError> { if docs.is_empty() { return Ok(()); @@ -162,7 +223,7 @@ impl KnowledgeFeatures for KnowledgeStore { } ids.push(xid::new().to_string()); - users.push(doc.user); + users.push(doc.user.to_ascii_lowercase()); texts.push(doc.text); metas.push(serde_json::to_string(&doc.meta)?); vecs.push(Some(doc.vec.into_iter().map(Some).collect())); @@ -224,13 +285,13 @@ mod tests { ks.knowledge_add(vec![ KnowledgeInput { - user: "a".to_string(), + user: "Anda".to_string(), text: "Hello".to_string(), meta: serde_json::json!({}), vec: vec![0.1; DIM as usize], }, KnowledgeInput { - user: "a".to_string(), + user: "Dom".to_string(), text: "Anda".to_string(), meta: serde_json::json!({}), vec: vec![0.1; DIM as usize], @@ -264,6 +325,26 @@ mod tests { .unwrap(); println!("{:?}", res); assert_eq!(res[0], res2[0].id); + + let res = ks.knowledge_latest_n(1, 10, None).await.unwrap(); + println!("latest_n\n{:?}", res); + assert_eq!(res.len(), 2); + + let res = ks + .knowledge_latest_n(1, 10, Some("Anda".to_string())) + .await + .unwrap(); + println!("latest_n Anda:\n{:?}", res); + assert_eq!(res.len(), 1); + assert_eq!(res[0].user, "anda"); + + let res = ks + .knowledge_latest_n(1, 10, Some("Dom".to_string())) + .await + .unwrap(); + println!("latest_n Dom:\n{:?}", res); + assert_eq!(res.len(), 1); + assert_eq!(res[0].user, "dom"); } #[tokio::test(flavor = "current_thread")] diff --git a/anda_lancedb/src/lancedb.rs b/anda_lancedb/src/lancedb.rs index 5e3110e..3404970 100644 --- a/anda_lancedb/src/lancedb.rs +++ b/anda_lancedb/src/lancedb.rs @@ -43,7 +43,16 @@ pub async fn hybrid_search( n: usize, filter: Option, ) -> Result, BoxError> { - let mut res = if let Some(embedder) = embedder { + let mut res = if query.is_empty() { + let mut q = table + .query() + .select(Select::Columns(select_columns.to_vec())) + .limit(n); + if let Some(filter) = filter { + q = q.only_if(filter); + } + q.execute().await? + } else if let Some(embedder) = embedder { let prompt_embedding = embedder.embed_query(query.clone()).await?; let mut q = table .vector_search(prompt_embedding.vec.clone())?