From eb1f8e457d4e63775f62ff75fb8a18a24b089abc Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Sat, 15 Feb 2025 00:21:36 +0800 Subject: [PATCH] feat: add `anda_engine_cli` --- Cargo.lock | 20 ++++++ Cargo.toml | 2 + agents/anda_bot/src/main.rs | 12 ++-- anda_core/src/model.rs | 9 ++- anda_engine/src/context/agent.rs | 34 ++++++---- anda_engine/src/engine.rs | 4 +- anda_engine/src/extension/character.rs | 5 +- anda_engine/src/extension/segmenter.rs | 1 + anda_engine/src/model/deepseek.rs | 51 ++++++++------ anda_engine/src/model/mod.rs | 6 +- anda_engine/src/model/openai.rs | 55 +++++++++------ anda_engine_cli/Cargo.toml | 27 ++++++++ anda_engine_cli/README.md | 21 ++++++ anda_engine_cli/src/main.rs | 92 ++++++++++++++++++++++++++ anda_engine_server/README.md | 2 +- anda_engine_server/src/handler.rs | 13 +++- anda_web3_client/src/client.rs | 11 ++- example.env | 3 +- examples/icp_ledger_agent/README.md | 9 ++- examples/icp_ledger_agent/src/agent.rs | 2 +- examples/icp_ledger_agent/src/main.rs | 20 ++++-- tools/anda_icp/Cargo.toml | 1 + tools/anda_icp/src/ledger/mod.rs | 13 ++++ 23 files changed, 329 insertions(+), 84 deletions(-) create mode 100644 anda_engine_cli/Cargo.toml create mode 100644 anda_engine_cli/README.md create mode 100644 anda_engine_cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index b63acb0..f5f924c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,25 @@ dependencies = [ "url", ] +[[package]] +name = "anda_engine_cli" +version = "0.1.0" +dependencies = [ + "anda_core", + "anda_engine", + "anda_web3_client", + "base64 0.22.1", + "ciborium", + "clap", + "const-hex", + "dotenv", + "ic_cose_types", + "ic_tee_agent", + "rand", + "structured-logger", + "tokio", +] + [[package]] name = "anda_engine_server" version = "0.1.0" @@ -219,6 +238,7 @@ dependencies = [ "anda_engine", "candid", "icrc-ledger-types", + "log", "num-traits", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index ed61786..802e195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "anda_core", "anda_engine", + "anda_engine_cli", "anda_engine_server", "anda_lancedb", "anda_web3_client", @@ -36,6 +37,7 @@ axum = { version = "0.8", features = [ "query", ], default-features = true } bytes = "1" +base64 = "0.22" candid = "0.10" ciborium = "0.2" futures = "0.3" diff --git a/agents/anda_bot/src/main.rs b/agents/anda_bot/src/main.rs index 7945713..60afbc9 100644 --- a/agents/anda_bot/src/main.rs +++ b/agents/anda_bot/src/main.rs @@ -409,7 +409,7 @@ async fn bootstrap_local( let default_agent = character.username.clone(); let knowledge_table: Path = default_agent.to_ascii_lowercase().into(); - let web3 = Web3Client::new(&ic_host, id_secret, root_secret, None).await?; + let web3 = Web3Client::new(&ic_host, id_secret, root_secret, None, None).await?; let my_principal = web3.get_principal(); log::info!(target: LOG_TARGET, "start local service, principal: {:?}", my_principal.to_text()); @@ -559,10 +559,6 @@ async fn connect_knowledge_store( fn connect_model(cfg: &config::Llm) -> Result { if cfg.openai_api_key.is_empty() { Ok(Model::new( - Arc::new( - cohere::Client::new(&cfg.cohere_api_key) - .embedding_model(&cfg.cohere_embedding_model), - ), Arc::new( deepseek::Client::new( &cfg.deepseek_api_key, @@ -578,6 +574,10 @@ fn connect_model(cfg: &config::Llm) -> Result { &cfg.deepseek_model }), ), + Arc::new( + cohere::Client::new(&cfg.cohere_api_key) + .embedding_model(&cfg.cohere_embedding_model), + ), )) } else { let cli = openai::Client::new( @@ -589,8 +589,8 @@ fn connect_model(cfg: &config::Llm) -> Result { }, ); Ok(Model::new( - Arc::new(cli.embedding_model(&cfg.openai_embedding_model)), Arc::new(cli.completion_model(&cfg.openai_completion_model)), + Arc::new(cli.embedding_model(&cfg.openai_embedding_model)), )) } } diff --git a/anda_core/src/model.rs b/anda_core/src/model.rs index ed508a5..5a8f6d9 100644 --- a/anda_core/src/model.rs +++ b/anda_core/src/model.rs @@ -30,6 +30,10 @@ pub struct AgentOutput { /// Tool call that this message is responding to. If this message is a response to a tool call, this field should be set to the tool call ID. #[serde(skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, + + /// full_history will not be included in the engine's response + #[serde(skip_serializing_if = "Option::is_none")] + pub full_history: Option>, } /// Represents a tool call response with it's ID, function name, and arguments @@ -192,13 +196,14 @@ pub struct CompletionRequest { /// The name of system role pub system_name: Option, - /// The chat history to be sent to the completion model provider - pub chat_history: Vec, + /// The chat history (raw message) to be sent to the completion model provider + pub chat_history: Vec, /// The documents to embed into the prompt pub documents: Documents, /// The prompt to be sent to the completion model provider as "user" role + /// It can be empty. pub prompt: String, /// The name of the prompter diff --git a/anda_engine/src/context/agent.rs b/anda_engine/src/context/agent.rs index dc95633..fefcfed 100644 --- a/anda_engine/src/context/agent.rs +++ b/anda_engine/src/context/agent.rs @@ -29,11 +29,12 @@ use anda_core::{ AgentContext, AgentOutput, AgentSet, BaseContext, BoxError, CacheExpiry, CacheFeatures, CancellationToken, CanisterCaller, CompletionFeatures, CompletionRequest, Embedding, EmbeddingFeatures, FunctionDefinition, HttpFeatures, KeysFeatures, Message, ObjectMeta, Path, - PutMode, PutResult, StateFeatures, StoreFeatures, ToolCall, ToolSet, + PutMode, PutResult, StateFeatures, StoreFeatures, ToolCall, ToolSet, Value, }; use candid::{utils::ArgumentEncoder, CandidType, Principal}; use serde::{de::DeserializeOwned, Serialize}; use serde_bytes::ByteBuf; +use serde_json::json; use std::{future::Future, sync::Arc, time::Duration}; use super::base::BaseCtx; @@ -260,27 +261,31 @@ impl CompletionFeatures for AgentCtx { let mut tool_calls_result: Vec = Vec::new(); loop { let mut res = self.model.completion(req.clone()).await?; - // 自动执行 tools 调用 - let mut tool_calls_continue: Vec = Vec::new(); + // automatically executes tools calls + let mut tool_calls_continue: Vec = Vec::new(); if let Some(tool_calls) = &mut res.tool_calls { - // 移除已处理的 tools - req.tools - .retain(|t| !tool_calls.iter().any(|o| o.name == t.name)); for tool in tool_calls.iter_mut() { - match self.tool_call(&tool.id, tool.args.clone()).await { + if !req.tools.iter().any(|t| t.name == tool.name) { + // tool already called, skip + continue; + } + + // remove called tool from req.tools + req.tools.retain(|t| t.name != tool.name); + match self.tool_call(&tool.name, tool.args.clone()).await { Ok((val, con)) => { if con { - // 需要使用大模型继续处理 tool 返回结果 - tool_calls_continue.push(Message { + // need to use LLM to continue processing tool_call result + tool_calls_continue.push(json!(Message { role: "tool".to_string(), content: val.clone().into(), - name: Some(tool.name.clone()), + name: None, tool_call_id: Some(tool.id.clone()), - }); + })); } tool.result = Some(val); } - Err(_) => { + Err(_err) => { // TODO: // support remote_tool_call // support agent_run @@ -301,7 +306,10 @@ impl CompletionFeatures for AgentCtx { return Ok(res); } - // 将 tools 处理结果追加到 history 消息列表,交给大模型继续处理 + req.system = None; + req.documents.clear(); + req.prompt = "".to_string(); + req.chat_history = res.full_history.unwrap_or_default(); req.chat_history.append(&mut tool_calls_continue); } } diff --git a/anda_engine/src/engine.rs b/anda_engine/src/engine.rs index 599cef4..c53452c 100644 --- a/anda_engine/src/engine.rs +++ b/anda_engine/src/engine.rs @@ -159,7 +159,9 @@ impl Engine { } let ctx = self.ctx.child_with(&name, user, caller)?; - self.ctx.agents.run(&name, ctx, prompt, attachment).await + let mut res = self.ctx.agents.run(&name, ctx, prompt, attachment).await?; + res.full_history = None; // clear full history + Ok(res) } /// Calls a tool by name with the specified arguments. diff --git a/anda_engine/src/extension/character.rs b/anda_engine/src/extension/character.rs index 7bbf9d2..aa32b80 100644 --- a/anda_engine/src/extension/character.rs +++ b/anda_engine/src/extension/character.rs @@ -34,6 +34,7 @@ use anda_core::{ }; use ic_cose_types::to_cbor_bytes; use serde::{Deserialize, Serialize}; +use serde_json::json; use std::{fmt::Write, sync::Arc, time::Duration}; use super::{ @@ -545,7 +546,7 @@ where .append_tools(tools); if let Some((user, chat)) = &mut chat_history { - req.chat_history = chat.clone(); + req.chat_history = chat.clone().into_iter().map(|m| json!(m)).collect(); chat.push(Message { role: "user".to_string(), content: req.prompt.clone().into(), @@ -568,7 +569,7 @@ where chat.push(Message { role: "tool".to_string(), content: "".to_string().into(), - name: Some(tool_res.name.clone()), + name: None, tool_call_id: Some(tool_res.id.clone()), }); } diff --git a/anda_engine/src/extension/segmenter.rs b/anda_engine/src/extension/segmenter.rs index fd67dc1..291a606 100644 --- a/anda_engine/src/extension/segmenter.rs +++ b/anda_engine/src/extension/segmenter.rs @@ -117,6 +117,7 @@ impl DocumentSegmenter { args: res_str.clone(), result: Some(res_str), }]), + full_history: None, }, )); } diff --git a/anda_engine/src/model/deepseek.rs b/anda_engine/src/model/deepseek.rs index 9e6930d..6fae8ff 100644 --- a/anda_engine/src/model/deepseek.rs +++ b/anda_engine/src/model/deepseek.rs @@ -82,7 +82,7 @@ impl Client { } /// Token usage statistics from DeepSeek API responses -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Usage { /// Number of tokens used in the prompt pub prompt_tokens: usize, @@ -101,7 +101,7 @@ impl std::fmt::Display for Usage { } /// Completion response from DeepSeek API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct CompletionResponse { /// Unique identifier for the completion pub id: String, @@ -118,8 +118,9 @@ pub struct CompletionResponse { } impl CompletionResponse { - fn try_into(mut self) -> Result { + fn try_into(mut self, mut full_history: Vec) -> Result { let choice = self.choices.pop().ok_or("No completion choice")?; + full_history.push(json!(choice.message)); let mut output = AgentOutput { content: choice.message.content, tool_calls: choice.message.tool_calls.map(|tools| { @@ -133,6 +134,7 @@ impl CompletionResponse { }) .collect() }), + full_history: Some(full_history), ..Default::default() }; @@ -148,7 +150,7 @@ impl CompletionResponse { } /// Individual completion choice from DeepSeek API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Choice { pub index: usize, pub message: MessageOutput, @@ -156,7 +158,7 @@ pub struct Choice { } /// Output message structure from DeepSeek API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct MessageOutput { pub role: String, #[serde(default)] @@ -166,7 +168,7 @@ pub struct MessageOutput { } /// Tool call output structure from DeepSeek API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ToolCallOutput { pub id: String, pub r#type: String, @@ -188,7 +190,7 @@ impl From for ToolDefinition { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Function { pub name: String, pub arguments: String, @@ -231,38 +233,40 @@ impl CompletionFeaturesDyn for CompletionModel { Box::pin(async move { // Add system to chat history (if available) let mut full_history = if let Some(system) = &req.system { - vec![Message { + vec![json!(Message { role: "system".into(), content: system.to_owned().into(), name: req.system_name.clone(), ..Default::default() - }] + })] } else { vec![] }; // Add context documents to chat history if !req.documents.is_empty() { - full_history.push(Message { + full_history.push(json!(Message { role: "user".into(), content: format!("{}", req.documents).into(), ..Default::default() - }); + })); } // Extend existing chat history full_history.append(&mut req.chat_history); - full_history.push(Message { - role: "user".into(), - content: req.prompt.into(), - name: req.prompter_name, - ..Default::default() - }); + if !req.prompt.is_empty() { + full_history.push(json!(Message { + role: "user".into(), + content: req.prompt.into(), + name: req.prompter_name, + ..Default::default() + })); + } let mut body = json!({ "model": model, - "messages": full_history, + "messages": full_history.clone(), "temperature": req.temperature, }); let body = body.as_object_mut().unwrap(); @@ -304,14 +308,21 @@ impl CompletionFeaturesDyn for CompletionModel { if log_enabled!(Debug) { if let Ok(val) = serde_json::to_string(&body) { - log::debug!("DeepSeek request: {}", val); + log::debug!(request = val; "DeepSeek completions request"); } } let response = client.post("/chat/completions").json(body).send().await?; if response.status().is_success() { match response.json::().await { - Ok(res) => res.try_into(), + Ok(res) => { + if log_enabled!(Debug) { + if let Ok(val) = serde_json::to_string(&res) { + log::debug!(response = val; "DeepSeek completions response"); + } + } + res.try_into(full_history) + } Err(err) => Err(format!("DeepSeek completions error: {}", err).into()), } } else { diff --git a/anda_engine/src/model/mod.rs b/anda_engine/src/model/mod.rs index 3581d7d..dc58850 100644 --- a/anda_engine/src/model/mod.rs +++ b/anda_engine/src/model/mod.rs @@ -129,8 +129,8 @@ pub struct Model { impl Model { /// Creates a new Model with specified embedder and completer pub fn new( - embedder: Arc, completer: Arc, + embedder: Arc, ) -> Self { Self { embedder, @@ -141,16 +141,16 @@ impl Model { /// Creates a Model with unimplemented features (returns errors for all operations) pub fn not_implemented() -> Self { Self { - embedder: Arc::new(NotImplemented), completer: Arc::new(NotImplemented), + embedder: Arc::new(NotImplemented), } } /// Creates a Model with mock implementations for testing pub fn mock_implemented() -> Self { Self { - embedder: Arc::new(MockImplemented), completer: Arc::new(MockImplemented), + embedder: Arc::new(MockImplemented), } } } diff --git a/anda_engine/src/model/openai.rs b/anda_engine/src/model/openai.rs index ba43490..3d24200 100644 --- a/anda_engine/src/model/openai.rs +++ b/anda_engine/src/model/openai.rs @@ -153,7 +153,7 @@ impl Client { } /// Response structure for OpenAI embedding API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct EmbeddingResponse { pub object: String, pub data: Vec, @@ -185,7 +185,7 @@ impl EmbeddingResponse { } /// Individual embedding data from OpenAI response -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct EmbeddingData { pub object: String, pub embedding: Vec, @@ -193,7 +193,7 @@ pub struct EmbeddingData { } /// Token usage information from OpenAI API -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Usage { pub prompt_tokens: usize, pub total_tokens: usize, @@ -210,7 +210,7 @@ impl std::fmt::Display for Usage { } /// Response structure for OpenAI completion API -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct CompletionResponse { pub id: String, pub object: String, @@ -221,8 +221,9 @@ pub struct CompletionResponse { } impl CompletionResponse { - fn try_into(mut self) -> Result { + fn try_into(mut self, mut full_history: Vec) -> Result { let choice = self.choices.pop().ok_or("No completion choice")?; + full_history.push(json!(choice.message)); let mut output = AgentOutput { content: choice.message.content, tool_calls: choice.message.tool_calls.map(|tools| { @@ -236,6 +237,7 @@ impl CompletionResponse { }) .collect() }), + full_history: Some(full_history), ..Default::default() }; @@ -250,14 +252,14 @@ impl CompletionResponse { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Choice { pub index: usize, pub message: MessageOutput, pub finish_reason: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct MessageOutput { pub role: String, #[serde(default)] @@ -266,7 +268,7 @@ pub struct MessageOutput { pub tool_calls: Option>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ToolCallOutput { pub id: String, pub r#type: String, @@ -288,7 +290,7 @@ impl From for ToolDefinition { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Function { pub name: String, pub arguments: String, @@ -425,7 +427,7 @@ impl CompletionFeaturesDyn for CompletionModel { Box::pin(async move { // Add preamble to chat history (if available) let mut full_history = if let Some(system) = &req.system { - vec![Message { + vec![json!(Message { role: if is_new { "developer".into() } else { @@ -434,33 +436,35 @@ impl CompletionFeaturesDyn for CompletionModel { content: system.to_owned().into(), name: req.system_name.clone(), ..Default::default() - }] + })] } else { vec![] }; // Add context documents to chat history if !req.documents.is_empty() { - full_history.push(Message { + full_history.push(json!(Message { role: "user".into(), content: format!("{}", req.documents).into(), ..Default::default() - }); + })); } // Extend existing chat history full_history.append(&mut req.chat_history); - full_history.push(Message { - role: "user".into(), - content: req.prompt.into(), - name: req.prompter_name, - ..Default::default() - }); + if !req.prompt.is_empty() { + full_history.push(json!(Message { + role: "user".into(), + content: req.prompt.into(), + name: req.prompter_name, + ..Default::default() + })); + } let mut body = json!({ "model": model, - "messages": full_history, + "messages": full_history.clone(), "temperature": req.temperature, }); let body = body.as_object_mut().unwrap(); @@ -502,14 +506,21 @@ impl CompletionFeaturesDyn for CompletionModel { if log_enabled!(Debug) { if let Ok(val) = serde_json::to_string(&body) { - log::debug!("DeepSeek request: {}", val); + log::debug!(request = val; "OpenAI completions request"); } } let response = client.post("/chat/completions").json(body).send().await?; if response.status().is_success() { match response.json::().await { - Ok(res) => res.try_into(), + Ok(res) => { + if log_enabled!(Debug) { + if let Ok(val) = serde_json::to_string(&res) { + log::debug!(response = val; "OpenAI completions response"); + } + } + res.try_into(full_history) + } Err(err) => Err(format!("OpenAI completions error: {}", err).into()), } } else { diff --git a/anda_engine_cli/Cargo.toml b/anda_engine_cli/Cargo.toml new file mode 100644 index 0000000..4a1a481 --- /dev/null +++ b/anda_engine_cli/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "anda_engine_cli" +description = "The command line interface for Anda engine server." +repository = "https://github.com/ldclabs/anda/tree/main/anda_engine_cli" +publish = false +version = "0.1.0" +edition.workspace = true +keywords.workspace = true +categories.workspace = true +license.workspace = true + +[dependencies] +anda_core = { path = "../anda_core", version = "0.4" } +anda_engine = { path = "../anda_engine", version = "0.4" } +anda_web3_client = { path = "../anda_web3_client", version = "0.1" } +base64 = { workspace = true } +clap = { workspace = true } +ciborium = { workspace = true } +dotenv = { workspace = true } +structured-logger = { workspace = true } +tokio = { workspace = true } +const-hex = { workspace = true } +rand = { workspace = true } +ic_cose_types = { workspace = true } +ic_tee_agent = { workspace = true } + +[dev-dependencies] diff --git a/anda_engine_cli/README.md b/anda_engine_cli/README.md new file mode 100644 index 0000000..cf7ba4c --- /dev/null +++ b/anda_engine_cli/README.md @@ -0,0 +1,21 @@ +# `anda_engine_cli` + +An AI agent example to interact with ICP blockchain ledgers. + +## Running locally + +```sh +git clone https://github.com/ldclabs/anda.git +cd anda +cp example.env .env +# update .env +cargo build -p anda_engine_cli +./target/debug/anda_engine_cli agent-run -p 'Please check my PANDA balance' +``` + +## License +Copyright © 2025 [LDC Labs](https://github.com/ldclabs). + +`ldclabs/anda` is licensed under the MIT License. See the [MIT license][license] for the full license text. + +[license]: ./../LICENSE-MIT diff --git a/anda_engine_cli/src/main.rs b/anda_engine_cli/src/main.rs new file mode 100644 index 0000000..3afd42c --- /dev/null +++ b/anda_engine_cli/src/main.rs @@ -0,0 +1,92 @@ +use anda_core::{AgentOutput, BoxError}; +use anda_engine::context::Web3ClientFeatures; +use anda_web3_client::client::Client as Web3Client; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use ciborium::from_reader; +use clap::{Parser, Subcommand}; +use ic_cose_types::to_cbor_bytes; +use rand::{thread_rng, RngCore}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[clap(short, long, default_value = "https://icp-api.io")] + ic_host: String, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +pub enum Commands { + RandBytes { + #[arg(short, long, default_value = "32")] + len: usize, + + #[arg(short, long, default_value = "hex")] + format: String, + }, + AgentRun { + #[arg(short, long, default_value = "http://127.0.0.1:8042/default")] + endpoint: String, + + #[arg(short, long, env = "ID_SECRET")] + id_secret: String, + + #[arg(short, long)] + prompt: String, + + #[arg(short, long)] + name: Option, + }, +} + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + dotenv::dotenv().ok(); + let cli = Cli::parse(); + + match &cli.command { + Some(Commands::RandBytes { len, format }) => { + let mut rng = thread_rng(); + let mut bytes = vec![0u8; (*len).min(1024)]; + rng.fill_bytes(&mut bytes); + match format.as_str() { + "hex" => { + println!("{}", const_hex::encode(&bytes)); + } + "base64" => { + println!("{}", BASE64_URL_SAFE_NO_PAD.encode(&bytes)); + } + _ => { + println!("{:?}", bytes); + } + } + } + + Some(Commands::AgentRun { + endpoint, + id_secret, + prompt, + name, + }) => { + let id_secret = const_hex::decode(id_secret)?; + let id_secret: [u8; 32] = id_secret.try_into().map_err(|_| "invalid id_secret")?; + let web3 = + Web3Client::new(&cli.ic_host, id_secret, [0u8; 48], None, Some(true)).await?; + println!("principal: {}", web3.get_principal()); + let params = to_cbor_bytes(&(&name, &prompt, None::>)); + let res = web3 + .https_signed_rpc_raw(endpoint.to_owned(), "agent_run".to_string(), params) + .await?; + let res: AgentOutput = from_reader(&res[..])?; + println!("{:?}", res); + } + + None => { + println!("no command"); + } + } + + Ok(()) +} diff --git a/anda_engine_server/README.md b/anda_engine_server/README.md index a1835f2..54743e6 100644 --- a/anda_engine_server/README.md +++ b/anda_engine_server/README.md @@ -2,7 +2,7 @@ A http server to serve multiple Anda engines. -Example: https://github.com/ldclabs/anda/tree/main/examples/icp_ledger_agent +Example: https://github.com/ldclabs/anda/blob/main/examples/icp_ledger_agent/src/main.rs ## License Copyright © 2025 [LDC Labs](https://github.com/ldclabs). diff --git a/anda_engine_server/src/handler.rs b/anda_engine_server/src/handler.rs index 42a9440..6c0371b 100644 --- a/anda_engine_server/src/handler.rs +++ b/anda_engine_server/src/handler.rs @@ -71,6 +71,12 @@ pub async fn anda_engine( match ct { Content::CBOR(req, _) => { + log::info!( + method = req.method.as_str(), + agent = id.to_text(), + caller = caller.to_text(); + "anda_engine", + ); let res = engine_run(&req, &app, caller, id).await; Content::CBOR(res, None).into_response() } @@ -91,11 +97,12 @@ async fn engine_run( match req.method.as_str() { "agent_run" => { - let args: (String, String, Option) = from_reader(req.params.as_slice()) - .map_err(|err| format!("failed to decode params: {err:?}"))?; + let args: (Option, String, Option) = + from_reader(req.params.as_slice()) + .map_err(|err| format!("failed to decode params: {err:?}"))?; let res = engine .agent_run( - Some(args.0), + args.0, args.1, args.2.map(|v| v.into_vec()), None, diff --git a/anda_web3_client/src/client.rs b/anda_web3_client/src/client.rs index bdad2c7..1dee774 100644 --- a/anda_web3_client/src/client.rs +++ b/anda_web3_client/src/client.rs @@ -36,6 +36,7 @@ pub struct Client { identity: Arc, agent: Arc, cose_canister: Principal, + is_dev: bool, } impl Client { @@ -53,15 +54,17 @@ impl Client { id_secret: [u8; 32], root_secret: [u8; 48], cose_canister: Option, + is_dev: Option, ) -> Result { + let is_dev = is_dev.unwrap_or(false); let outer_http = reqwest::Client::builder() .use_rustls_tls() - .https_only(true) + .https_only(!is_dev) .http2_keep_alive_interval(Some(Duration::from_secs(25))) .http2_keep_alive_timeout(Duration::from_secs(15)) .http2_keep_alive_while_idle(true) .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(120)) + .timeout(Duration::from_secs(180)) .user_agent(APP_USER_AGENT) .build() .expect("Anda web3 client should build"); @@ -85,6 +88,7 @@ impl Client { identity, agent: Arc::new(agent), cose_canister: cose_canister.unwrap_or(Principal::anonymous()), + is_dev, }) } @@ -351,8 +355,9 @@ impl Web3ClientFeatures for Client { } let outer_http = self.outer_http.clone(); + let is_dev = self.is_dev; Box::pin(async move { - if !url.starts_with("https://") { + if !is_dev && !url.starts_with("https://") { return Err("Invalid URL, must start with https://".into()); } let mut req = outer_http.request(method, url); diff --git a/example.env b/example.env index b60dcf9..0d15852 100644 --- a/example.env +++ b/example.env @@ -2,4 +2,5 @@ LOG_LEVEL=debug ID_SECRET=0000000000000000000000000000000000000000000000000000000000000000 ROOT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 CHARACTER_FILE_PATH='agents/anda_bot/nitro_enclave/Character.toml' -CONFIG_FILE_PATH='agents/anda_bot/nitro_enclave/Config.toml' \ No newline at end of file +CONFIG_FILE_PATH='agents/anda_bot/nitro_enclave/Config.toml' +DEEPSEEK_API_KEY='' \ No newline at end of file diff --git a/examples/icp_ledger_agent/README.md b/examples/icp_ledger_agent/README.md index f3b7c7d..2748512 100644 --- a/examples/icp_ledger_agent/README.md +++ b/examples/icp_ledger_agent/README.md @@ -7,12 +7,19 @@ An AI agent example to interact with ICP blockchain ledgers. ```sh git clone https://github.com/ldclabs/anda.git cd anda -mkdir -p object_store cp example.env .env # update .env cargo run -p icp_ledger_agent ``` +Build CLI tool and run agent: +```sh +cargo build -p anda_engine_cli +./target/debug/anda_engine_cli agent-run -p 'Please check my PANDA balance' +``` + +**Notice**: The current version of the deepseek-chat model's Function Calling capabilitity is unstable. https://api-docs.deepseek.com/guides/function_calling + ## License Copyright © 2025 [LDC Labs](https://github.com/ldclabs). diff --git a/examples/icp_ledger_agent/src/agent.rs b/examples/icp_ledger_agent/src/agent.rs index e57a56b..eaeddbe 100644 --- a/examples/icp_ledger_agent/src/agent.rs +++ b/examples/icp_ledger_agent/src/agent.rs @@ -61,7 +61,7 @@ impl Agent for ICPLedgerAgent { let req = CompletionRequest { system: Some( "\ - You are an AI assistant designed to interact with the ICP blockchain ledger.\n\ + You are an AI assistant designed to interact with the ICP blockchain ledger by given tools.\n\ 1. Please decline any requests that are not related to the ICP blockchain ledger.\n\ 2. For requests that are not supported by the tools available, kindly inform the user \ of your current capabilities." diff --git a/examples/icp_ledger_agent/src/main.rs b/examples/icp_ledger_agent/src/main.rs index f0d38d3..a2fd33e 100644 --- a/examples/icp_ledger_agent/src/main.rs +++ b/examples/icp_ledger_agent/src/main.rs @@ -39,6 +39,16 @@ struct Cli { #[arg(long, env = "DEEPSEEK_API_KEY")] deepseek_api_key: String, + + #[arg( + long, + env = "DEEPSEEK_ENDPOINT", + default_value = "https://api.deepseek.com" + )] + deepseek_endpoint: String, + + #[arg(long, env = "DEEPSEEK_MODEL", default_value = "deepseek-chat")] + deepseek_model: String, } // cargo run -p icp_ledger_agent @@ -58,7 +68,7 @@ async fn main() -> Result<(), BoxError> { let root_secret = const_hex::decode(&cli.root_secret)?; let root_secret: [u8; 48] = root_secret.try_into().map_err(|_| "invalid root_secret")?; - let web3 = Web3Client::new(&cli.ic_host, id_secret, root_secret, None).await?; + let web3 = Web3Client::new(&cli.ic_host, id_secret, root_secret, None, Some(true)).await?; let my_principal = web3.get_principal(); log::info!( "start local service, principal: {:?}", @@ -67,11 +77,11 @@ async fn main() -> Result<(), BoxError> { // LL Models let model = Model::new( - Arc::new(NotImplemented), Arc::new( - deepseek::Client::new(&cli.deepseek_api_key, None) - .completion_model(deepseek::DEEKSEEK_V3), + deepseek::Client::new(&cli.deepseek_api_key, Some(cli.deepseek_endpoint)) + .completion_model(&cli.deepseek_model), ), + Arc::new(NotImplemented), ); // ObjectStore @@ -103,7 +113,7 @@ async fn main() -> Result<(), BoxError> { .with_app_version(APP_VERSION.to_string()) .with_addr(format!("127.0.0.1:{}", cli.port)) .with_engines(engines, None) - .serve(shutdown_signal(global_cancel_token, Duration::from_secs(5))) + .serve(shutdown_signal(global_cancel_token, Duration::from_secs(3))) .await?; Ok(()) diff --git a/tools/anda_icp/Cargo.toml b/tools/anda_icp/Cargo.toml index 8316d69..cc5e4c8 100644 --- a/tools/anda_icp/Cargo.toml +++ b/tools/anda_icp/Cargo.toml @@ -19,6 +19,7 @@ serde_bytes = { workspace = true } num-traits = { workspace = true } tokio = { workspace = true } schemars = { workspace = true } +log = { workspace = true } icrc-ledger-types = "0.1" [dev-dependencies] diff --git a/tools/anda_icp/src/ledger/mod.rs b/tools/anda_icp/src/ledger/mod.rs index 3f64322..ddd755b 100644 --- a/tools/anda_icp/src/ledger/mod.rs +++ b/tools/anda_icp/src/ledger/mod.rs @@ -171,6 +171,13 @@ impl ICPLedgers { },), ) .await?; + log::info!( + account = args.account, + symbol = args.symbol, + amount = args.amount, + result = res.is_ok(); + "icrc1_transfer", + ); res.map_err(|err| format!("failed to transfer tokens, error: {:?}", err).into()) } @@ -203,6 +210,12 @@ impl ICPLedgers { .await?; let amount = res.0.to_f64().unwrap_or_default() / 10u64.pow(*decimals as u32) as f64; + log::info!( + account = args.account, + symbol = args.symbol, + balance = amount; + "balance_of", + ); Ok(amount) } }