Creating an AI Agent Integrated with ICP
Anda is an AI agent framework built with Rust, enabling developers to create intelligent agents that interact with blockchains, Web3, and other complex systems.
This tutorial guides you through developing a simple AI agent using the Anda framework in just over 100 lines of code, capable of interacting with the ICP (Internet Computer Protocol) blockchain.
We'll use the icp_ledger_agent
example project for demonstration.
The complete code for this tutorial is available here: https://github.com/ldclabs/anda/tree/main/examples/icp_ledger_agent
Note: New to Rust?
This guide assumes basic Rust knowledge and a set-up coding environment. If you're just starting or need to set up your environment, check out these quick guides:
- Getting Started with Rust: https://www.rust-lang.org/learn
- Setting Up Rust with VS Code: https://users.rust-lang.org/t/setting-up-rust-with-vs-code/76907 These resources will help you get up to speed quickly!
1 Running the Example Project
1.1 Cloning the Project
First, clone the anda
repository:
git clone https://github.com/ldclabs/anda.git
cd anda
1.2 Project Structure
The icp_ledger_agent
project is located in the examples/icp_ledger_agent
directory, structured as follows:
icp_ledger_agent/
├── Cargo.toml # Project configuration file, defining dependencies and metadata
├── README.md # Project documentation, including instructions on how to run it
└── src/
├── agent.rs # Defines the `ICPLedgerAgent` struct and its implementation for ICP blockchain interaction
└── main.rs # Entry point of the project, initializes services and starts the AI agent
1.3 Configuring Environment Variables
Before running the project, configure the environment variables. Create a .env
file in the anda
directory and modify it:
LOG_LEVEL=debug
ID_SECRET=0000000000000000000000000000000000000000000000000000000000000000
ROOT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
XAI_API_KEY=''
OPENAI_API_KEY=''
DEEPSEEK_API_KEY=''
ID_SECRET
: A 32-byte hex string or an identity PEM file generated by ICPdfx
tool, serving as the AI agent's identity.ROOT_SECRET
: A 48-byte hex string for deriving cryptographic operations like encryption, decryption, and signing.XAI_API_KEY
: API key for xAI models (if using xAI).OPENAI_API_KEY
: API key for OpenAI models (if using OpenAI).DEEPSEEK_API_KEY
: API key for Deepseek models (currently unavailable).
This project supports xAI's grok-2 or OpenAI's o3-mini models, but not DeepSeek due to unstable Function Calling.
Generate ID_SECRET
and ROOT_SECRET
using anda_engine_cli
:
cargo build -p anda_engine_cli
./target/debug/anda_engine_cli --help
./target/debug/anda_engine_cli rand-bytes --help
Generate 32-byte ID_SECRET
:
./target/debug/anda_engine_cli rand-bytes --len 32
Generate 48-byte ROOT_SECRET
:
./target/debug/anda_engine_cli rand-bytes --len 48
1.4 Running the AI Agent Service
After configuring the environment variables, run the project with:
cargo run -p icp_ledger_agent
This starts the AI agent service, listening on the default port 8042
.
1.5 Calling the Agent
Use anda_engine_cli
to call the AI agent service:
./target/debug/anda_engine_cli agent-run --help
./target/debug/anda_engine_cli agent-run -p 'Please check my PANDA balance'
The AI agent service can include multiple Anda engines, each with multiple agents and tools. The above command calls the default agent in the default Anda engine.
Use a different identity to call the AI agent service if you have dfx
installed:
./target/debug/anda_engine_cli agent-run -i ~/.config/dfx/identity/default/identity.pem -p 'Please transfer 0.1 PANDA tokens to me'
You can also directly call tools to query balances:
./target/debug/anda_engine_cli tool-call -n icp_ledger_balance_of -a '{"account":"535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe","symbol":"PANDA"}'
Code Analysis
2.1 Agent Implementation agent.rs
The agent.rs
file defines the ICPLedgerAgent
struct, implementing the Agent
trait. This agent interacts with the ICP blockchain, including querying balances and transferring tokens.
The ICPLedgerAgent
struct is defined as:
pub struct ICPLedgerAgent {
ledgers: Arc<ICPLedgers>, // ICP ledgers struct
tools: Vec<&'static str>, // List of tools ICPLedgerAgent depends on
}
ICPLedgerAgent
uses the ICPLedgers
struct from the anda_icp
library, which supports querying balances and transferring tokens for multiple tokens.
The tools
method returns tools to register with the Anda engine:
pub fn tools(&self) -> Result<ToolSet<BaseCtx>, BoxError> {
let mut tools = ToolSet::new();
tools.add(BalanceOfTool::new(self.ledgers.clone()))?; // Balance query tool
tools.add(TransferTool::new(self.ledgers.clone()))?; // Transfer tool
Ok(tools)
}
The Agent
trait implementation for ICPLedgerAgent
:
impl Agent<AgentCtx> for ICPLedgerAgent {
/// Returns the agent's name identifier
fn name(&self) -> String {
Self::NAME.to_string()
}
/// Returns a description of the agent's purpose and capabilities.
fn description(&self) -> String {
"Interacts with ICP blockchain ledgers".to_string()
}
/// Returns a list of tool names that this agent depends on
fn tool_dependencies(&self) -> Vec<String> {
self.tools.iter().map(|v| v.to_string()).collect()
}
/// Main execution method for the agent.
async fn run(
&self,
ctx: AgentCtx,
prompt: String,
_attachment: Option<Vec<u8>>,
) -> Result<AgentOutput, BoxError> {
let caller = ctx.caller();
if caller == ANONYMOUS {
return Err("anonymous caller not allowed".into());
}
let req = CompletionRequest {
system: Some(
"\
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."
.to_string(),
),
prompt,
tools: ctx.tool_definitions(Some(&self.tools)),
tool_choice_required: false,
..Default::default()
}
.context("user_address".to_string(), caller.to_string());
let res = ctx.completion(req).await?;
Ok(res)
}
}
Key aspects of the code:
ctx.caller()
retrieves the caller's identity ID, provided by the Anda engine runtime. If it's notANONYMOUS
, it indicates the caller's request has been signature-verified. The Anda framework uses ICP's identity protocol for AI Agent identification.- The
system
field inCompletionRequest
is an optional system prompt that defines the AI Agent's functional purpose. - The
tools
field inCompletionRequest
lists the tools available to the AI Agent. The AI model selects the appropriate tool based on user prompts. context("user_address".to_string(), caller.to_string())
injects the requester's identity ID and can also include other context information, such as knowledge documents.ctx.completion(req).await
executes the AI model's inferring, returning theAgentOutput
struct.
For more on CompletionRequest
, see: https://docs.rs/anda_core/latest/anda_core/model/struct.CompletionRequest.html
Besides ctx.caller
and ctx.completion
, AgentCtx
offers many other useful methods for Agent development. Check the anda_engine
library docs for more: https://docs.rs/anda_engine/latest/anda_engine/context/struct.AgentCtx.html
2.2 Starting the Service main.rs
main.rs
is the project's entry file, initializing the AI Agent. It configures Web3 client, AI model, object storage, and starts the HTTP service to accept external requests.
Key initialization variables:
let global_cancel_token = CancellationToken::new();
let identity = load_identity(&cli.id_secret)?;
let root_secret = const_hex::decode(&cli.root_secret)?;
let root_secret: [u8; 48] = root_secret
.try_into()
.map_err(|_| format!("invalid root_secret: {:?}", cli.root_secret))?;
Key aspects:
global_cancel_token
is a global async cancellation token for canceling all async tasks upon service exit.load_identity
loads the AI Agent's identity defined byID_SECRET
.root_secret
is used for deriving encryption operations inKeysFeatures
. More info: https://docs.rs/anda_core/latest/anda_core/context/trait.KeysFeatures.html.
Initializing Web3Client
:
let web3 = Web3Client::builder()
.with_ic_host(&cli.ic_host)
.with_identity(Arc::new(identity))
.with_root_secret(root_secret)
.build()
.await?;
let my_principal = web3.get_principal();
log::info!(
"start local service, principal: {:?}",
my_principal.to_text()
);
Web3Client
is a functional module of anda_web3_client
, providing encryption operations, external communication, and ICP smart contract interactions for BaseCtx
and AgentCtx
. Due to dependencies on unpublished libraries ic-crypto-secp256k1
and ic-crypto-ed25519
, it cannot be published to https://crates.io/.
For more on anda_web3_client
, see:
- https://docs.rs/anda_engine/latest/anda_engine/context/struct.Web3Client.html
- https://github.com/ldclabs/anda/tree/main/anda_web3_client
In TEE environments, the ic_tee_gateway_sdk
library is used. More info: https://docs.rs/ic_tee_gateway_sdk/latest/ic_tee_gateway_sdk/client/index.html
Building the engine:
let engine = EngineBuilder::new()
.with_id(my_principal)
.with_name(APP_NAME.to_string())
.with_cancellation_token(global_cancel_token.clone())
.with_web3_client(Arc::new(Web3SDK::from_web3(Arc::new(web3.clone()))))
.with_model(model)
.with_store(Store::new(object_store))
.register_tools(agent.tools()?)?
.register_agent(agent)?;
let engine = engine.build(ICPLedgerAgent::NAME.to_string())?;
Key aspects:
with_id(my_principal)
sets the engine's identity ID, i.e., the AI Agent's ID. The engine, as the AI Agent's main body, can include multiple agents and tools.register_tools(agent.tools()?)?
registers available tool sets.register_agent(agent)?
registers an Agent, or useregister_agents
for multiple agents.build(ICPLedgerAgent::NAME.to_string())?
builds the engine, setting the default agent withICPLedgerAgent::NAME
.
For more on EngineBuilder
, see: https://docs.rs/anda_engine/latest/anda_engine/engine/index.html
Starting the Anda engine's HTTP runtime service:
let mut engines = BTreeMap::new();
engines.insert(engine.id(), engine);
ServerBuilder::new()
.with_app_name(APP_NAME.to_string())
.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(3)))
.await?;
Key aspects:
with_engines(engines, None)
: A single HTTP runtime service can host multiple engines.serve(shutdown_signal(global_cancel_token, Duration::from_secs(3)))
implements a graceful shutdown mechanism.
ServerBuilder
, provided by the anda_engine_server
library, implements an HTTP runtime service offering HTTP service interfaces for the Anda engine, including authentication mechanisms for receiving external requests.
Like anda_web3_client
, anda_engine_server
cannot be published to https://crates.io/.
For more on ServerBuilder
, see: https://github.com/ldclabs/anda/tree/main/anda_engine_server
Conclusion
This tutorial has taught you how to develop a simple AI Agent using the Anda framework and interact with the ICP blockchain. You can expand functionalities, such as supporting more blockchain operations or integrating other AI models.
For questions or further assistance, visit https://github.com/ldclabs/anda/discussions.