diff --git a/.github/workflows/rust-lint-test.yml b/.github/workflows/rust-lint-test.yml new file mode 100644 index 000000000..33f0ff977 --- /dev/null +++ b/.github/workflows/rust-lint-test.yml @@ -0,0 +1,56 @@ +name: Lint & Test + +on: + push: + branches: [ "main" ] + paths: + - "crates/**" + - ".github/workflows/rust.yml" + - "tests/**" + - "Cargo.toml" + - ".cargo/**" + pull_request: + branches: [ "main" ] + paths: + - "sdks/community/rust/crates/**" + - "sdks/community/rust/**/tests/**" + - "sdks/community/rust/Cargo.toml" + - "sdks/community/rust/.cargo/**" + - ".github/workflows/rust-lint-test.yml" + +defaults: + run: + working-directory: ./rust + +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + + name: Test multiple workspaces on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + env: + CARGO_TERM_COLOR: always + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --verbose + + - name: Check formatting + run: cargo fmt -- --check + + - name: Check clippy + run: cargo clippy -- -D warnings + + - name: Publish dry-run + run: cargo publish -p ag-ui-core --dry-run + + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index abb94deaa..6da3adf19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ **/.claude/settings.local.json .vscode/ .idea/ + +**/target diff --git a/sdks/community/rust/Cargo.lock b/sdks/community/rust/Cargo.lock new file mode 100644 index 000000000..441f3c189 --- /dev/null +++ b/sdks/community/rust/Cargo.lock @@ -0,0 +1,2026 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ag-ui-cli" +version = "0.1.0" +dependencies = [ + "ag-ui-core", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "ag-ui-client" +version = "0.1.0" +dependencies = [ + "ag-ui-core", + "async-trait", + "bytes", + "env_logger", + "futures", + "json-patch", + "log", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "ag-ui-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.12", + "uuid", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbb9122ea75b11bf96e7492afb723e8a7fbe12c67417aa95e7e3d18144d37cd" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/sdks/community/rust/Cargo.toml b/sdks/community/rust/Cargo.toml new file mode 100644 index 000000000..1fcda1f8a --- /dev/null +++ b/sdks/community/rust/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +resolver = "3" +members = ["crates/*"] + +[workspace.dependencies] +thiserror = "^2" +serde = { version = "^1", features = ["derive"] } +serde_json = "^1" +uuid = { version = "1.17.0", features = ["v4", "serde"] } + +[patch.crates-io] +ag-ui-core = { path = "crates/ag-ui-core" } +ag-ui-client = { path = "crates/ag-ui-client" } \ No newline at end of file diff --git a/sdks/community/rust/TODO b/sdks/community/rust/TODO new file mode 100644 index 000000000..0fd634ece --- /dev/null +++ b/sdks/community/rust/TODO @@ -0,0 +1,11 @@ +- [ ] Agent "state" handling (possibly with actor model) +- [X] Run parameters DX improvements +- [ ] More elaborate error handling (especially for deserialization errors) +- [ ] Retries? +- [ ] Documentation +- [ ] JSON Patch types in `ag-ui-core` +- [ ] Builder patterns on most-used structs +- [ ] Review EventHandler & Mutations +- [ ] Subscriber + mutations DX improvement +- [ ] Not all messages are being saved? +- [ ] SSE review diff --git a/sdks/community/rust/crates/ag-ui-cli/Cargo.toml b/sdks/community/rust/crates/ag-ui-cli/Cargo.toml new file mode 100644 index 000000000..7b7042aa8 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-cli/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ag-ui-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +ag-ui-core = { path = "../ag-ui-core"} +thiserror = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } \ No newline at end of file diff --git a/sdks/community/rust/crates/ag-ui-cli/src/main.rs b/sdks/community/rust/crates/ag-ui-cli/src/main.rs new file mode 100644 index 000000000..98ecdb013 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + todo!() +} diff --git a/sdks/community/rust/crates/ag-ui-client/Cargo.lock b/sdks/community/rust/crates/ag-ui-client/Cargo.lock new file mode 100644 index 000000000..4e5919a07 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ag-ui-client" +version = "0.1.0" diff --git a/sdks/community/rust/crates/ag-ui-client/Cargo.toml b/sdks/community/rust/crates/ag-ui-client/Cargo.toml new file mode 100644 index 000000000..a3ec65a81 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ag-ui-client" +version = "0.1.0" +edition = "2024" +description = "Client library for the AG-UI protocol." +license = "MIT" +readme = "README.md" + +[dependencies] +ag-ui-core = { version = "0.1.0" } +thiserror = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["v4"] } +futures = "0.3.31" +json-patch = "4.0.0" +log = "0.4.27" +reqwest = { version = "0.12.22" , features = ["json", "stream"]} +bytes = "1.5.0" +tokio = "1.36.0" + +[dev-dependencies] +env_logger = "0.11.8" +tokio = { version = "1.36.0", features = ["full"] } + +[[example]] +name = "http-agent" +path = "examples/basic_agent.rs" \ No newline at end of file diff --git a/sdks/community/rust/crates/ag-ui-client/README.md b/sdks/community/rust/crates/ag-ui-client/README.md new file mode 100644 index 000000000..c4a40dc72 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/README.md @@ -0,0 +1,36 @@ +# AG-UI Rust Client + +Rust client for working with the AG-UI protocol. The client API has been designed to mimic the Typescript client as +close as possible. However, a key difference is that state & messages are not yet an attribute of an implementation of +[`Agent`](src/agent.rs) because it would require `&mut self` for straightforward implementations. This is a work in +progress. + +## Example + +For each example make sure to read the instructions on starting the associated AG-UI server. + +### Basic + +```rust +use std::error::Error; +use ag_ui_client::{core::types::Message, Agent, HttpAgent, RunAgentParams}; + +#[tokio::main] +async fn main() -> Result<(), Box>{ + let agent = HttpAgent::builder() + .with_url_str("http://127.0.0.1:3001/")? + .build()?; + + let message = Message::new_user("Can you give me the current temperature in New York?"); + // Create run parameters + let params = RunAgentParams::new().add_message(message); + + // Run the agent with the subscriber + let result = agent.run_agent(¶ms, ()).await?; + + println!("{:#?}", result); + Ok(()) +} +``` + +For more examples check the [examples folder](examples). \ No newline at end of file diff --git a/sdks/community/rust/crates/ag-ui-client/examples/basic_agent.rs b/sdks/community/rust/crates/ag-ui-client/examples/basic_agent.rs new file mode 100644 index 000000000..b0a5361d8 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/examples/basic_agent.rs @@ -0,0 +1,19 @@ +use ag_ui_client::{Agent, HttpAgent, RunAgentParams, core::types::Message}; +use std::error::Error; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let agent = HttpAgent::builder() + .with_url_str("http://127.0.0.1:3001/")? + .build()?; + + let message = Message::new_user("Can you give me the current temperature in New York?"); + // Create run parameters + let params = RunAgentParams::new().add_message(message); + + // Run the agent with the subscriber + let result = agent.run_agent(¶ms, ()).await?; + + println!("{:#?}", result); + Ok(()) +} diff --git a/sdks/community/rust/crates/ag-ui-client/examples/generative_ui.rs b/sdks/community/rust/crates/ag-ui-client/examples/generative_ui.rs new file mode 100644 index 000000000..98c45e149 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/examples/generative_ui.rs @@ -0,0 +1,177 @@ +use std::error::Error; +use std::fmt::Debug; + +use async_trait::async_trait; +use log::info; +use reqwest::Url; +use serde::{Deserialize, Serialize}; + +use ag_ui_client::agent::{AgentError, AgentStateMutation, RunAgentParams}; +use ag_ui_client::core::AgentState; +use ag_ui_client::core::event::{StateDeltaEvent, StateSnapshotEvent}; +use ag_ui_client::core::types::Message; +use ag_ui_client::subscriber::{AgentSubscriber, AgentSubscriberParams}; +use ag_ui_client::{Agent, HttpAgent}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum StepStatus { + Pending, + Completed, +} + +impl Default for StepStatus { + fn default() -> Self { + StepStatus::Pending + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Step { + pub description: String, + #[serde(default)] + pub status: StepStatus, +} + +impl Step { + pub fn new(description: String) -> Self { + Self { + description, + status: StepStatus::Pending, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Plan { + #[serde(default)] + pub steps: Vec, +} + +impl AgentState for Plan {} + +pub struct GenerativeUiSubscriber; + +impl GenerativeUiSubscriber { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl AgentSubscriber for GenerativeUiSubscriber { + async fn on_state_snapshot_event( + &self, + event: &StateSnapshotEvent, + _params: AgentSubscriberParams<'async_trait, Plan, ()>, + ) -> Result, AgentError> { + info!("State snapshot received:"); + let plan = &event.snapshot; + info!(" Plan with {} steps:", plan.steps.len()); + for (i, step) in plan.steps.iter().enumerate() { + let status_icon = match step.status { + StepStatus::Pending => "[ ]", + StepStatus::Completed => "[X]", + }; + info!(" {}. {} {}", i + 1, status_icon, step.description); + } + Ok(AgentStateMutation::default()) + } + + async fn on_state_delta_event( + &self, + event: &StateDeltaEvent, + _params: AgentSubscriberParams<'async_trait, Plan, ()>, + ) -> Result, AgentError> { + info!("State delta received:"); + for patch in &event.delta { + match patch.get("op").and_then(|v| v.as_str()) { + Some("replace") => { + if let (Some(path), Some(value)) = ( + patch.get("path").and_then(|v| v.as_str()), + patch.get("value"), + ) { + if path.contains("/status") { + let status = value.as_str().unwrap_or("unknown"); + let status_icon = match status { + "completed" => "[X]", + "pending" => "[ ]", + _ => "[?]", + }; + info!(" {} Step status updated to: {}", status_icon, status); + } else if path.contains("/description") { + info!( + " Step description updated to: {}", + value.as_str().unwrap_or("unknown") + ); + } + } + } + Some(op) => info!(" Operation: {}", op), + None => info!(" Unknown operation"), + } + } + Ok(AgentStateMutation::default()) + } + + async fn on_state_changed( + &self, + params: AgentSubscriberParams<'async_trait, Plan, ()>, + ) -> Result<(), AgentError> { + info!("Overall state changed"); + let completed_steps = params + .state + .steps + .iter() + .filter(|step| matches!(step.status, StepStatus::Completed)) + .count(); + info!( + " Progress: {}/{} steps completed", + completed_steps, + params.state.steps.len() + ); + + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::Builder::from_default_env().init(); + + // Base URL for the mock server + // Run the following command to start the mock server: + // `uv run rust/crates/ag-ui-client/scripts/generative_ui.py` + let base_url = Url::parse("http://127.0.0.1:3001/")?; + + // Create the HTTP agent + let agent = HttpAgent::builder().with_url(base_url).build()?; + + let message = Message::new_user( + "I need to organize a birthday party for my friend. Can you help me \ + create a plan? When you have created the plan, please fully execute it.", + ); + + let subscriber = GenerativeUiSubscriber::new(); + + // Create run parameters for testing generative UI with planning + // State & FwdProps types are defined by GenerativeUiSubscriber + let params = RunAgentParams::new_typed().add_message(message); + + info!("Starting generative UI agent run..."); + info!("Testing planning functionality with state snapshots and deltas"); + + let result = agent.run_agent(¶ms, [subscriber]).await?; + + info!("Agent run completed successfully!"); + info!("Final result: {}", result.result); + info!("Generated {} new messages", result.new_messages.len()); + info!("Final state: {:#?}", result.new_state); + + // Print the messages for debugging + for (i, message) in result.new_messages.iter().enumerate() { + info!("Message {}: {:?}", i + 1, message); + } + + Ok(()) +} diff --git a/sdks/community/rust/crates/ag-ui-client/examples/logging_subscriber.rs b/sdks/community/rust/crates/ag-ui-client/examples/logging_subscriber.rs new file mode 100644 index 000000000..a85123b46 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/examples/logging_subscriber.rs @@ -0,0 +1,385 @@ +use ag_ui_client::agent::{AgentError, AgentStateMutation, RunAgentParams}; +use ag_ui_client::core::JsonValue; +use ag_ui_client::core::event::{ + CustomEvent, Event, MessagesSnapshotEvent, RawEvent, RunErrorEvent, RunFinishedEvent, + RunStartedEvent, StateDeltaEvent, StateSnapshotEvent, StepFinishedEvent, StepStartedEvent, + TextMessageChunkEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, + ThinkingEndEvent, ThinkingStartEvent, ThinkingTextMessageContentEvent, + ThinkingTextMessageEndEvent, ThinkingTextMessageStartEvent, ToolCallArgsEvent, + ToolCallChunkEvent, ToolCallEndEvent, ToolCallResultEvent, ToolCallStartEvent, +}; +use ag_ui_client::http::HttpAgent; +use ag_ui_client::subscriber::{AgentSubscriber, AgentSubscriberParams}; +use async_trait::async_trait; +use reqwest::Url; +use std::error::Error; + +// Import our simple subscriber implementation +use ag_ui_client::Agent; +use ag_ui_client::core::types::{Message, ToolCall}; +use ag_ui_client::core::{AgentState, FwdProps}; +use log::info; +use std::collections::HashMap; +use std::fmt::Debug; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // Base URL for the mock server + // Run the following command to start the mock server: + // `uv run rust/crates/ag-ui-client/scripts/basic_agent.py` + let base_url = Url::parse("http://127.0.0.1:3001/")?; + + // Create the HTTP agent + let agent = HttpAgent::builder().with_url(base_url).build()?; + + // Create a simple subscriber + let subscriber = LoggingSubscriber::new(true); + + // Create run parameters + let params = RunAgentParams::new().add_message(Message::new_user( + "Can you give me the current temperature in New York?", + )); + + info!("Running agent with simple subscriber..."); + + // Run the agent with the subscriber + let result = agent.run_agent(¶ms, [subscriber]).await?; + + info!( + "Agent run completed with {} new messages", + result.new_messages.len() + ); + info!("Result: {}", result.result); + + Ok(()) +} + +/// A simple implementation of the AgentSubscriber trait that logs events +pub struct LoggingSubscriber { + pub verbose: bool, +} + +impl LoggingSubscriber { + pub fn new(verbose: bool) -> Self { + Self { verbose } + } + + fn log_event(&self, event_name: &str, event: &T) { + if self.verbose { + info!("Event: {} - {:?}", event_name, event); + } else { + info!("Event: {}", event_name); + } + } +} + +#[async_trait] +impl AgentSubscriber for LoggingSubscriber +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + async fn on_run_initialized( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + info!("Run initialized with {} messages", params.messages.len()); + Ok(AgentStateMutation::default()) + } + + async fn on_run_failed( + &self, + error: &AgentError, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + info!("Run failed: {:?}", error); + Ok(AgentStateMutation::default()) + } + + async fn on_run_finalized( + &self, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + info!("Run finalized"); + Ok(AgentStateMutation::default()) + } + + async fn on_event( + &self, + event: &Event, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("Generic event", &event); + Ok(AgentStateMutation::default()) + } + + async fn on_run_started_event( + &self, + event: &RunStartedEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("RunStarted", event); + Ok(AgentStateMutation::default()) + } + + async fn on_run_finished_event( + &self, + event: &RunFinishedEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("RunFinished", event); + Ok(AgentStateMutation::default()) + } + + async fn on_run_error_event( + &self, + event: &RunErrorEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("RunError", event); + Ok(AgentStateMutation::default()) + } + + async fn on_step_started_event( + &self, + event: &StepStartedEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("StepStarted", event); + Ok(AgentStateMutation::default()) + } + + async fn on_step_finished_event( + &self, + event: &StepFinishedEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("StepFinished", event); + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_start_event( + &self, + event: &TextMessageStartEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("TextMessageStart", event); + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_content_event( + &self, + event: &TextMessageContentEvent, + text_message_buffer: &str, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("TextMessageContent", event); + info!("Current buffer: {}", text_message_buffer); + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_end_event( + &self, + event: &TextMessageEndEvent, + text_message_buffer: &str, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("TextMessageEnd", event); + info!("Final message: {}", text_message_buffer); + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_start_event( + &self, + event: &ToolCallStartEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ToolCallStart", event); + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_args_event( + &self, + event: &ToolCallArgsEvent, + tool_call_buffer: &str, + tool_call_name: &str, + partial_tool_call_args: &HashMap, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ToolCallArgs", event); + info!( + "Tool call: {} with args: {}", + tool_call_name, tool_call_buffer + ); + info!("Partial args: {:?}", partial_tool_call_args); + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_end_event( + &self, + event: &ToolCallEndEvent, + tool_call_name: &str, + tool_call_args: &HashMap, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ToolCallEnd", event); + info!( + "Tool call completed: {} with args: {:?}", + tool_call_name, tool_call_args + ); + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_result_event( + &self, + event: &ToolCallResultEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ToolCallResult", event); + Ok(AgentStateMutation::default()) + } + + async fn on_state_snapshot_event( + &self, + event: &StateSnapshotEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("StateSnapshot", event); + Ok(AgentStateMutation::default()) + } + + async fn on_state_delta_event( + &self, + event: &StateDeltaEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("StateDelta", event); + Ok(AgentStateMutation::default()) + } + + async fn on_messages_snapshot_event( + &self, + event: &MessagesSnapshotEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("MessagesSnapshot", event); + Ok(AgentStateMutation::default()) + } + + async fn on_raw_event( + &self, + event: &RawEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("Raw", event); + Ok(AgentStateMutation::default()) + } + + async fn on_custom_event( + &self, + event: &CustomEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("Custom", event); + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_chunk_event( + &self, + event: &TextMessageChunkEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("TextMessageChunk", event); + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_start_event( + &self, + event: &ThinkingTextMessageStartEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ThinkingTextMessageStart", event); + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_content_event( + &self, + event: &ThinkingTextMessageContentEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ThinkingTextMessageContent", event); + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_end_event( + &self, + event: &ThinkingTextMessageEndEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ThinkingTextMessageEnd", event); + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_chunk_event( + &self, + event: &ToolCallChunkEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ToolCallChunk", event); + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_start_event( + &self, + event: &ThinkingStartEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ThinkingStart", event); + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_end_event( + &self, + event: &ThinkingEndEvent, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + self.log_event("ThinkingEnd", event); + Ok(AgentStateMutation::default()) + } + + async fn on_messages_changed( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + info!("Messages changed: {} messages", params.messages.len()); + Ok(()) + } + + async fn on_state_changed( + &self, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + info!("State changed"); + Ok(()) + } + + async fn on_new_message( + &self, + message: &Message, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + info!("New message: {:?}", message); + Ok(()) + } + + async fn on_new_tool_call( + &self, + tool_call: &ToolCall, + _params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + info!("New tool call: {:?}", tool_call); + Ok(()) + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/examples/shared_state.rs b/sdks/community/rust/crates/ag-ui-client/examples/shared_state.rs new file mode 100644 index 000000000..d1d4bfd20 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/examples/shared_state.rs @@ -0,0 +1,169 @@ +use ag_ui_client::agent::{AgentError, AgentStateMutation, RunAgentParams}; +use ag_ui_client::core::event::{StateDeltaEvent, StateSnapshotEvent}; +use ag_ui_client::core::types::Message; +use ag_ui_client::subscriber::{AgentSubscriber, AgentSubscriberParams}; +use ag_ui_client::{Agent, HttpAgent}; +use async_trait::async_trait; + +use ag_ui_client::core::AgentState; +use log::info; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Hash)] +pub enum SkillLevel { + #[default] + Beginner, + Intermediate, + Advanced, +} + +impl Display for SkillLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub enum SpecialPreferences { + #[serde(rename = "High Protein")] + HighProtein, + #[serde(rename = "Low Carb")] + LowCarb, + Spicy, + #[serde(rename = "Budget-Friendly")] + BudgetFriendly, + #[serde(rename = "One-Pot Meal")] + OnePotMeal, + Vegetarian, + Vegan, +} + +impl Display for SpecialPreferences { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)] +pub enum CookingTime { + #[default] + #[serde(rename = "5 min")] + FiveMin, + #[serde(rename = "15 min")] + FifteenMin, + #[serde(rename = "30 min")] + ThirtyMin, + #[serde(rename = "45 min")] + FortyFiveMin, + #[serde(rename = "60+ min")] + SixtyPlusMin, +} + +impl Display for CookingTime { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +fn default_icon() -> String { + "ingredient".to_string() +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Ingredient { + #[serde(default = "default_icon")] + pub icon: String, + pub name: String, + pub amount: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct Recipe { + #[serde(default)] + pub skill_level: SkillLevel, + #[serde(default)] + pub special_preferences: Vec, + #[serde(default)] + pub cooking_time: CookingTime, + #[serde(default)] + pub ingredients: Vec, + #[serde(default)] + pub instructions: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct RecipeSnapshot { + #[serde(default)] + pub recipe: Recipe, +} + +impl AgentState for RecipeSnapshot {} + +pub struct RecipeSubscriber; + +impl RecipeSubscriber { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl AgentSubscriber for RecipeSubscriber { + async fn on_state_snapshot_event( + &self, + event: &StateSnapshotEvent, + _params: AgentSubscriberParams<'async_trait, RecipeSnapshot, ()>, + ) -> Result, AgentError> { + info!("Received state snapshot update: {:#?}", event.snapshot); + Ok(AgentStateMutation::default()) + } + + async fn on_state_delta_event( + &self, + event: &StateDeltaEvent, + _params: AgentSubscriberParams<'async_trait, RecipeSnapshot, ()>, + ) -> Result, AgentError> { + info!("Received state delta event {:#?}", event.delta); + Ok(AgentStateMutation::default()) + } + + async fn on_state_changed( + &self, + params: AgentSubscriberParams<'async_trait, RecipeSnapshot, ()>, + ) -> Result<(), AgentError> { + info!("Received state changed event: {:?}", params.state); + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::Builder::from_default_env().init(); + + // Base URL for the mock server + // Run the following command to start the mock server: + // `uv run rust/crates/ag-ui-client/scripts/shared_state.py` + let base_url = Url::parse("http://127.0.0.1:3001/")?; + + // Create the HTTP agent + let agent = HttpAgent::builder().with_url(base_url).build()?; + + let subscriber = RecipeSubscriber::new(); + + // Create run parameters + // State & FwdProps types are defined by RecipeSubscriber + let params = RunAgentParams::new_typed().add_message(Message::new_user( + "I want to bake a loaf of bread, can you give me a recipe?", + )); + + info!("Starting agent run with input: {:#?}", params); + + let result = agent.run_agent(¶ms, [subscriber]).await?; + + info!("Agent run finished. Final result: {:#?}", result); + + Ok(()) +} diff --git a/sdks/community/rust/crates/ag-ui-client/examples/sse_example.rs b/sdks/community/rust/crates/ag-ui-client/examples/sse_example.rs new file mode 100644 index 000000000..62180f82e --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/examples/sse_example.rs @@ -0,0 +1,38 @@ +use ag_ui_client::sse::SseResponseExt; +use futures::StreamExt; +use serde::Deserialize; +use std::error::Error; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +enum EventType { + Ping, + Update, + Message, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a client + let client = reqwest::Client::new(); + + // Example 1: Stream with custom event and data types + println!("Example 1: Typed events with custom event and data types"); + let response = client.get("https://httpbun.org/sse").send().await?; + let mut stream = response.event_source().await; + + while let Some(result) = stream.next().await { + match result { + Ok(sse_event) => { + if let Some(event_type) = &sse_event.event { + match event_type.as_str() { + "ping" => println!("Ping: {}", sse_event.data), + &_ => panic!("Unknown event type {event_type}"), + } + } + } + Err(err) => eprintln!("Error: {}", err), + } + } + Ok(()) +} diff --git a/sdks/community/rust/crates/ag-ui-client/scripts/basic_agent.py b/sdks/community/rust/crates/ag-ui-client/scripts/basic_agent.py new file mode 100644 index 000000000..e3ec628b0 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/scripts/basic_agent.py @@ -0,0 +1,36 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "uvicorn == 0.34.3", +# "pydantic-ai==0.4.10" +# ] +# /// + +import uvicorn +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIModel +from pydantic_ai.providers.openai import OpenAIProvider + +model = OpenAIModel( + model_name="gpt-oss:20b", + provider=OpenAIProvider( + base_url="http://localhost:11434/v1", api_key="ollama" + ), +) +agent = Agent(model) + + +@agent.tool_plain +def temperature_celsius(city: str) -> float: + return 21.0 + + +@agent.tool_plain +def temperature_fahrenheit(city: str) -> float: + return 69.8 + + +app = agent.to_ag_ui() + +if __name__ == "__main__": + uvicorn.run(app, port=3001) diff --git a/sdks/community/rust/crates/ag-ui-client/scripts/generative_ui.py b/sdks/community/rust/crates/ag-ui-client/scripts/generative_ui.py new file mode 100644 index 000000000..e417d10e7 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/scripts/generative_ui.py @@ -0,0 +1,140 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "uvicorn == 0.34.3", +# "pydantic-ai==0.4.10" +# ] +# /// + +from __future__ import annotations + +from textwrap import dedent +from typing import Any, Literal + +from pydantic import BaseModel, Field + +from ag_ui.core import EventType, StateDeltaEvent, StateSnapshotEvent +import uvicorn +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIModel +from pydantic_ai.providers.openai import OpenAIProvider +from pydantic_ai.ag_ui import StateDeps + +StepStatus = Literal['pending', 'completed'] + + +class Step(BaseModel): + """Represents a step in a plan.""" + + description: str = Field(description='The description of the step') + status: StepStatus = Field( + default='pending', + description='The status of the step (e.g., pending, completed)', + ) + + +class Plan(BaseModel): + """Represents a plan with multiple steps.""" + + steps: list[Step] = Field(default_factory=list, description='The steps in the plan') + + +class JSONPatchOp(BaseModel): + """A class representing a JSON Patch operation (RFC 6902).""" + + op: Literal['add', 'remove', 'replace', 'move', 'copy', 'test'] = Field( + description='The operation to perform: add, remove, replace, move, copy, or test', + ) + path: str = Field(description='JSON Pointer (RFC 6901) to the target location') + value: Any = Field( + default=None, + description='The value to apply (for add, replace operations)', + ) + from_: str | None = Field( + default=None, + alias='from', + description='Source path (for move, copy operations)', + ) + +model = OpenAIModel( + model_name="gpt-oss:20b", + provider=OpenAIProvider( + base_url="http://localhost:11434/v1", api_key="ollama", + ), + +) +agent = Agent( + model=model, + instructions=dedent( + """ + When planning use tools only, without any other messages. + IMPORTANT: + - Use the `create_plan` tool to set the initial state of the steps + - Use the `update_plan_step` tool to update the status of each step + - Do NOT repeat the plan or summarise it in a message + - Do NOT confirm the creation or updates in a message + - Do NOT ask the user for additional information or next steps + + Only one plan can be active at a time, so do not call the `create_plan` tool + again until all the steps in current plan are completed. + """ + ), + retries=3 +) + + +@agent.tool_plain +async def create_plan(steps: list[str]) -> StateSnapshotEvent: + """Create a plan with multiple steps. + + Args: + steps: List of step descriptions to create the plan. + + Returns: + StateSnapshotEvent containing the initial state of the steps. + """ + plan: Plan = Plan( + steps=[Step(description=step) for step in steps], + ) + return StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot=plan.model_dump(), + ) + + +@agent.tool_plain +async def update_plan_step( + index: int, description: str | None = None, status: StepStatus | None = None +) -> StateDeltaEvent: + """Update the plan with new steps or changes. + + Args: + index: The index of the step to update. + description: The new description for the step. + status: The new status for the step. + + Returns: + StateDeltaEvent containing the changes made to the plan. + """ + changes: list[JSONPatchOp] = [] + if description is not None: + changes.append( + JSONPatchOp( + op='replace', path=f'/steps/{index}/description', value=description + ) + ) + if status is not None: + changes.append( + JSONPatchOp(op='replace', path=f'/steps/{index}/status', value=status) + ) + return StateDeltaEvent( + type=EventType.STATE_DELTA, + delta=changes, + ) + + +app = agent.to_ag_ui(deps=StateDeps(Plan())) + + +if __name__ == "__main__": + uvicorn.run(app, port=3001) diff --git a/sdks/community/rust/crates/ag-ui-client/scripts/shared_state.py b/sdks/community/rust/crates/ag-ui-client/scripts/shared_state.py new file mode 100644 index 000000000..a49a2f96f --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/scripts/shared_state.py @@ -0,0 +1,156 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "uvicorn == 0.34.3", +# "pydantic-ai==0.4.10" +# ] +# /// + +from __future__ import annotations + +from enum import StrEnum +from textwrap import dedent + +from pydantic import BaseModel, Field + +from ag_ui.core import EventType, StateSnapshotEvent +from pydantic_ai import Agent, RunContext +from pydantic_ai.ag_ui import StateDeps +from pydantic_ai.models.openai import OpenAIModel +from pydantic_ai.providers.openai import OpenAIProvider +import uvicorn + + +class SkillLevel(StrEnum): + """The level of skill required for the recipe.""" + + BEGINNER = 'Beginner' + INTERMEDIATE = 'Intermediate' + ADVANCED = 'Advanced' + + +class SpecialPreferences(StrEnum): + """Special preferences for the recipe.""" + + HIGH_PROTEIN = 'High Protein' + LOW_CARB = 'Low Carb' + SPICY = 'Spicy' + BUDGET_FRIENDLY = 'Budget-Friendly' + ONE_POT_MEAL = 'One-Pot Meal' + VEGETARIAN = 'Vegetarian' + VEGAN = 'Vegan' + + +class CookingTime(StrEnum): + """The cooking time of the recipe.""" + + FIVE_MIN = '5 min' + FIFTEEN_MIN = '15 min' + THIRTY_MIN = '30 min' + FORTY_FIVE_MIN = '45 min' + SIXTY_PLUS_MIN = '60+ min' + + +class Ingredient(BaseModel): + """A class representing an ingredient in a recipe.""" + + icon: str = Field( + default='ingredient', + description="The icon emoji (not emoji code like '\x1f35e', but the actual emoji like 🥕) of the ingredient", + ) + name: str + amount: str + + +class Recipe(BaseModel): + """A class representing a recipe.""" + + skill_level: SkillLevel = Field( + default=SkillLevel.BEGINNER, + description='The skill level required for the recipe', + ) + special_preferences: list[SpecialPreferences] = Field( + default_factory=list, + description='Any special preferences for the recipe', + ) + cooking_time: CookingTime = Field( + default=CookingTime.FIVE_MIN, description='The cooking time of the recipe' + ) + ingredients: list[Ingredient] = Field( + default_factory=list, + description='Ingredients for the recipe', + ) + instructions: list[str] = Field( + default_factory=list, description='Instructions for the recipe' + ) + + +class RecipeSnapshot(BaseModel): + """A class representing the state of the recipe.""" + + recipe: Recipe = Field( + default_factory=Recipe, description='The current state of the recipe' + ) + +model = OpenAIModel( + model_name="gpt-oss:20b", + provider=OpenAIProvider( + base_url="http://localhost:11434/v1", api_key="ollama" + ), +) +agent = Agent(model=model, deps_type=StateDeps[RecipeSnapshot]) + + +@agent.tool_plain +async def display_recipe(recipe: Recipe) -> StateSnapshotEvent: + """Display the recipe to the user. + + Args: + recipe: The recipe to display. + + Returns: + StateSnapshotEvent containing the recipe snapshot. + """ + return StateSnapshotEvent( + type=EventType.STATE_SNAPSHOT, + snapshot={'recipe': recipe}, + ) + + +@agent.instructions +async def recipe_instructions(ctx: RunContext[StateDeps[RecipeSnapshot]]) -> str: + """Instructions for the recipe generation agent. + + Args: + ctx: The run context containing recipe state information. + + Returns: + Instructions string for the recipe generation agent. + """ + return dedent( + f""" + You are a helpful assistant for creating recipes. + + IMPORTANT: + - Create a complete recipe using the existing ingredients + - Append new ingredients to the existing ones + - Use the `display_recipe` tool to present the recipe to the user + - Do NOT repeat the recipe in the message, use the tool instead + - Do NOT run the `display_recipe` tool multiple times in a row + + Once you have created the updated recipe and displayed it to the user, + summarise the changes in one sentence, don't describe the recipe in + detail or send it as a message to the user. + + The current state of the recipe is: + + {ctx.deps.state.recipe.model_dump_json(indent=2)} + """, + ) + + +app = agent.to_ag_ui(deps=StateDeps(RecipeSnapshot())) + + +if __name__ == "__main__": + uvicorn.run(app, port=3001) diff --git a/sdks/community/rust/crates/ag-ui-client/src/agent.rs b/sdks/community/rust/crates/ag-ui-client/src/agent.rs new file mode 100644 index 000000000..cd5475ae6 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/agent.rs @@ -0,0 +1,225 @@ +use futures::stream::StreamExt; +use std::collections::HashSet; +use thiserror::Error; + +use crate::core::JsonValue; +use crate::core::types::{ + AgentId, Context, Message, MessageId, RunAgentInput, RunId, ThreadId, Tool, +}; +use crate::core::{AgentState, FwdProps}; +use crate::event_handler::EventHandler; +use crate::stream::EventStream; +use crate::subscriber::IntoSubscribers; + +#[derive(Debug, Clone)] +pub struct AgentConfig { + pub agent_id: Option, + pub description: Option, + pub thread_id: Option, + pub initial_messages: Option>, + pub initial_state: Option, + pub debug: Option, +} + +impl Default for AgentConfig +where + S: Default, +{ + fn default() -> Self { + Self { + agent_id: None, + description: None, + thread_id: None, + initial_messages: None, + initial_state: None, + debug: None, + } + } +} + +/// Parameters for running an agent. +#[derive(Debug, Clone, Default)] +pub struct RunAgentParams { + pub run_id: Option, + pub tools: Vec, + pub context: Vec, + pub forwarded_props: FwdPropsT, + pub messages: Vec, + pub state: StateT, +} + +impl + RunAgentParams +{ + pub fn new_typed() -> Self { + Self { + run_id: None, + tools: Vec::new(), + context: Vec::new(), + forwarded_props: FwdPropsT::default(), + messages: Vec::new(), + state: StateT::default(), + } + } + + pub fn with_run_id(mut self, run_id: RunId) -> Self { + self.run_id = Some(run_id); + self + } + pub fn add_tool(mut self, tool: Tool) -> Self { + self.tools.push(tool); + self + } + pub fn add_context(mut self, ctx: Context) -> Self { + self.context.push(ctx); + self + } + pub fn with_forwarded_props(mut self, props: FwdPropsT) -> Self { + self.forwarded_props = props; + self + } + pub fn with_state(mut self, state: StateT) -> Self { + self.state = state; + self + } + pub fn add_message(mut self, msg: Message) -> Self { + self.messages.push(msg); + self + } + pub fn user(mut self, content: impl Into) -> Self { + self.messages.push(Message::User { + id: MessageId::random(), + content: content.into(), + name: None, + }); + self + } +} + +impl RunAgentParams { + /// Construct an empty parameter object with JSON Values for state and forwarded props. + /// + /// If you want typed state and/or fwd props, use [Self::new_typed] + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, Clone)] +pub struct RunAgentResult { + pub result: JsonValue, + pub new_messages: Vec, + pub new_state: StateT, +} + +pub type AgentRunState = RunAgentInput; + +#[derive(Debug, Clone)] +pub struct AgentStateMutation { + pub messages: Option>, + pub state: Option, + pub stop_propagation: bool, +} + +impl Default for AgentStateMutation { + fn default() -> Self { + Self { + messages: None, + state: None, + stop_propagation: false, + } + } +} + +// Error types +#[derive(Error, Debug)] +pub enum AgentError { + #[error("Agent execution failed: {message}")] + ExecutionError { message: String }, + #[error("Invalid configuration: {message}")] + ConfigError { message: String }, + #[error("Serialization error: {source}")] + SerializationError { + #[from] + source: serde_json::Error, + }, +} + +// TODO: Expand documentation +/// Agent trait +#[async_trait::async_trait] +pub trait Agent: Send + Sync +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + async fn run( + &self, + input: &RunAgentInput, + ) -> Result, AgentError>; + + // TODO: Expand documentation + /// The main execution method, containing the full pipeline logic. + async fn run_agent( + &self, + params: &RunAgentParams, + subscribers: impl IntoSubscribers, + ) -> Result, AgentError> { + // TODO: Use Agent ID? + let _agent_id = AgentId::random(); + + let input = RunAgentInput { + thread_id: ThreadId::random(), + run_id: params.run_id.clone().unwrap_or_else(RunId::random), + state: params.state.clone(), + messages: params.messages.clone(), + tools: params.tools.clone(), + context: params.context.clone(), + // TODO: Find suitable default value + forwarded_props: params.forwarded_props.clone(), + }; + let current_message_ids: HashSet<&MessageId> = + params.messages.iter().map(|m| m.id()).collect(); + + // Initialize event handler with the current state + let subscribers = subscribers.into_subscribers(); + let mut event_handler = EventHandler::new( + params.messages.clone(), + params.state.clone(), + &input, + subscribers, + ); + + let mut stream = self.run(&input).await?.fuse(); + + while let Some(event_result) = stream.next().await { + match event_result { + Ok(event) => { + let mutation = event_handler.handle_event(&event).await?; + event_handler.apply_mutation(mutation).await?; + } + Err(e) => { + event_handler.on_error(&e).await?; + return Err(e); + } + } + } + + // Finalize the run + event_handler.on_finalize().await?; + + // Collect new messages + let new_messages = event_handler + .messages + .iter() + .filter(|m| !current_message_ids.contains(&m.id())) + .cloned() + .collect(); + + Ok(RunAgentResult { + result: event_handler.result, + new_messages, + new_state: event_handler.state, + }) + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/src/event_handler.rs b/sdks/community/rust/crates/ag-ui-client/src/event_handler.rs new file mode 100644 index 000000000..55f5bedc6 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/event_handler.rs @@ -0,0 +1,543 @@ +use crate::agent::{AgentError, AgentStateMutation}; +use crate::core::event::Event; +use crate::core::types::{FunctionCall, Message, MessageId, Role, RunAgentInput, ToolCall}; +use crate::core::{AgentState, FwdProps, JsonValue}; +use crate::subscriber::{AgentSubscriberParams, Subscribers}; +use json_patch::PatchOperation; +use log::error; +use std::collections::{HashMap, HashSet}; + +/// Captures the run state and handles events +#[derive(Clone)] +pub(crate) struct EventHandler<'a, StateT, FwdPropsT> +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + pub messages: Vec, + pub state: StateT, + pub input: &'a RunAgentInput, + pub subscribers: Subscribers, + pub result: JsonValue, +} + +impl<'a, StateT, FwdPropsT> EventHandler<'a, StateT, FwdPropsT> +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + pub fn new( + messages: Vec, + state: StateT, + input: &'a RunAgentInput, + subscribers: Subscribers, + ) -> Self { + Self { + messages, + state, + input, + subscribers, + result: JsonValue::Null, + } + } + + fn to_subscriber_params(&'a self) -> AgentSubscriberParams<'a, StateT, FwdPropsT> { + AgentSubscriberParams { + messages: &self.messages, + state: &self.state, + input: self.input, + } + } + + // Helper method to directly update state and messages without using apply_mutation + fn update_from_mutation(&mut self, mutation: &AgentStateMutation) { + if let Some(messages) = &mutation.messages { + self.messages = messages.clone(); + } + if let Some(state) = &mutation.state { + self.state = state.clone(); + } + } + + // Helper method to process a subscriber's mutation + fn process_mutation( + &mut self, + mutation: AgentStateMutation, + current_mutation: &mut AgentStateMutation, + ) { + // Apply any mutations + if mutation.messages.is_some() || mutation.state.is_some() { + // Update directly without using apply_mutation + self.update_from_mutation(&mutation); + + // Update current_mutation with the applied changes + if mutation.messages.is_some() { + current_mutation.messages = mutation.messages; + } + if mutation.state.is_some() { + current_mutation.state = mutation.state; + } + } + } + + pub async fn handle_event( + &mut self, + event: &Event, + ) -> Result, AgentError> { + let mut current_mutation = AgentStateMutation::default(); + let mut mutations = Vec::new(); + + // Clone subscribers to avoid borrowing issues + for subscriber in &self.subscribers.clone() { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_event(event, params).await?; + mutations.push(mutation); + } + + // Then handle specific event types + match event { + Event::TextMessageStart(e) => { + // Default behavior + let new_message = Message::Assistant { + id: e.message_id.clone(), + content: Some(String::new()), + name: None, + tool_calls: None, + }; + self.messages.push(new_message); + current_mutation.messages = Some(self.messages.clone()); + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_text_message_start_event(e, params).await?; + mutations.push(mutation); + } + } + Event::TextMessageContent(e) => { + // Default behavior + if let Some(last_message) = self.messages.last_mut() { + let content = last_message.content_mut(); + if let Some(s) = content { + s.push_str(&e.delta) + } + current_mutation.messages = Some(self.messages.clone()); + } + + // Get the current text message buffer + let text_message_buffer = self + .messages + .last() + .and_then(|m| m.content()) + .unwrap_or_default() + .to_string(); // Clone to avoid borrowing issues + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_text_message_content_event(e, &text_message_buffer, params) + .await?; + mutations.push(mutation); + } + } + Event::TextMessageEnd(e) => { + // Get the current text message buffer + let text_message_buffer = self + .messages + .last() + .and_then(|m| m.content()) + .unwrap_or_default() + .to_string(); // Clone to avoid borrowing issues + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_text_message_end_event(e, &text_message_buffer, params) + .await?; + mutations.push(mutation); + } + } + Event::TextMessageChunk(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_text_message_chunk_event(e, params).await?; + mutations.push(mutation); + } + } + Event::ThinkingTextMessageStart(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_thinking_text_message_start_event(e, params) + .await?; + mutations.push(mutation); + } + } + Event::ThinkingTextMessageContent(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_thinking_text_message_content_event(e, params) + .await?; + mutations.push(mutation); + } + } + Event::ThinkingTextMessageEnd(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_thinking_text_message_end_event(e, params) + .await?; + mutations.push(mutation); + } + } + Event::ToolCallStart(e) => { + // Default behavior + let new_tool_call = ToolCall { + id: e.tool_call_id.clone(), + call_type: "function".to_string(), + function: FunctionCall { + name: e.tool_call_name.clone(), + arguments: String::new(), + }, + }; + + if let Some(last_message) = self.messages.last_mut() { + if Some(last_message.id()) == e.parent_message_id.clone().as_ref() { + let _ = last_message.tool_calls_mut().get_or_insert(&mut Vec::new()); + + let _ = last_message + .tool_calls_mut() + .map(|tc| tc.push(new_tool_call)); + } + } else { + let new_message = Message::Assistant { + id: e + .parent_message_id + .clone() + .unwrap_or_else(MessageId::random), + content: None, + name: None, + tool_calls: None, + }; + self.messages.push(new_message); + } + current_mutation.messages = Some(self.messages.clone()); + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_tool_call_start_event(e, params).await?; + mutations.push(mutation); + } + } + Event::ToolCallArgs(e) => { + // Default behavior + if let Some(last_message) = self.messages.last_mut() { + if let Some(tool_calls) = last_message.tool_calls_mut() { + if let Some(last_tool_call) = tool_calls.last_mut() { + last_tool_call.function.arguments.push_str(&e.delta); + current_mutation.messages = Some(self.messages.clone()); + } + } + } + + // Get the current tool call buffer and name + let (tool_call_buffer, tool_call_name, partial_args) = if let Some(last_message) = + self.messages.last() + { + if let Some(tool_calls) = last_message.tool_calls() { + if let Some(last_tool_call) = tool_calls.last() { + // Try to parse the arguments as JSON to get partial args + let partial_args = serde_json::from_str::>( + &last_tool_call.function.arguments, + ) + .unwrap_or_default(); + ( + last_tool_call.function.arguments.clone(), + last_tool_call.function.name.clone(), + partial_args, + ) + } else { + (String::new(), String::new(), HashMap::new()) + } + } else { + (String::new(), String::new(), HashMap::new()) + } + } else { + (String::new(), String::new(), HashMap::new()) + }; + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_tool_call_args_event( + e, + &tool_call_buffer, + &tool_call_name, + &partial_args, + params, + ) + .await?; + mutations.push(mutation); + } + } + Event::ToolCallEnd(e) => { + // Get the current tool call buffer and name + let (tool_call_name, tool_call_args) = + if let Some(last_message) = self.messages.last() { + if let Some(tool_calls) = last_message.tool_calls() { + if let Some(last_tool_call) = tool_calls.last() { + // Try to parse the arguments as JSON + let args = serde_json::from_str::>( + &last_tool_call.function.arguments, + ) + .unwrap_or_default(); + (last_tool_call.function.name.clone(), args) + } else { + (String::new(), HashMap::new()) + } + } else { + (String::new(), HashMap::new()) + } + } else { + (String::new(), HashMap::new()) + }; + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber + .on_tool_call_end_event(e, &tool_call_name, &tool_call_args, params) + .await?; + mutations.push(mutation); + } + } + Event::ToolCallChunk(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_tool_call_chunk_event(e, params).await?; + mutations.push(mutation); + } + } + Event::ToolCallResult(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_tool_call_result_event(e, params).await?; + mutations.push(mutation); + } + } + Event::ThinkingStart(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_thinking_start_event(e, params).await?; + mutations.push(mutation); + } + } + Event::ThinkingEnd(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_thinking_end_event(e, params).await?; + mutations.push(mutation); + } + } + Event::StateSnapshot(e) => { + // Default behavior + self.state = e.snapshot.clone(); + current_mutation.state = Some(self.state.clone()); + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_state_snapshot_event(e, params).await?; + mutations.push(mutation); + } + } + Event::StateDelta(e) => { + // Default behavior + let mut state_val = serde_json::to_value(&self.state)?; + + // TODO: This cast to and from JsonValue seems unnecessary + let patches: Vec = + serde_json::from_value(serde_json::to_value(e.delta.clone())?)?; + + json_patch::patch(&mut state_val, &patches).map_err(|err| { + AgentError::ExecutionError { + message: format!("Failed to apply state patch: {err}"), + } + })?; + let new_state: StateT = serde_json::from_value(state_val)?; + self.state = new_state; + current_mutation.state = Some(self.state.clone()); + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_state_delta_event(e, params).await?; + mutations.push(mutation); + } + } + Event::MessagesSnapshot(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_messages_snapshot_event(e, params).await?; + mutations.push(mutation); + } + } + Event::Raw(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_raw_event(e, params).await?; + mutations.push(mutation); + } + } + Event::Custom(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_custom_event(e, params).await?; + mutations.push(mutation); + } + } + Event::RunStarted(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_run_started_event(e, params).await?; + mutations.push(mutation); + } + } + Event::RunFinished(e) => { + // Default behavior + self.result = e.result.clone().unwrap_or(JsonValue::Null); + + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_run_finished_event(e, params).await?; + mutations.push(mutation); + } + } + Event::RunError(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_run_error_event(e, params).await?; + mutations.push(mutation); + } + } + Event::StepStarted(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_step_started_event(e, params).await?; + mutations.push(mutation); + } + } + Event::StepFinished(e) => { + for subscriber in &self.subscribers { + let params = self.to_subscriber_params(); + let mutation = subscriber.on_step_finished_event(e, params).await?; + mutations.push(mutation); + } + } + } + + for mutation in mutations { + if mutation.stop_propagation { + self.update_from_mutation(&mutation); + return Ok(mutation); + } else { + self.process_mutation(mutation, &mut current_mutation); + } + } + + Ok(current_mutation) + } + + pub async fn apply_mutation( + &mut self, + mutation: AgentStateMutation, + ) -> Result<(), AgentError> { + if let Some(messages) = mutation.messages { + // Check for new messages to notify about + let old_message_ids: HashSet<&MessageId> = + self.messages.iter().map(|m| m.id()).collect(); + + let new_messages: Vec<&Message> = messages + .iter() + .filter(|m| !old_message_ids.contains(m.id())) + .collect(); + + // Set the new messages first + self.messages = messages.clone(); + + // Notify about new messages + for message in new_messages { + self.notify_new_message(message).await?; + + // If the message is from assistant and has tool calls, notify about those too + if message.role() == Role::Assistant && message.tool_calls().is_some() { + for tool_call in message.tool_calls().unwrap() { + self.notify_new_tool_call(tool_call).await?; + } + } + } + + // Then notify about messages changed + self.notify_messages_changed().await?; + } + + if let Some(state) = mutation.state { + self.state = state; + self.notify_state_changed().await?; + } + + Ok(()) + } + + async fn notify_new_message(&self, message: &Message) -> Result<(), AgentError> { + for subscriber in &self.subscribers { + subscriber + .on_new_message(message, self.to_subscriber_params()) + .await?; + } + Ok(()) + } + + async fn notify_new_tool_call(&self, tool_call: &ToolCall) -> Result<(), AgentError> { + for subscriber in &self.subscribers { + subscriber + .on_new_tool_call(tool_call, self.to_subscriber_params()) + .await?; + } + Ok(()) + } + + async fn notify_messages_changed(&self) -> Result<(), AgentError> { + for subscriber in &self.subscribers { + subscriber + .on_messages_changed(self.to_subscriber_params()) + .await?; + } + Ok(()) + } + + async fn notify_state_changed(&self) -> Result<(), AgentError> { + for subscriber in &self.subscribers { + subscriber + .on_state_changed(self.to_subscriber_params()) + .await?; + } + Ok(()) + } + + pub async fn on_error(&self, error: &AgentError) -> Result<(), AgentError> { + error!("Agent error: {error}"); + for subscriber in &self.subscribers { + let _mutation = subscriber + .on_run_failed(error, self.to_subscriber_params()) + .await?; + } + Ok(()) + } + + pub async fn on_finalize(&self) -> Result<(), AgentError> { + for subscriber in &self.subscribers { + let _mutation = subscriber + .on_run_finalized(self.to_subscriber_params()) + .await?; + } + Ok(()) + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/src/http.rs b/sdks/community/rust/crates/ag-ui-client/src/http.rs new file mode 100644 index 000000000..ae9bda189 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/http.rs @@ -0,0 +1,189 @@ +use async_trait::async_trait; +use futures::StreamExt; +use log::{debug, trace}; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::{Client as HttpClient, Url}; +use std::str::FromStr; + +use crate::Agent; +use crate::agent::AgentError; +use crate::agent::AgentError::SerializationError; +use crate::core::event::Event; +use crate::core::types::RunAgentInput; +use crate::core::{AgentState, FwdProps}; +use crate::sse::SseResponseExt; +use crate::stream::EventStream; + +pub struct HttpAgent { + http_client: HttpClient, + base_url: Url, + header_map: HeaderMap, +} + +impl HttpAgent { + pub fn new(base_url: Url, header_map: HeaderMap) -> Self { + let http_client = HttpClient::new(); + let mut header_map: HeaderMap = header_map; + + header_map.insert("Content-Type", HeaderValue::from_static("application/json")); + Self { + http_client, + base_url, + header_map, + } + } + + pub fn builder() -> HttpAgentBuilder { + HttpAgentBuilder::new() + } +} + +pub struct HttpAgentBuilder { + base_url: Option, + header_map: HeaderMap, + http_client: Option, +} + +impl HttpAgentBuilder { + pub fn new() -> Self { + Self { + base_url: None, + header_map: HeaderMap::new(), + http_client: None, + } + } + + /// Set the base URL from a Url instance + pub fn with_url(mut self, base_url: Url) -> Self { + self.base_url = Some(base_url); + self + } + + /// Set the base URL from a string, returning Result for validation + pub fn with_url_str(mut self, url: &str) -> Result { + let parsed_url = Url::parse(url).map_err(|e| AgentError::ConfigError { + message: format!("Invalid URL '{url}': {e}"), + })?; + self.base_url = Some(parsed_url); + Ok(self) + } + + /// Replace all headers with the provided HeaderMap + pub fn with_headers(mut self, header_map: HeaderMap) -> Self { + self.header_map = header_map; + self + } + + /// Add a single header by name and value strings + pub fn with_header(mut self, name: &str, value: &str) -> Result { + let header_name = HeaderName::from_str(name).map_err(|e| AgentError::ConfigError { + message: format!("Invalid header name '{value}': {e}"), + })?; + let header_value = HeaderValue::from_str(value).map_err(|e| AgentError::ConfigError { + message: format!("Invalid header value '{value}': {e}"), + })?; + self.header_map.insert(header_name, header_value); + Ok(self) + } + + /// Add a header using HeaderName and HeaderValue directly + pub fn with_header_typed(mut self, name: HeaderName, value: HeaderValue) -> Self { + self.header_map.insert(name, value); + self + } + + /// Add an authorization bearer token + pub fn with_bearer_token(self, token: &str) -> Result { + let auth_value = format!("Bearer {token}"); + self.with_header("Authorization", &auth_value) + } + + /// Set a custom HTTP client + pub fn with_http_client(mut self, client: HttpClient) -> Self { + self.http_client = Some(client); + self + } + + /// Set request timeout in seconds + pub fn with_timeout(mut self, timeout_secs: u64) -> Self { + let client = HttpClient::builder() + .timeout(std::time::Duration::from_secs(timeout_secs)) + .build() + .unwrap_or_else(|_| HttpClient::new()); + self.http_client = Some(client); + self + } + + pub fn build(self) -> Result { + let base_url = self.base_url.ok_or(AgentError::ConfigError { + message: "Base URL is required".to_string(), + })?; + + // Validate URL scheme + if !["http", "https"].contains(&base_url.scheme()) { + return Err(AgentError::ConfigError { + message: format!("Unsupported URL scheme: {}", base_url.scheme()), + }); + } + + let http_client = self.http_client.unwrap_or_default(); + + Ok(HttpAgent { + http_client, + base_url, + header_map: self.header_map, + }) + } +} + +impl Default for HttpAgentBuilder { + fn default() -> Self { + Self::new() + } +} + +impl From for AgentError { + fn from(err: reqwest::Error) -> Self { + AgentError::ExecutionError { + message: err.to_string(), + } + } +} + +#[async_trait] +impl Agent for HttpAgent { + async fn run( + &self, + input: &RunAgentInput, + ) -> Result, AgentError> { + // Send the request and get the response + let response = self + .http_client + .post(self.base_url.clone()) + .json(input) + .headers(self.header_map.clone()) + .send() + .await?; + + // Convert the response to an SSE event stream + let stream = response + .event_source() + .await + .map(|result| match result { + Ok(event) => { + trace!("Received event: {event:?}"); + + let event_data: Event = serde_json::from_str(&event.data) + .map_err(|err| SerializationError { source: err })?; + debug!("Deserialized event: {event_data:?}"); + + Ok(event_data) + } + Err(err) => Err(AgentError::ExecutionError { + message: err.to_string(), + }), + }) + .boxed(); + Ok(stream) + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/src/lib.rs b/sdks/community/rust/crates/ag-ui-client/src/lib.rs new file mode 100644 index 000000000..95fd8b4fa --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/lib.rs @@ -0,0 +1,10 @@ +pub mod agent; +pub mod event_handler; +pub mod http; +pub mod sse; +pub(crate) mod stream; +pub mod subscriber; +pub use agent::{Agent, RunAgentParams}; +pub use http::HttpAgent; + +pub use ag_ui_core as core; diff --git a/sdks/community/rust/crates/ag-ui-client/src/sse.rs b/sdks/community/rust/crates/ag-ui-client/src/sse.rs new file mode 100644 index 000000000..e9f322abc --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/sse.rs @@ -0,0 +1,349 @@ +use bytes::Bytes; +use futures::{Stream, StreamExt}; +use reqwest::Response; +use std::future::Future; +use thiserror::Error; + +/// Error type for SSE response processing +#[derive(Error, Debug)] +pub enum SseError { + /// Error when parsing SSE data + #[error("Failed to parse SSE data: {0}")] + ParseError(String), + + /// Error from reqwest + #[error("HTTP error: {0}")] + HttpError(#[from] reqwest::Error), + + /// Error when deserializing JSON + #[error("JSON deserialization error: {0}")] + JsonError(#[from] serde_json::Error), +} + +/// Represents a parsed Server-Sent Event +#[derive(Debug)] +pub struct SseEvent { + /// The event type (from the "event:" field) + pub event: Option, + + /// The event ID (from the "id:" field) + pub id: Option, + + /// The event data (from the "data:" field) + pub data: String, +} + +/// Extension trait for processing Server-Sent Events (SSE) responses from reqwest::Response +/// +/// This trait provides methods to process SSE responses as a stream of events with customizable +/// type parameters for event type, data, and id fields. +/// +/// # SSE Format +/// +/// Server-Sent Events typically follow this format: +/// ```text +/// event: ping +/// id: 1 +/// data: {"message": "hello"} +/// +/// event: update +/// id: 2 +/// data: {"id": 123, "status": "ok"} +/// ``` +/// +/// Where: +/// - `event`: Optional field specifying the event type +/// - `id`: Optional field providing an event identifier +/// - `data`: The event payload, often JSON data +/// +/// Events are separated by double newlines (`\n\n`). +pub trait SseResponseExt { + /// Converts a reqwest::Response into a Stream of SSE events + fn event_source( + self, + ) -> impl Future>> + Send; +} + +impl SseResponseExt for Response { + #[allow(clippy::manual_async_fn)] + fn event_source( + self, + ) -> impl Future>> + Send { + async move { + // Create a stream of bytes from the response + let stream = self.bytes_stream(); + + // Process the stream with type conversions + SseEventProcessor::new(stream) + } + } +} + +/// A processor that converts a byte stream into an SSE event stream +struct SseEventProcessor; + +impl SseEventProcessor { + /// Creates a new SSE event processor + #[allow(clippy::new_ret_no_self)] + fn new( + stream: impl Stream> + 'static, + ) -> impl Stream> { + let mut buffer = String::new(); + + // Process the stream + stream + .map(move |chunk_result| { + // Map reqwest errors + let chunk = match chunk_result { + Ok(chunk) => chunk, + Err(err) => return vec![Err(SseError::HttpError(err))], + }; + + // Convert bytes to string and append to buffer + match String::from_utf8(chunk.to_vec()) { + Ok(text) => { + buffer.push_str(&text); + + // Process complete events from the buffer + let (events, new_buffer) = process_raw_sse_events(&buffer); + buffer = new_buffer; + + events + } + Err(e) => vec![Err(SseError::ParseError(format!("Invalid UTF-8: {e}")))], + } + }) + .flat_map(futures::stream::iter) + } +} + +/// Process SSE data from a buffer string into raw SSE events +/// +/// Returns a tuple of (events, new_buffer) where: +/// - events: A vector of parsed events or errors +/// - new_buffer: The remaining buffer that might contain incomplete events +fn process_raw_sse_events(buffer: &str) -> (Vec>, String) { + let mut results = Vec::new(); + let chunks: Vec<&str> = buffer.split("\n\n").collect(); + + // If there's only one chunk and it doesn't end with a double newline, + // it might be incomplete - keep it in the buffer + if chunks.len() == 1 && !buffer.ends_with("\n\n") { + return (Vec::new(), buffer.to_string()); + } + + let complete_chunks = if buffer.ends_with("\n\n") { + // All chunks are complete + &chunks[..] + } else { + // Last chunk might be incomplete + &chunks[..chunks.len() - 1] + }; + + // Process all complete events + for chunk in complete_chunks { + if !chunk.is_empty() { + results.push(parse_sse_event(chunk)); + } + } + + // If the buffer doesn't end with a double newline and we have chunks, + // the last chunk is incomplete - keep it in the buffer + let new_buffer = if !buffer.ends_with("\n\n") && !chunks.is_empty() { + chunks.last().unwrap().to_string() + } else { + String::new() + }; + + (results, new_buffer) +} + +/// Parse a single SSE event text into an SseEvent +fn parse_sse_event(event_text: &str) -> Result { + let mut event = None; + let mut id = None; + let mut data_lines = Vec::new(); + + for line in event_text.lines() { + if line.is_empty() { + continue; + } + + if let Some(value) = line.strip_prefix("event:") { + event = Some(value.trim().to_string()); + } else if let Some(value) = line.strip_prefix("id:") { + id = Some(value.trim().to_string()); + } else if let Some(value) = line.strip_prefix("data:") { + // For data lines, trim a leading space if present + let data_content = value.strip_prefix(" ").unwrap_or(value); + data_lines.push(data_content); + } + // Ignore other fields like "retry:" + } + + // Join all data lines with newlines + let data = data_lines.join("\n"); + + Ok(SseEvent { event, id, data }) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Deserialize, Debug, PartialEq)] + struct TestEvent { + event_type: String, + data: String, + } + + #[tokio::test] + async fn test_process_raw_sse_events() { + // Test with a single complete event + let buffer = "data: {\"event_type\":\"test\",\"data\":\"hello\"}\n\n"; + let (events, new_buffer) = process_raw_sse_events(buffer); + assert_eq!(events.len(), 1); + assert_eq!(new_buffer, ""); + let event = events[0].as_ref().unwrap(); + assert_eq!(event.data, "{\"event_type\":\"test\",\"data\":\"hello\"}"); + + // Test with multiple events + let buffer = "data: {\"event_type\":\"test1\",\"data\":\"hello1\"}\n\n\ + data: {\"event_type\":\"test2\",\"data\":\"hello2\"}\n\n"; + let (events, new_buffer) = process_raw_sse_events(buffer); + assert_eq!(events.len(), 2); + assert_eq!(new_buffer, ""); + + // Test with incomplete event + let buffer = "data: {\"event_type\":\"test\",\"data\":\"hello\"}"; + let (events, new_buffer) = process_raw_sse_events(buffer); + assert_eq!(events.len(), 0); + assert_eq!(new_buffer, buffer); + + // Test with complete and incomplete events + let buffer = "data: {\"event_type\":\"test1\",\"data\":\"hello1\"}\n\n\ + data: {\"event_type\":\"test2\",\"data\":\"hello2\"}"; + let (events, new_buffer) = process_raw_sse_events(buffer); + assert_eq!(events.len(), 1); + assert_eq!( + new_buffer, + "data: {\"event_type\":\"test2\",\"data\":\"hello2\"}" + ); + } + + #[tokio::test] + async fn test_parse_sse_event() { + // Test with event and data + let event_text = "event: ping\ndata: {\"message\":\"hello\"}"; + let sse_event = parse_sse_event(event_text).unwrap(); + assert_eq!(sse_event.event, Some("ping".to_string())); + assert_eq!(sse_event.id, None); + assert_eq!(sse_event.data, "{\"message\":\"hello\"}"); + + // Test with event, id, and data + let event_text = "event: update\nid: 123\ndata: {\"status\":\"ok\"}"; + let sse_event = parse_sse_event(event_text).unwrap(); + assert_eq!(sse_event.event, Some("update".to_string())); + assert_eq!(sse_event.id, Some("123".to_string())); + assert_eq!(sse_event.data, "{\"status\":\"ok\"}"); + + // Test with multi-line data + let event_text = "event: message\ndata: line 1\ndata: line 2\ndata: line 3"; + let sse_event = parse_sse_event(event_text).unwrap(); + assert_eq!(sse_event.event, Some("message".to_string())); + assert_eq!(sse_event.data, "line 1\nline 2\nline 3"); + } + + #[tokio::test] + async fn test_different_event_types() { + // Define different data structures for different event types + #[derive(Deserialize, Debug, PartialEq)] + struct PingData { + message: String, + } + + #[derive(Deserialize, Debug, PartialEq)] + struct UpdateData { + id: u32, + status: String, + } + + // Create a buffer with different event types + let buffer = "event: ping\ndata: {\"message\":\"hello\"}\n\n\ + event: update\ndata: {\"id\":123,\"status\":\"ok\"}\n\n"; + + // Process the raw events + let (raw_events, new_buffer) = process_raw_sse_events(buffer); + assert_eq!(raw_events.len(), 2); + assert_eq!(new_buffer, ""); + + // Process each event based on its type + let ping_event = raw_events[0].as_ref().unwrap(); + let update_event = raw_events[1].as_ref().unwrap(); + + assert_eq!(ping_event.event, Some("ping".to_string())); + assert_eq!(update_event.event, Some("update".to_string())); + + // Deserialize the ping event + let ping_data: PingData = serde_json::from_str(&ping_event.data).unwrap(); + assert_eq!( + ping_data, + PingData { + message: "hello".to_string() + } + ); + + // Deserialize the update event + let update_data: UpdateData = serde_json::from_str(&update_event.data).unwrap(); + assert_eq!( + update_data, + UpdateData { + id: 123, + status: "ok".to_string() + } + ); + } + + #[tokio::test] + async fn test_enum_event_types() { + // Define an enum for event types + #[derive(Deserialize, Debug, PartialEq)] + #[serde(rename_all = "lowercase")] + enum EventType { + Ping, + Update, + Message, + } + + // Define a data structure + #[derive(Deserialize, Debug, PartialEq)] + struct EventData { + value: String, + } + + // Test direct deserialization with stream_with_types + let buffer = "event: ping\ndata: {\"value\":\"ping data\"}\n\n\ + event: update\ndata: {\"value\":\"update data\"}\n\n\ + event: message\ndata: {\"value\":\"message data\"}\n\n"; + + // Process the raw events + let (raw_events, _) = process_raw_sse_events(buffer); + assert_eq!(raw_events.len(), 3); + + // Parse event types as enum values + for raw_event in raw_events { + let sse_event = raw_event.unwrap(); + let event_type: EventType = + serde_json::from_str(&format!("\"{}\"", sse_event.event.unwrap())).unwrap(); + let data: EventData = serde_json::from_str(&sse_event.data).unwrap(); + + // Verify the event type matches the expected enum variant + match event_type { + EventType::Ping => assert_eq!(data.value, "ping data"), + EventType::Update => assert_eq!(data.value, "update data"), + EventType::Message => assert_eq!(data.value, "message data"), + } + } + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/src/stream.rs b/sdks/community/rust/crates/ag-ui-client/src/stream.rs new file mode 100644 index 000000000..626c6d316 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/stream.rs @@ -0,0 +1,5 @@ +use crate::agent::AgentError; +use crate::core::event::Event; +use futures::stream::BoxStream; + +pub type EventStream<'a, StateT> = BoxStream<'a, Result, AgentError>>; diff --git a/sdks/community/rust/crates/ag-ui-client/src/subscriber.rs b/sdks/community/rust/crates/ag-ui-client/src/subscriber.rs new file mode 100644 index 000000000..2f83864e5 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/src/subscriber.rs @@ -0,0 +1,420 @@ +#![allow(unused)] + +use std::collections::HashMap; +use std::slice::Iter; +use std::sync::Arc; + +use crate::agent::{AgentError, AgentStateMutation}; +use crate::core::event::*; +use crate::core::types::{Message, RunAgentInput, ToolCall}; +use crate::core::{AgentState, FwdProps, JsonValue}; + +pub struct AgentSubscriberParams<'a, StateT: AgentState, FwdPropsT: FwdProps> { + pub messages: &'a [Message], + pub state: &'a StateT, + pub input: &'a RunAgentInput, +} + +/// Subscriber trait for handling agent events +#[async_trait::async_trait] +pub trait AgentSubscriber: Send + Sync +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + // Request lifecycle + async fn on_run_initialized( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_run_failed( + &self, + error: &AgentError, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_run_finalized( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + // Events + async fn on_event( + &self, + event: &Event, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_run_started_event( + &self, + event: &RunStartedEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_run_finished_event( + &self, + event: &RunFinishedEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_run_error_event( + &self, + event: &RunErrorEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_step_started_event( + &self, + event: &StepStartedEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_step_finished_event( + &self, + event: &StepFinishedEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_start_event( + &self, + event: &TextMessageStartEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_content_event( + &self, + event: &TextMessageContentEvent, + _text_message_buffer: &str, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_end_event( + &self, + event: &TextMessageEndEvent, + _text_message_buffer: &str, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_start_event( + &self, + event: &ToolCallStartEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_args_event( + &self, + event: &ToolCallArgsEvent, + _tool_call_buffer: &str, + tool_call_name: &str, + _partial_tool_call_args: &HashMap, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_end_event( + &self, + event: &ToolCallEndEvent, + tool_call_name: &str, + _tool_call_args: &HashMap, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_result_event( + &self, + event: &ToolCallResultEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_state_snapshot_event( + &self, + event: &StateSnapshotEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_state_delta_event( + &self, + event: &StateDeltaEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_messages_snapshot_event( + &self, + event: &MessagesSnapshotEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_raw_event( + &self, + event: &RawEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_custom_event( + &self, + event: &CustomEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_text_message_chunk_event( + &self, + event: &TextMessageChunkEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_start_event( + &self, + event: &ThinkingTextMessageStartEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_content_event( + &self, + event: &ThinkingTextMessageContentEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_text_message_end_event( + &self, + event: &ThinkingTextMessageEndEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_tool_call_chunk_event( + &self, + event: &ToolCallChunkEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_start_event( + &self, + event: &ThinkingStartEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + async fn on_thinking_end_event( + &self, + event: &ThinkingEndEvent, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result, AgentError> { + Ok(AgentStateMutation::default()) + } + + // State changes + async fn on_messages_changed( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + Ok(()) + } + + async fn on_state_changed( + &self, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + Ok(()) + } + + async fn on_new_message( + &self, + message: &Message, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + Ok(()) + } + + async fn on_new_tool_call( + &self, + tool_call: &ToolCall, + params: AgentSubscriberParams<'async_trait, StateT, FwdPropsT>, + ) -> Result<(), AgentError> { + Ok(()) + } +} + +/// Wrapper for subscriber implementations. +/// +/// Facilitates easy casting to and from types that implement [`AgentSubscriber`]. +/// +/// # Examples +/// +/// ``` +/// # use ag_ui_client::subscriber::{Subscribers, AgentSubscriber}; +/// # use std::sync::Arc; +/// # struct MySubscriber; +/// # impl AgentSubscriber for MySubscriber {} +/// +/// // Create from a single subscriber +/// let subscriber = MySubscriber; +/// let subscribers = Subscribers::from_subscriber(subscriber); +/// +/// // Create from multiple subscribers +/// let subscriber_vec = vec![MySubscriber, MySubscriber]; +/// let subscribers = Subscribers::from_iter(subscriber_vec); +/// +/// // Create from pre-wrapped Arc subscribers +/// let arc_subscribers: Vec> = vec![ +/// Arc::new(MySubscriber) +/// ]; +/// let subscribers = Subscribers::new(arc_subscribers); +/// ``` +/// +#[derive(Clone)] +pub struct Subscribers { + subs: Vec>>, +} + +impl Subscribers { + pub fn new(subscribers: Vec>>) -> Self { + Self { subs: subscribers } + } + + /// Creates a new Subscribers collection from a single subscriber + pub fn from_subscriber(subscriber: T) -> Self + where + T: AgentSubscriber + 'static, + { + Self::new(vec![Arc::new(subscriber)]) + } +} + +impl FromIterator for Subscribers +where + StateT: AgentState, + FwdPropsT: FwdProps, + T: AgentSubscriber + 'static, +{ + fn from_iter>(iter: I) -> Self { + Self::new( + iter.into_iter() + .map(|s| Arc::new(s) as Arc>) + .collect(), + ) + } +} + +impl<'a, StateT, FwdPropsT> IntoIterator for &'a Subscribers +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + type Item = &'a Arc>; + type IntoIter = Iter<'a, Arc>>; + + fn into_iter(self) -> Self::IntoIter { + self.subs.iter() + } +} + +/// Trait for types that can be converted into a Subscribers collection +/// This allows for flexible input types in APIs that accept subscribers +pub trait IntoSubscribers: Send { + fn into_subscribers(self) -> Subscribers; +} + +// Implementation for Subscribers itself (identity conversion) +impl IntoSubscribers for Subscribers +where + StateT: AgentState, + FwdPropsT: FwdProps, +{ + fn into_subscribers(self) -> Subscribers { + self + } +} + +// Implementation for single subscribers, as a unit-sized tuple +impl IntoSubscribers for (T,) +where + StateT: AgentState, + FwdPropsT: FwdProps, + T: AgentSubscriber + 'static, +{ + fn into_subscribers(self) -> Subscribers { + Subscribers::from_subscriber(self.0) + } +} + +// Implementation for Vec of subscribers +impl IntoSubscribers for Vec +where + StateT: AgentState, + FwdPropsT: FwdProps, + T: AgentSubscriber + 'static, +{ + fn into_subscribers(self) -> Subscribers { + Subscribers::from_iter(self) + } +} + +// Implementation for arrays of subscribers +impl IntoSubscribers for [T; N] +where + StateT: AgentState, + FwdPropsT: FwdProps, + T: AgentSubscriber + 'static, +{ + fn into_subscribers(self) -> Subscribers { + Subscribers::from_iter(self) + } +} + +// Implementation for empty case (no subscribers) +impl IntoSubscribers for () { + fn into_subscribers(self) -> Subscribers { + Subscribers::new(vec![]) + } +} diff --git a/sdks/community/rust/crates/ag-ui-client/tests/http_agent_test.rs b/sdks/community/rust/crates/ag-ui-client/tests/http_agent_test.rs new file mode 100644 index 000000000..44c47ae06 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/tests/http_agent_test.rs @@ -0,0 +1,116 @@ +use ag_ui_client::HttpAgent; +use ag_ui_client::agent::{Agent, RunAgentParams}; +use ag_ui_client::core::types::{Message, Role}; + +#[tokio::test] +async fn test_http_agent_basic_functionality() { + env_logger::init(); + + // Create an HttpAgent + let agent = HttpAgent::builder() + .with_url_str("http://localhost:3001/") + .unwrap() + .build() + .unwrap(); + + // Create a message asking about temperature + let message = Message::new_user("What's the temperature in Amsterdam?"); + + // Set up the run parameters + let params = RunAgentParams::new().add_message(message); + + // Run the agent + let result = agent.run_agent(¶ms, ()).await; + + // Check that the run was successful + assert!(result.is_ok(), "Agent run failed: {:?}", result.err()); + + // Check that we got some messages back + let result = result.unwrap(); + assert!(!result.new_messages.is_empty(), "No messages returned"); + + // Print the messages for debugging + for msg in &result.new_messages { + println!("Message role: {:?}", msg.role()); + println!("Message content: {:?}", msg.content().unwrap()); + if let Some(tool_calls) = msg.tool_calls() { + for tool_call in tool_calls { + println!( + "Tool call: {} with args {}", + tool_call.function.name, tool_call.function.arguments + ); + } + } + } + + // Check that we got a response from the assistant + assert!( + result + .new_messages + .iter() + .any(|m| m.role() == Role::Assistant), + "No assistant messages returned" + ); +} + +#[tokio::test] +async fn test_http_agent_tool_calls() { + // Create an HttpAgent + let agent = HttpAgent::builder() + .with_url_str("http://localhost:3001/") + .unwrap() + .build() + .unwrap(); + + // Create a message that should trigger a tool call + let message = Message::new_user("What's the temperature in Amsterdam in Celsius?"); + + // Set up the run parameters + let params = RunAgentParams::new().add_message(message); + + // Run the agent + let result = agent.run_agent(¶ms, ()).await; + + // Check that the run was successful + assert!(result.is_ok(), "Agent run failed: {:?}", result.err()); + + // Check that we got some messages back + let result = result.unwrap(); + assert!(!result.new_messages.is_empty(), "No messages returned"); + + // Check that at least one message has tool calls + let has_tool_calls = result.new_messages.iter().any(|m| { + if let Some(tool_calls) = m.tool_calls() { + !tool_calls.is_empty() + } else { + false + } + }); + + assert!(has_tool_calls, "No tool calls were made"); +} + +#[tokio::test] +async fn test_http_agent_error_handling() { + // Create an HttpAgent with an invalid URL + let agent = HttpAgent::builder() + .with_url_str("http://localhost:9999/invalid") + .unwrap() + .build() + .unwrap(); + + // Create a simple message + let message = Message::new_user("Hello."); + + // Set up the run parameters + let params = RunAgentParams::new().add_message(message); + + // Run the agent + let result = agent.run_agent(¶ms, ()).await; + + // Check that the run failed as expected + assert!( + result.is_err(), + "Agent run should have failed but succeeded" + ); +} diff --git a/sdks/community/rust/crates/ag-ui-client/tests/sse_test.rs b/sdks/community/rust/crates/ag-ui-client/tests/sse_test.rs new file mode 100644 index 000000000..8b1abb85c --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-client/tests/sse_test.rs @@ -0,0 +1,113 @@ +use ag_ui_client::sse::SseResponseExt; +use futures::StreamExt; +use reqwest::Client; +use serde::Deserialize; +use std::time::Duration; + +#[tokio::test] +async fn test_sse_with_httpbun() { + // Create a reqwest client + let client = Client::new(); + + // Make a request to httpbun.org/sse + let response = client + .get("https://httpbun.org/sse") + .timeout(Duration::from_secs(10)) + .send() + .await + .expect("Failed to send request to httpbun.org/sse"); + + // Get the events stream + let mut stream = response.event_source().await; + + // Collect a few events + let mut events: Vec<_> = Vec::new(); + let mut count = 0; + + // Collect up to 5 events + while let Some(result) = stream.next().await { + match result { + Ok(event) => { + println!("Received event: {:?}", event); + events.push(event); + count += 1; + if count >= 5 { + break; + } + } + Err(err) => { + panic!("Error receiving SSE event: {}", err); + } + } + } + + // Verify that we received events + assert!( + !events.is_empty(), + "No events received from httpbun.org/sse" + ); + + // Verify the event format + for event in &events { + // Check that the event has the expected format + assert!(event.id.is_some(), "Event should have an ID"); + assert_eq!( + event.data, "a ping event", + "Event data should be 'a ping event'" + ); + } + + // Verify that the IDs are sequential + for i in 1..events.len() { + let prev_id = events[i - 1].id.as_ref().unwrap().parse::().unwrap(); + let curr_id = events[i].id.as_ref().unwrap().parse::().unwrap(); + assert_eq!(curr_id, prev_id + 1, "Event IDs should be sequential"); + } +} + +#[tokio::test] +async fn test_sse_with_json_data() { + // Create a reqwest client + let client = Client::new(); + + #[derive(Debug, Deserialize)] + #[allow(unused)] + struct UserData { + name: String, + age: u16, + } + + // Make a request to httpbun.org/sse + let response = client + .get(r#"https://sse.dev/test?jsonobj={"name":"werner","age":38}"#) + .timeout(Duration::from_secs(5)) + .send() + .await + .expect("Failed to send request to sse.dev"); + + // Get the events stream + let mut stream = response.event_source().await; + + // Collect a few events + let mut events: Vec<_> = Vec::new(); + let mut count = 0; + + // Collect up to 5 events + while let Some(result) = stream.next().await { + match result { + Ok(event) => { + println!("Received event: {:?}", event); + let user_data: UserData = serde_json::from_str(&event.data).unwrap(); + println!("{user_data:?}"); + events.push(event); + count += 1; + if count >= 2 { + break; + } + } + Err(err) => { + panic!("Error receiving SSE event: {}", err); + } + } + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/Cargo.lock b/sdks/community/rust/crates/ag-ui-core/Cargo.lock new file mode 100644 index 000000000..1f4377081 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ag-ui" +version = "0.1.0" diff --git a/sdks/community/rust/crates/ag-ui-core/Cargo.toml b/sdks/community/rust/crates/ag-ui-core/Cargo.toml new file mode 100644 index 000000000..fa1375595 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ag-ui-core" +version = "0.1.0" +edition = "2024" +description = "Core type library for the AG-UI protocol." +license = "MIT" + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +uuid = { workspace = true } \ No newline at end of file diff --git a/sdks/community/rust/crates/ag-ui-core/README.md b/sdks/community/rust/crates/ag-ui-core/README.md new file mode 100644 index 000000000..749e5cff6 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/README.md @@ -0,0 +1,16 @@ +# AG-UI Core Types + +This repo contains the Rust types needed to work with the AG-UI protocol. Implemented using `serde` to support +(de)serialization. + +Contained are: + +* [Message types](src/types/message.rs) +* [Event types](src/event.rs) +* [State trait bounds](src/state.rs) +* [Input types](src/types/input.rs) +* [Tool type](src/types/tool.rs) +* [Context type](src/types/context.rs) +* [ID (new)types](src/types/ids.rs) + +Intended to be used with [`ag-ui-client`](../ag-ui-client). \ No newline at end of file diff --git a/sdks/community/rust/crates/ag-ui-core/src/error.rs b/sdks/community/rust/crates/ag-ui-core/src/error.rs new file mode 100644 index 000000000..d2f923c40 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/error.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +impl AguiError { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl From for AguiError { + fn from(err: serde_json::Error) -> Self { + let msg = format!("Failed to parse JSON: {err}"); + Self::new(msg) + } +} + +#[derive(Error, Debug)] +#[error("AG-UI Error: {message}")] +pub struct AguiError { + pub message: String, +} + +pub type Result = std::result::Result; diff --git a/sdks/community/rust/crates/ag-ui-core/src/event.rs b/sdks/community/rust/crates/ag-ui-core/src/event.rs new file mode 100644 index 000000000..c74c9e855 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/event.rs @@ -0,0 +1,566 @@ +use crate::JsonValue; +use crate::state::AgentState; +use crate::types::{Message, Role}; +use crate::types::{MessageId, RunId, ThreadId, ToolCallId}; +use serde::{Deserialize, Serialize}; + +/// Event types for AG-UI protocol +/// Event types for AG-UI protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum EventType { + /// Event indicating the start of a text message + TextMessageStart, + /// Event containing a piece of text message content + TextMessageContent, + /// Event indicating the end of a text message + TextMessageEnd, + /// Event containing a chunk of text message content + TextMessageChunk, + /// Event indicating the start of a thinking text message + ThinkingTextMessageStart, + /// Event indicating a piece of a thinking text message + ThinkingTextMessageContent, + /// Event indicating the end of a thinking text message + ThinkingTextMessageEnd, + /// Event indicating the start of a tool call + ToolCallStart, + /// Event containing tool call arguments + ToolCallArgs, + /// Event indicating the end of a tool call + ToolCallEnd, + /// Event containing a chunk of tool call content + ToolCallChunk, + /// Event containing the result of a tool call + ToolCallResult, + /// Event indicating the start of a thinking step event + ThinkingStart, + /// Event indicating the end of a thinking step event + ThinkingEnd, + /// Event containing a snapshot of the state + StateSnapshot, + /// Event containing a delta of the state + StateDelta, + /// Event containing a snapshot of the messages + MessagesSnapshot, + /// Event containing a raw event + Raw, + /// Event containing a custom event + Custom, + /// Event indicating that a run has started + RunStarted, + /// Event indicating that a run has finished + RunFinished, + /// Event indicating that a run has encountered an error + RunError, + /// Event indicating that a step has started + StepStarted, + /// Event indicating that a step has finished + StepFinished, +} + +/// Base event for all events in the Agent User Interaction Protocol. +/// Contains common fields that are present in all event types. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BaseEvent { + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + #[serde(rename = "rawEvent", skip_serializing_if = "Option::is_none")] + pub raw_event: Option, +} + +/// Event indicating the start of a text message. +/// This event is sent when the agent begins generating a text message. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TextMessageStartEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "messageId")] + pub message_id: MessageId, + pub role: Role, // "assistant" +} + +/// Event containing a piece of text message content. +/// This event is sent for each chunk of content as the agent generates a message. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TextMessageContentEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "messageId")] + pub message_id: MessageId, + pub delta: String, +} + +/// Event indicating the end of a text message. +/// This event is sent when the agent completes a text message. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TextMessageEndEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "messageId")] + pub message_id: MessageId, +} + +/// Event containing a chunk of text message content. +/// This event combines start, content, and potentially end information in a single event, +/// with optional fields that may or may not be present. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TextMessageChunkEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")] + pub message_id: Option, + pub role: Role, + #[serde(skip_serializing_if = "Option::is_none")] + pub delta: Option, +} + +/// Event indicating the start of a thinking text message. +/// This event is sent when the agent begins generating internal thinking content. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ThinkingTextMessageStartEvent { + #[serde(flatten)] + pub base: BaseEvent, +} + +/// Event indicating a piece of a thinking text message. +/// This event contains chunks of the agent's internal thinking process. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ThinkingTextMessageContentEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub delta: String, +} + +/// Event indicating the end of a thinking text message. +/// This event is sent when the agent completes its internal thinking process. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ThinkingTextMessageEndEvent { + #[serde(flatten)] + pub base: BaseEvent, +} + +/// Event indicating the start of a tool call. +/// This event is sent when the agent begins to call a tool with specific parameters. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCallStartEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "toolCallId")] + pub tool_call_id: ToolCallId, + #[serde(rename = "toolCallName")] + pub tool_call_name: String, + #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")] + pub parent_message_id: Option, +} + +/// Event containing tool call arguments. +/// This event contains chunks of the arguments being passed to a tool. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCallArgsEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "toolCallId")] + pub tool_call_id: ToolCallId, + pub delta: String, +} + +/// Event indicating the end of a tool call. +/// This event is sent when the agent completes sending arguments to a tool. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCallEndEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "toolCallId")] + pub tool_call_id: ToolCallId, +} + +/// Event containing the result of a tool call. +/// This event is sent when a tool has completed execution and returns its result. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCallResultEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "messageId")] + pub message_id: MessageId, + #[serde(rename = "toolCallId")] + pub tool_call_id: ToolCallId, + pub content: String, + #[serde(default = "Role::tool")] + pub role: Role, // "tool" +} + +/// Event containing a chunk of tool call content. +/// This event combines start, args, and potentially end information in a single event, +/// with optional fields that may or may not be present. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCallChunkEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + #[serde(rename = "toolCallName", skip_serializing_if = "Option::is_none")] + pub tool_call_name: Option, + #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")] + pub parent_message_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delta: Option, +} + +/// Event indicating the start of a thinking step event. +/// This event is sent when the agent begins a deliberate thinking phase. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ThinkingStartEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +/// Event indicating the end of a thinking step event. +/// This event is sent when the agent completes a thinking phase. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ThinkingEndEvent { + #[serde(flatten)] + pub base: BaseEvent, +} + +/// Event containing a snapshot of the state. +/// This event provides a complete representation of the current agent state. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(bound(deserialize = ""))] +pub struct StateSnapshotEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub snapshot: StateT, +} + +/// Event containing a delta of the state. +/// This event contains JSON Patch operations (RFC 6902) that describe changes to the agent state. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StateDeltaEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub delta: Vec, +} + +/// Event containing a snapshot of the messages. +/// This event provides a complete list of all current conversation messages. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MessagesSnapshotEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub messages: Vec, +} + +/// Event containing a raw event. +/// This event type allows wrapping arbitrary events from external sources. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RawEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub event: JsonValue, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Event containing a custom event. +/// This event type allows for application-specific custom events with arbitrary data. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CustomEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub name: String, + pub value: JsonValue, +} + +/// Event indicating that a run has started. +/// This event is sent when an agent run begins execution within a specific thread. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunStartedEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "threadId")] + pub thread_id: ThreadId, + #[serde(rename = "runId")] + pub run_id: RunId, +} + +/// Event indicating that a run has finished. +/// This event is sent when an agent run completes successfully, potentially with a result. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunFinishedEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "threadId")] + pub thread_id: ThreadId, + #[serde(rename = "runId")] + pub run_id: RunId, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Event indicating that a run has encountered an error. +/// This event is sent when an agent run fails with an error message and optional error code. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunErrorEvent { + #[serde(flatten)] + pub base: BaseEvent, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, +} + +/// Event indicating that a step has started. +/// This event is sent when a specific named step within a run begins execution. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StepStartedEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "stepName")] + pub step_name: String, +} + +/// Event indicating that a step has finished. +/// This event is sent when a specific named step within a run completes execution. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StepFinishedEvent { + #[serde(flatten)] + pub base: BaseEvent, + #[serde(rename = "stepName")] + pub step_name: String, +} + +/// Union of all possible events in the Agent User Interaction Protocol. +/// This enum represents the full set of events that can be exchanged +/// between the agent and the client. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde( + tag = "type", + rename_all = "SCREAMING_SNAKE_CASE", + bound(deserialize = "") +)] +pub enum Event { + /// Signals the start of a text message from an agent. + /// Contains the message ID and role information. + TextMessageStart(TextMessageStartEvent), + + /// Represents a chunk of content being added to an in-progress text message. + /// Contains the message ID and the text delta to append. + TextMessageContent(TextMessageContentEvent), + + /// Signals the completion of a text message. + /// Contains the message ID of the completed message. + TextMessageEnd(TextMessageEndEvent), + + /// Represents a complete or partial message chunk in a single event. + /// May contain optional message ID, role, and delta information. + TextMessageChunk(TextMessageChunkEvent), + + /// Signals the start of a thinking text message. + /// Used for internal agent thought processes that should be displayed to the user. + ThinkingTextMessageStart(ThinkingTextMessageStartEvent), + + /// Represents content being added to an in-progress thinking text message. + /// Contains the delta text to append. + ThinkingTextMessageContent(ThinkingTextMessageContentEvent), + + /// Signals the completion of a thinking text message. + ThinkingTextMessageEnd(ThinkingTextMessageEndEvent), + + /// Signals the start of a tool call by the agent. + /// Contains the tool call ID, name, and optional parent message ID. + ToolCallStart(ToolCallStartEvent), + + /// Represents arguments being added to an in-progress tool call. + /// Contains the tool call ID and argument data delta. + ToolCallArgs(ToolCallArgsEvent), + + /// Signals the completion of a tool call. + /// Contains the tool call ID of the completed call. + ToolCallEnd(ToolCallEndEvent), + + /// Represents a complete or partial tool call in a single event. + /// May contain optional tool call ID, name, parent message ID, and delta. + ToolCallChunk(ToolCallChunkEvent), + + /// Represents the result of a completed tool call. + /// Contains the message ID, tool call ID, content, and optional role. + ToolCallResult(ToolCallResultEvent), + + /// Signals the start of a thinking process. + /// Contains an optional title describing the thinking process. + ThinkingStart(ThinkingStartEvent), + + /// Signals the end of a thinking process. + ThinkingEnd(ThinkingEndEvent), + + /// Provides a complete snapshot of the current state. + /// Contains the full state as a JSON value. + StateSnapshot(StateSnapshotEvent), + + /// Provides incremental changes to the state. + /// Contains a vector of delta operations to apply to the state. + StateDelta(StateDeltaEvent), + + /// Provides a complete snapshot of all messages. + /// Contains a vector of all current messages. + MessagesSnapshot(MessagesSnapshotEvent), + + /// Wraps a raw event from an external source. + /// Contains the original event as a JSON value and an optional source identifier. + Raw(RawEvent), + + /// Represents a custom event type not covered by the standard events. + /// Contains a name identifying the custom event type and an associated value. + Custom(CustomEvent), + + /// Signals the start of an agent run. + /// Contains thread ID and run ID to identify the run. + RunStarted(RunStartedEvent), + + /// Signals the completion of an agent run. + /// Contains thread ID, run ID, and optional result data. + RunFinished(RunFinishedEvent), + + /// Signals an error that occurred during an agent run. + /// Contains error message and optional error code. + RunError(RunErrorEvent), + + /// Signals the start of a step within an agent run. + /// Contains the name of the step being started. + StepStarted(StepStartedEvent), + + /// Signals the completion of a step within an agent run. + /// Contains the name of the completed step. + StepFinished(StepFinishedEvent), +} + +impl Event { + /// Get the event type + pub fn event_type(&self) -> EventType { + match self { + Event::TextMessageStart(_) => EventType::TextMessageStart, + Event::TextMessageContent(_) => EventType::TextMessageContent, + Event::TextMessageEnd(_) => EventType::TextMessageEnd, + Event::TextMessageChunk(_) => EventType::TextMessageChunk, + Event::ThinkingTextMessageStart(_) => EventType::ThinkingTextMessageStart, + Event::ThinkingTextMessageContent(_) => EventType::ThinkingTextMessageContent, + Event::ThinkingTextMessageEnd(_) => EventType::ThinkingTextMessageEnd, + Event::ToolCallStart(_) => EventType::ToolCallStart, + Event::ToolCallArgs(_) => EventType::ToolCallArgs, + Event::ToolCallEnd(_) => EventType::ToolCallEnd, + Event::ToolCallChunk(_) => EventType::ToolCallChunk, + Event::ToolCallResult(_) => EventType::ToolCallResult, + Event::ThinkingStart(_) => EventType::ThinkingStart, + Event::ThinkingEnd(_) => EventType::ThinkingEnd, + Event::StateSnapshot(_) => EventType::StateSnapshot, + Event::StateDelta(_) => EventType::StateDelta, + Event::MessagesSnapshot(_) => EventType::MessagesSnapshot, + Event::Raw(_) => EventType::Raw, + Event::Custom(_) => EventType::Custom, + Event::RunStarted(_) => EventType::RunStarted, + Event::RunFinished(_) => EventType::RunFinished, + Event::RunError(_) => EventType::RunError, + Event::StepStarted(_) => EventType::StepStarted, + Event::StepFinished(_) => EventType::StepFinished, + } + } + + /// Get the timestamp if available + pub fn timestamp(&self) -> Option { + match self { + Event::TextMessageStart(e) => e.base.timestamp, + Event::TextMessageContent(e) => e.base.timestamp, + Event::TextMessageEnd(e) => e.base.timestamp, + Event::TextMessageChunk(e) => e.base.timestamp, + Event::ThinkingTextMessageStart(e) => e.base.timestamp, + Event::ThinkingTextMessageContent(e) => e.base.timestamp, + Event::ThinkingTextMessageEnd(e) => e.base.timestamp, + Event::ToolCallStart(e) => e.base.timestamp, + Event::ToolCallArgs(e) => e.base.timestamp, + Event::ToolCallEnd(e) => e.base.timestamp, + Event::ToolCallChunk(e) => e.base.timestamp, + Event::ToolCallResult(e) => e.base.timestamp, + Event::ThinkingStart(e) => e.base.timestamp, + Event::ThinkingEnd(e) => e.base.timestamp, + Event::StateSnapshot(e) => e.base.timestamp, + Event::StateDelta(e) => e.base.timestamp, + Event::MessagesSnapshot(e) => e.base.timestamp, + Event::Raw(e) => e.base.timestamp, + Event::Custom(e) => e.base.timestamp, + Event::RunStarted(e) => e.base.timestamp, + Event::RunFinished(e) => e.base.timestamp, + Event::RunError(e) => e.base.timestamp, + Event::StepStarted(e) => e.base.timestamp, + Event::StepFinished(e) => e.base.timestamp, + } + } +} + +/// Validation error types for events in the Agent User Interaction Protocol. +/// These errors represent validation failures when creating or processing events. +#[derive(Debug, thiserror::Error)] +pub enum EventValidationError { + #[error("Delta must not be an empty string")] + EmptyDelta, + #[error("Invalid event format: {0}")] + InvalidFormat(String), +} + +/// Validate text message content event +impl TextMessageContentEvent { + pub fn validate(&self) -> Result<(), EventValidationError> { + if self.delta.is_empty() { + return Err(EventValidationError::EmptyDelta); + } + Ok(()) + } +} + +/// Builder pattern for creating events +impl TextMessageStartEvent { + pub fn new(message_id: impl Into) -> Self { + Self { + base: BaseEvent { + timestamp: None, + raw_event: None, + }, + message_id: message_id.into(), + role: Role::Assistant, + } + } + + pub fn with_timestamp(mut self, timestamp: f64) -> Self { + self.base.timestamp = Some(timestamp); + self + } + + pub fn with_raw_event(mut self, raw_event: JsonValue) -> Self { + self.base.raw_event = Some(raw_event); + self + } +} + +impl TextMessageContentEvent { + pub fn new( + message_id: impl Into, + delta: String, + ) -> Result { + let event = Self { + base: BaseEvent { + timestamp: None, + raw_event: None, + }, + message_id: message_id.into(), + delta, + }; + event.validate()?; + Ok(event) + } + + pub fn with_timestamp(mut self, timestamp: f64) -> Self { + self.base.timestamp = Some(timestamp); + self + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/src/lib.rs b/sdks/community/rust/crates/ag-ui-core/src/lib.rs new file mode 100644 index 000000000..82b52f9e1 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/lib.rs @@ -0,0 +1,10 @@ +pub mod error; +pub mod event; +mod state; +pub mod types; + +pub use error::{AguiError, Result}; +pub use state::{AgentState, FwdProps}; + +/// Re-export to ensure the same type is used +pub use serde_json::Value as JsonValue; diff --git a/sdks/community/rust/crates/ag-ui-core/src/state.rs b/sdks/community/rust/crates/ag-ui-core/src/state.rs new file mode 100644 index 000000000..0436ac7f6 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/state.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::fmt::Debug; + +/// Trait bounds for agent's state +pub trait AgentState: + 'static + Debug + Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize + Default +{ +} + +impl AgentState for JsonValue {} +impl AgentState for () {} + +/// Trait bounds for forwarded props +pub trait FwdProps: + 'static + Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize + Default +{ +} + +impl FwdProps for JsonValue {} +impl FwdProps for () {} diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/context.rs b/sdks/community/rust/crates/ag-ui-core/src/types/context.rs new file mode 100644 index 000000000..837bd417e --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/context.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Context { + /// A description of the context item + pub description: String, + /// The value of the context item + pub value: String, +} + +impl Context { + pub fn new(description: String, value: String) -> Self { + Self { description, value } + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/ids.rs b/sdks/community/rust/crates/ag-ui-core/src/types/ids.rs new file mode 100644 index 000000000..30e0d2843 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/ids.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use uuid::Uuid; + +/// Macro to define a newtype ID based on Uuid. +macro_rules! define_id_type { + // This arm of the macro handles calls that don't specify extra derives. + ($name:ident) => { + define_id_type!($name,); + }; + // This arm handles calls that do specify extra derives (like Eq). + ($name:ident, $($extra_derive:ident),*) => { + #[doc = concat!(stringify!($name), ": A newtype used to prevent mixing it with other ID values.")] + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, $($extra_derive),*)] + pub struct $name(Uuid); + + impl $name { + /// Creates a new random ID. + pub fn random() -> Self { + Self(Uuid::new_v4()) + } + } + + /// Allows creating an ID from a Uuid. + impl From for $name { + fn from(uuid: Uuid) -> Self { + Self(uuid) + } + } + + /// Allows converting an ID back into a Uuid. + impl From<$name> for Uuid { + fn from(id: $name) -> Self { + id.0 + } + } + + /// Allows getting a reference to the inner Uuid. + impl AsRef for $name { + fn as_ref(&self) -> &Uuid { + &self.0 + } + } + + /// Allows printing the ID. + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + /// Allows parsing an ID from a string slice. + impl std::str::FromStr for $name { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(Uuid::parse_str(s)?)) + } + } + + /// Allows comparing the ID with a Uuid. + impl PartialEq for $name { + fn eq(&self, other: &Uuid) -> bool { + self.0 == *other + } + } + + /// Allows comparing the ID with a string slice. + impl PartialEq for $name { + fn eq(&self, other: &str) -> bool { + if let Ok(uuid) = Uuid::parse_str(other) { + self.0 == uuid + } else { + false + } + } + } + }; +} + +define_id_type!(AgentId); +define_id_type!(ThreadId); +define_id_type!(RunId); +define_id_type!(MessageId); + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct ToolCallId(String); + +/// Tool Call ID +/// +/// Does not follow UUID format, instead uses "call_xxxxxxxx" +impl ToolCallId { + pub fn random() -> Self { + let uuid = &Uuid::new_v4().to_string()[..8]; + let id = format!("call_{uuid}"); + Self(id) + } +} + +impl Deref for ToolCallId { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + // Test whether tool call ID has same format as rest of AG-UI + #[test] + fn test_tool_call_random() { + let id = super::ToolCallId::random(); + assert_eq!(id.0.len(), 5 + 8); + assert!(id.0.starts_with("call_")); + dbg!(id); + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/input.rs b/sdks/community/rust/crates/ag-ui-core/src/types/input.rs new file mode 100644 index 000000000..916a8b07b --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/input.rs @@ -0,0 +1,43 @@ +use crate::JsonValue; +use crate::types::context::Context; +use crate::types::ids::{RunId, ThreadId}; +use crate::types::message::Message; +use crate::types::tool::Tool; +use serde::{Deserialize, Serialize}; + +/// Input for running an agent. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunAgentInput { + #[serde(rename = "threadId")] + pub thread_id: ThreadId, + #[serde(rename = "runId")] + pub run_id: RunId, + pub state: StateT, + pub messages: Vec, + pub tools: Vec, + pub context: Vec, + #[serde(rename = "forwardedProps")] + pub forwarded_props: FwdPropsT, +} + +impl RunAgentInput { + pub fn new( + thread_id: impl Into, + run_id: impl Into, + state: StateT, + messages: Vec, + tools: Vec, + context: Vec, + forwarded_props: FwdPropsT, + ) -> Self { + Self { + thread_id: thread_id.into(), + run_id: run_id.into(), + state, + messages, + tools, + context, + forwarded_props, + } + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/message.rs b/sdks/community/rust/crates/ag-ui-core/src/types/message.rs new file mode 100644 index 000000000..8ce523cad --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/message.rs @@ -0,0 +1,372 @@ +use crate::types::ids::{MessageId, ToolCallId}; +use crate::types::tool::ToolCall; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FunctionCall { + pub name: String, + // TODO: More suitable to use JsonValue here? + pub arguments: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Role { + Developer, + System, + Assistant, + User, + Tool, +} + +// Utility methods for serde defaults +impl Role { + pub(crate) fn developer() -> Self { + Self::Developer + } + pub(crate) fn system() -> Self { + Self::System + } + pub(crate) fn assistant() -> Self { + Self::Assistant + } + pub(crate) fn user() -> Self { + Self::User + } + pub(crate) fn tool() -> Self { + Self::Tool + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BaseMessage { + pub id: MessageId, + pub role: Role, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeveloperMessage { + pub id: MessageId, + #[serde(default = "Role::developer")] + pub role: Role, // Always Role::Developer + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl DeveloperMessage { + pub fn new(id: impl Into, content: String) -> Self { + Self { + id: id.into(), + role: Role::Developer, + content, + name: None, + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SystemMessage { + pub id: MessageId, + #[serde(default = "Role::system")] + pub role: Role, // Always Role::System + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl SystemMessage { + pub fn new(id: impl Into, content: String) -> Self { + Self { + id: id.into(), + role: Role::System, + content, + name: None, + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AssistantMessage { + pub id: MessageId, + #[serde(default = "Role::assistant")] + pub role: Role, // Always Role::Assistant + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "toolCalls", skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, +} + +impl AssistantMessage { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + role: Role::Assistant, + content: None, + name: None, + tool_calls: None, + } + } + + pub fn with_content(mut self, content: String) -> Self { + self.content = Some(content); + self + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + pub fn with_tool_calls(mut self, tool_calls: Vec) -> Self { + self.tool_calls = Some(tool_calls); + self + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserMessage { + pub id: MessageId, + #[serde(default = "Role::user")] + pub role: Role, // Always Role::User + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl UserMessage { + pub fn new(id: impl Into, content: String) -> Self { + Self { + id: id.into(), + role: Role::User, + content, + name: None, + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolMessage { + pub id: MessageId, + pub content: String, + #[serde(default = "Role::tool")] + pub role: Role, // Always Role::Tool + #[serde(rename = "toolCallId")] + pub tool_call_id: ToolCallId, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl ToolMessage { + pub fn new( + id: impl Into, + content: String, + tool_call_id: impl Into, + ) -> Self { + Self { + id: id.into(), + content, + role: Role::Tool, + tool_call_id: tool_call_id.into(), + error: None, + } + } + + pub fn with_error(mut self, error: String) -> Self { + self.error = Some(error); + self + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "role", rename_all = "lowercase")] +pub enum Message { + Developer { + id: MessageId, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + }, + System { + id: MessageId, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + }, + Assistant { + id: MessageId, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(rename = "toolCalls", skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + }, + User { + id: MessageId, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + }, + Tool { + id: MessageId, + content: String, + #[serde(rename = "toolCallId")] + tool_call_id: ToolCallId, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + }, +} + +impl Message { + pub fn new>(role: Role, id: impl Into, content: S) -> Self { + match role { + Role::Developer => Self::Developer { + id: id.into(), + content: content.as_ref().to_string(), + name: None, + }, + Role::System => Self::System { + id: id.into(), + content: content.as_ref().to_string(), + name: None, + }, + Role::Assistant => Self::Assistant { + id: id.into(), + content: Some(content.as_ref().to_string()), + name: None, + tool_calls: None, + }, + Role::User => Self::User { + id: id.into(), + content: content.as_ref().to_string(), + name: None, + }, + Role::Tool => Self::Tool { + id: id.into(), + content: content.as_ref().to_string(), + tool_call_id: ToolCallId::random(), + error: None, + }, + } + } + + /// Returns a User message with a random ID and the given content + pub fn new_user>(content: S) -> Self { + Self::new(Role::User, MessageId::random(), content) + } + + /// Returns a Tool message with a random ID and the given content + pub fn new_tool>(content: S) -> Self { + Self::new(Role::Tool, MessageId::random(), content) + } + + /// Returns a System message with a random ID and the given content + pub fn new_system>(content: S) -> Self { + Self::new(Role::System, MessageId::random(), content) + } + + /// Returns an Assistant message with a random ID and the given content + pub fn new_assistant>(content: S) -> Self { + Self::new(Role::Assistant, MessageId::random(), content) + } + + /// Returns a Developer message with a random ID and the given content + pub fn new_developer>(content: S) -> Self { + Self::new(Role::Developer, MessageId::random(), content) + } + + pub fn id(&self) -> &MessageId { + match self { + Message::Developer { id, .. } => id, + Message::System { id, .. } => id, + Message::Assistant { id, .. } => id, + Message::User { id, .. } => id, + Message::Tool { id, .. } => id, + } + } + + pub fn id_mut(&mut self) -> &mut MessageId { + match self { + Message::Developer { id, .. } => id, + Message::System { id, .. } => id, + Message::Assistant { id, .. } => id, + Message::User { id, .. } => id, + Message::Tool { id, .. } => id, + } + } + + pub fn role(&self) -> Role { + match self { + Message::Developer { .. } => Role::Developer, + Message::System { .. } => Role::System, + Message::Assistant { .. } => Role::Assistant, + Message::User { .. } => Role::User, + Message::Tool { .. } => Role::Tool, + } + } + pub fn content(&self) -> Option<&str> { + match self { + Message::Developer { content, .. } => Some(content), + Message::System { content, .. } => Some(content), + Message::User { content, .. } => Some(content), + Message::Tool { content, .. } => Some(content), + Message::Assistant { content, .. } => content.as_deref(), + } + } + + pub fn content_mut(&mut self) -> Option<&mut String> { + match self { + Message::Developer { content, .. } + | Message::System { content, .. } + | Message::User { content, .. } + | Message::Tool { content, .. } => Some(content), + Message::Assistant { content, .. } => { + if content.is_none() { + *content = Some(String::new()); + } + content.as_mut() + } + } + } + + pub fn tool_calls(&self) -> Option<&[ToolCall]> { + match self { + Message::Assistant { tool_calls, .. } => tool_calls.as_deref(), + _ => None, + } + } + + pub fn tool_calls_mut(&mut self) -> Option<&mut Vec> { + match self { + Message::Assistant { tool_calls, .. } => { + if tool_calls.is_none() { + *tool_calls = Some(Vec::new()); + } + tool_calls.as_mut() + } + _ => None, + } + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/mod.rs b/sdks/community/rust/crates/ag-ui-core/src/types/mod.rs new file mode 100644 index 000000000..0ab5dbeaa --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/mod.rs @@ -0,0 +1,11 @@ +mod context; +mod ids; +mod input; +mod message; +mod tool; + +pub use context::*; +pub use ids::*; +pub use input::*; +pub use message::*; +pub use tool::*; diff --git a/sdks/community/rust/crates/ag-ui-core/src/types/tool.rs b/sdks/community/rust/crates/ag-ui-core/src/types/tool.rs new file mode 100644 index 000000000..c9195c205 --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/src/types/tool.rs @@ -0,0 +1,43 @@ +use crate::types::ids::ToolCallId; +use crate::types::message::FunctionCall; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolCall { + pub id: ToolCallId, + #[serde(rename = "type")] + pub call_type: String, + pub function: FunctionCall, +} + +impl ToolCall { + pub fn new(id: impl Into, function: FunctionCall) -> Self { + Self { + id: id.into(), + call_type: "function".to_string(), + function, + } + } +} + +/// A tool definition. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Tool { + /// The tool name + pub name: String, + /// The tool description + pub description: String, + /// The tool parameters + pub parameters: serde_json::Value, +} + +impl Tool { + pub fn new(name: String, description: String, parameters: JsonValue) -> Self { + Self { + name, + description, + parameters, + } + } +} diff --git a/sdks/community/rust/crates/ag-ui-core/tests/unit.rs b/sdks/community/rust/crates/ag-ui-core/tests/unit.rs new file mode 100644 index 000000000..5975fffcc --- /dev/null +++ b/sdks/community/rust/crates/ag-ui-core/tests/unit.rs @@ -0,0 +1,320 @@ +#[cfg(test)] +mod tests { + use ag_ui_core::error::AguiError; + use ag_ui_core::types::{ + AssistantMessage, Context, DeveloperMessage, FunctionCall, Message, MessageId, Role, + RunAgentInput, RunId, SystemMessage, ThreadId, Tool, ToolCall, ToolCallId, ToolMessage, + UserMessage, + }; + use serde::{Deserialize, Serialize}; + use serde_json::json; + use uuid::Uuid; + + #[test] + fn test_role_serialization() { + let role = Role::Developer; + let json = serde_json::to_string(&role).unwrap(); + assert_eq!(json, r#""developer""#); + } + + #[test] + fn test_message_types() { + let dev_msg = DeveloperMessage::new(MessageId::random(), "dev content".to_string()) + .with_name("dev".to_string()); + assert_eq!(dev_msg.role, Role::Developer); + assert_eq!(dev_msg.name, Some("dev".to_string())); + + let sys_msg = SystemMessage::new(MessageId::random(), "sys content".to_string()) + .with_name("sys".to_string()); + assert_eq!(sys_msg.role, Role::System); + + let user_msg = UserMessage::new(MessageId::random(), "user content".to_string()) + .with_name("user".to_string()); + assert_eq!(user_msg.role, Role::User); + + let tool_msg = ToolMessage::new( + MessageId::random(), + "result".to_string(), + ToolCallId::random(), + ) + .with_error("error".to_string()); + assert_eq!(tool_msg.role, Role::Tool); + assert_eq!(tool_msg.error, Some("error".to_string())); + } + + #[test] + fn test_message_serialization() { + let user_msg = Message::User { + id: MessageId::random(), + content: "Hello".to_string(), + name: None, + }; + + let json = serde_json::to_string(&user_msg).unwrap(); + let deserialized: Message = serde_json::from_str(&json).unwrap(); + + assert_eq!(user_msg, deserialized); + } + + #[test] + fn test_tool_call_creation() { + let function_call = FunctionCall { + name: "test_function".to_string(), + arguments: "{}".to_string(), + }; + + let tool_call = ToolCall::new(ToolCallId::random(), function_call); + assert_eq!(tool_call.call_type, "function"); + } + + #[test] + fn test_assistant_message_builder() { + let msg = AssistantMessage::new(MessageId::random()) + .with_content("Hello".to_string()) + .with_name("Assistant".to_string()); + + assert_eq!(msg.content, Some("Hello".to_string())); + assert_eq!(msg.name, Some("Assistant".to_string())); + } + + #[test] + fn test_context_and_tool() { + let context = Context::new("test desc".to_string(), "test value".to_string()); + assert_eq!(context.description, "test desc"); + + let tool = Tool::new( + "test_tool".to_string(), + "tool desc".to_string(), + json!({"type": "object"}), + ); + assert_eq!(tool.name, "test_tool"); + } + + #[test] + fn test_agui_error() { + let error = AguiError::new("test error"); + assert_eq!(error.to_string(), "AG-UI Error: test error"); + } + + #[test] + fn test_custom_state() { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + struct CustomState { + pub document: String, + pub num_edits: u64, + } + + let state = CustomState { + document: "Hello, world!".to_string(), + num_edits: 0, + }; + + // If this compiles, it's okay + let _input = RunAgentInput::new( + ThreadId::random(), + RunId::random(), + state, + vec![], + vec![], + vec![], + json!({}), + ); + } + + #[test] + fn test_custom_forward_props() { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + struct CustomFwdProps { + pub document: String, + pub num_edits: u64, + } + + let fwd_props = CustomFwdProps { + document: "Hello, world!".to_string(), + num_edits: 0, + }; + + // If this compiles, it's okay + let _input = RunAgentInput::new( + Uuid::new_v4(), + Uuid::new_v4(), + json!({}), + vec![], + vec![], + vec![], + fwd_props, + ); + } + + #[test] + fn test_complex_assistant_message_deserialization() { + let json_str = r#"{ + "role": "assistant", + "id": "00000000-0000-0000-0000-000000000000", + "content": "I'll help you with that function.", + "name": "CodeHelper", + "toolCalls": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "type": "function", + "function": { + "name": "write_function", + "arguments": "{\"language\":\"rust\",\"name\":\"example\"}" + } + } + ] + }"#; + + let msg: Message = serde_json::from_str(json_str).unwrap(); + match msg { + Message::Assistant { + id, + content, + name, + tool_calls, + } => { + assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000"); + assert_eq!( + content, + Some("I'll help you with that function.".to_string()) + ); + assert_eq!(name, Some("CodeHelper".to_string())); + assert!(tool_calls.is_some()); + let calls = tool_calls.unwrap(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].function.name, "write_function"); + } + _ => panic!("Wrong message type"), + } + } + + #[test] + fn test_complex_message_array_deserialization() { + let json_str = r#"[ + { + "role": "user", + "id": "00000000-0000-0000-0000-000000000000", + "content": "Hello!", + "name": "Alice" + }, + { + "role": "assistant", + "id": "00000000-0000-0000-0000-000000000000", + "content": "Hi Alice!", + "name": "Assistant" + }, + { + "role": "tool", + "id": "00000000-0000-0000-0000-000000000000", + "content": "Function result", + "toolCallId": "00000000-0000-0000-0000-000000000000" + } + ]"#; + + let messages: Vec = serde_json::from_str(json_str).unwrap(); + assert_eq!(messages.len(), 3); + + match &messages[0] { + Message::User { id, content, name } => { + assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000"); + assert_eq!(content, "Hello!"); + assert_eq!(*name, Some("Alice".to_string())); + } + _ => panic!("Wrong message type"), + } + } + + #[test] + fn test_complex_run_agent_input_deserialization() { + let json_str = r#"{ + "threadId": "00000000-0000-0000-0000-000000000000", + "runId": "00000000-0000-0000-0000-000000000000", + "state": {"counter": 42}, + "messages": [ + { + "role": "user", + "id": "00000000-0000-0000-0000-000000000000", + "content": "Hello" + } + ], + "tools": [ + { + "name": "calculator", + "description": "Performs calculations", + "parameters": { + "type": "object", + "properties": { + "operation": {"type": "string"} + } + } + } + ], + "context": [ + { + "description": "Current time", + "value": "2024-02-14T12:00:00Z" + } + ], + "forwardedProps": {"settings": {"debug": true}} + }"#; + + let input: RunAgentInput = serde_json::from_str(json_str).unwrap(); + assert_eq!(input.messages.len(), 1); + assert_eq!(input.tools.len(), 1); + assert_eq!(input.context.len(), 1); + } + + #[test] + fn test_complex_run_agent_input_deserialization_custom_state() { + #[derive(Debug, Deserialize, Serialize)] + struct CustomState { + counter: u32, + } + + #[derive(Debug, Deserialize, Serialize)] + struct OtherState { + document: String, + } + + let json_str = r#"{ + "threadId": "00000000-0000-0000-0000-000000000000", + "runId": "00000000-0000-0000-0000-000000000000", + "state": {"counter": 42}, + "messages": [ + { + "role": "user", + "id": "00000000-0000-0000-0000-000000000000", + "content": "Hello" + } + ], + "tools": [ + { + "name": "calculator", + "description": "Performs calculations", + "parameters": { + "type": "object", + "properties": { + "operation": {"type": "string"} + } + } + } + ], + "context": [ + { + "description": "Current time", + "value": "2024-02-14T12:00:00Z" + } + ], + "forwardedProps": {"settings": {"debug": true}} + }"#; + + let input: RunAgentInput = serde_json::from_str(json_str).unwrap(); + assert_eq!(input.messages.len(), 1); + assert_eq!(input.tools.len(), 1); + assert_eq!(input.context.len(), 1); + + let wrong_input: serde_json::Result> = + serde_json::from_str(json_str); + assert!(wrong_input.is_err()) + } +}