use letta::client::ClientBuilder;
use letta::error::LettaResult;
use letta::types::agent::CreateAgentRequest;
use letta::types::memory::Block;
use letta::types::tool::{CreateToolRequest, ListToolsParams, SourceType, Tool, UpdateToolRequest};
use letta::{LettaClient, LettaId};
use serial_test::serial;
fn create_test_client() -> LettaResult<LettaClient> {
ClientBuilder::new()
.base_url("http://localhost:8283")
.build()
}
async fn create_test_agent(client: &LettaClient) -> LettaResult<LettaId> {
let request = CreateAgentRequest::builder()
.name("Test Tools Agent")
.model("letta/letta-free")
.embedding("letta/letta-free")
.memory_block(Block {
id: None,
label: "human".to_string(),
value: "The human's name is Test User.".to_string(),
limit: Some(1000),
is_template: false,
preserve_on_migration: true,
read_only: false,
description: Some("Human information".to_string()),
metadata: None,
name: None,
organization_id: None,
created_by_id: None,
last_updated_by_id: None,
created_at: None,
updated_at: None,
})
.build();
let agent = client.agents().create(request).await?;
Ok(agent.id)
}
async fn create_test_tool(client: &LettaClient, base_name: &str) -> LettaResult<Tool> {
let unique_name = format!(
"{}_{}",
base_name,
chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let request = CreateToolRequest {
description: Some(format!("Test tool: {}", base_name)),
source_code: format!(
r#"def {}(message: str) -> str:
"""
Echo the provided message.
This test function takes a message and returns it with an 'Echo: ' prefix.
Args:
message: The message to echo back
Returns:
str: The echoed message with 'Echo: ' prefix
"""
return f"Echo: {{message}}""#,
unique_name
),
source_type: Some(SourceType::Python),
json_schema: Some(serde_json::json!({
"name": unique_name,
"description": format!("Test tool: {}", base_name),
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Message to echo"
}
},
"required": ["message"]
}
})),
tags: Some(vec!["test".to_string()]),
return_char_limit: Some(1000),
pip_requirements: None,
args_json_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Message to echo"
}
},
"required": ["message"]
})),
};
println!(
"Sending request: {}",
serde_json::to_string_pretty(&request).unwrap()
);
client.tools().create(request).await
}
#[tokio::test]
#[serial]
async fn test_create_tool() -> LettaResult<()> {
let client = create_test_client()?;
let tool = create_test_tool(&client, "echo_tool").await?;
assert!(tool.name.starts_with("echo_tool_"));
assert!(tool.id.is_some());
assert_eq!(tool.source_type, Some(SourceType::Python));
assert!(tool.description.as_ref().unwrap().contains("Test tool"));
if let Some(id) = &tool.id {
client.tools().delete(id).await?;
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_list_tools() -> LettaResult<()> {
let client = create_test_client()?;
let tool1 = create_test_tool(&client, "list_test_1").await?;
let tool2 = create_test_tool(&client, "list_test_2").await?;
let params = ListToolsParams {
limit: Some(100),
..Default::default()
};
let tools = client.tools().list(Some(params)).await?;
eprintln!("Found {} tools total", tools.len());
eprintln!("Looking for: {} and {}", tool1.name, tool2.name);
let retrieved_tool1 = client.tools().get(tool1.id.as_ref().unwrap()).await?;
let retrieved_tool2 = client.tools().get(tool2.id.as_ref().unwrap()).await?;
assert_eq!(retrieved_tool1.name, tool1.name);
assert_eq!(retrieved_tool2.name, tool2.name);
let params = ListToolsParams {
limit: Some(5),
..Default::default()
};
let limited_tools = client.tools().list(Some(params)).await?;
assert!(limited_tools.len() <= 5);
if let Some(id) = &tool1.id {
client.tools().delete(id).await?;
}
if let Some(id) = &tool2.id {
client.tools().delete(id).await?;
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_get_tool() -> LettaResult<()> {
let client = create_test_client()?;
let created_tool = create_test_tool(&client, "get_test").await?;
let tool_id = created_tool.id.as_ref().unwrap();
let retrieved_tool = client.tools().get(tool_id).await?;
assert!(retrieved_tool.name.starts_with("get_test_"));
assert_eq!(retrieved_tool.id, created_tool.id);
client.tools().delete(tool_id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_update_tool() -> LettaResult<()> {
let client = create_test_client()?;
let tool = create_test_tool(&client, "update_test").await?;
let tool_id = tool.id.as_ref().unwrap();
let update_request = UpdateToolRequest {
description: Some("Updated description".to_string()),
tags: Some(vec!["test".to_string(), "updated".to_string()]),
return_char_limit: Some(2000),
..Default::default()
};
let updated_tool = client.tools().update(tool_id, update_request).await?;
assert_eq!(
updated_tool.description,
Some("Updated description".to_string())
);
assert_eq!(updated_tool.return_char_limit, Some(2000));
let tags = updated_tool.tags.unwrap_or_default();
assert!(tags.contains(&"updated".to_string()));
client.tools().delete(tool_id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_delete_tool() -> LettaResult<()> {
let client = create_test_client()?;
let tool = create_test_tool(&client, "delete_test").await?;
let tool_id = tool.id.as_ref().unwrap();
client.tools().delete(tool_id).await?;
let result = client.tools().get(tool_id).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
#[serial]
async fn test_tool_count() -> LettaResult<()> {
let client = create_test_client()?;
let initial_count = client.tools().count().await?;
println!("Initial tool count: {}", initial_count);
let tool = create_test_tool(&client, "count_test").await?;
println!("Created tool with id: {:?}", tool.id);
if let Some(id) = &tool.id {
let retrieved = client.tools().get(id).await;
println!("Tool retrieval result: {:?}", retrieved.is_ok());
}
let all_tools = client.tools().list(None).await?;
let our_tool = all_tools.iter().find(|t| t.id == tool.id);
println!("Found our tool in list: {}", our_tool.is_some());
println!("Total tools in list: {}", all_tools.len());
let new_count = client.tools().count().await?;
println!("New tool count: {}", new_count);
assert!(
tool.id.is_some(),
"Tool should have been created with an ID"
);
if let Some(id) = &tool.id {
client.tools().delete(id).await?;
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_agent_tools() -> LettaResult<()> {
let client = create_test_client()?;
let agent_id = create_test_agent(&client).await?;
let tool = create_test_tool(&client, "agent_tool_test").await?;
let tool_id = tool.id.as_ref().unwrap();
let initial_tools = client.memory().list_agent_tools(&agent_id).await?;
let initial_custom_tools: Vec<_> = initial_tools
.iter()
.filter(|t| t.name.starts_with("agent_tool_test_"))
.collect();
assert_eq!(initial_custom_tools.len(), 0);
let updated_agent = client
.memory()
.attach_tool_to_agent(&agent_id, tool_id)
.await?;
assert_eq!(updated_agent.id, agent_id);
let tools_after_attach = client.memory().list_agent_tools(&agent_id).await?;
let attached_tools: Vec<_> = tools_after_attach
.iter()
.filter(|t| t.name.starts_with("agent_tool_test_"))
.collect();
assert_eq!(attached_tools.len(), 1);
let _updated_agent = client
.memory()
.detach_tool_from_agent(&agent_id, tool_id)
.await?;
let tools_after_detach = client.memory().list_agent_tools(&agent_id).await?;
let detached_tools: Vec<_> = tools_after_detach
.iter()
.filter(|t| t.name.starts_with("agent_tool_test_"))
.collect();
assert_eq!(detached_tools.len(), 0);
client.agents().delete(&agent_id).await?;
client.tools().delete(tool_id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_run_tool_from_source() -> LettaResult<()> {
let client = create_test_client()?;
let request = letta::RunToolFromSourceRequest {
source_code: r#"
def add_numbers(a: float, b: float) -> float:
"""Add two numbers together.
Args:
a: The first number
b: The second number
Returns:
float: The sum of a and b
"""
return a + b
"#
.to_string(),
args: serde_json::json!({ "a": 5.0, "b": 3.0 }),
source_type: Some(SourceType::Python),
args_json_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "The first number"
},
"b": {
"type": "number",
"description": "The second number"
}
},
"required": ["a", "b"]
})),
json_schema: Some(serde_json::json!({
"name": "add_numbers",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "The first number"
},
"b": {
"type": "number",
"description": "The second number"
}
},
"required": ["a", "b"]
}
})),
name: Some("add_numbers".to_string()),
..Default::default()
};
let result = client.tools().run_from_source(request).await?;
assert_eq!(result.status, letta::ToolExecutionStatus::Success);
assert_eq!(result.tool_return, "8.0");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_upsert_base_tools() {
let client = create_test_client().unwrap();
let initial_count = client.tools().count().await.unwrap_or(0);
println!("Initial tool count: {}", initial_count);
let tools = client
.tools()
.upsert_base_tools()
.await
.expect("Should have some base tools");
println!("Upserted {} base tools", tools.len());
let tool_names: Vec<String> = tools.iter().map(|t| t.name.clone()).collect();
println!("Base tools include:");
for (i, name) in tool_names.iter().enumerate() {
if i < 10 {
println!(" - {}", name);
}
}
}