use std::{future::Future, pin::Pin};
use claudius::{
Anthropic, ContentBlock, KnownModel, Message, MessageCreateParams, MessageParam, MessageRole,
ToolResultBlock, ToolUnionParam, ToolUseBlock,
};
use langcontinuation::{
Tool, ToolCallId, ToolDispatch, Trampoline, Workflow, dispatch_tool_uses, from_env,
generate_goto, live::Executor, push_env,
};
use serde::{Deserialize, Serialize};
struct CalculatorTool;
impl Tool for CalculatorTool {
fn name(&self) -> String {
"add".to_string()
}
fn to_param(&self) -> ToolUnionParam {
ToolUnionParam::new_custom_tool(
"add".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}),
)
}
fn call<'a>(
&'a self,
id: ToolCallId,
tool_use: &'a ToolUseBlock,
) -> Pin<Box<dyn Future<Output = ToolResultBlock> + Send + 'a>> {
let tool_use_id = tool_use.id.clone();
let input = tool_use.input.clone();
Box::pin(async move {
let _ = id;
#[derive(Deserialize)]
struct Args {
a: f64,
b: f64,
}
match serde_json::from_value::<Args>(input) {
Ok(args) => ToolResultBlock::new(tool_use_id)
.with_string_content((args.a + args.b).to_string()),
Err(err) => ToolResultBlock::new(tool_use_id)
.with_string_content(err.to_string())
.with_error(true),
}
})
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct Conversation {
messages: Vec<MessageParam>,
}
fn request(conversation: &Conversation) -> MessageCreateParams {
MessageCreateParams::new(
256,
conversation.messages.clone(),
KnownModel::ClaudeHaiku45.into(),
)
.with_system(
"You can call the `add` tool to add two numbers. Use it for any arithmetic, \
then answer the user in plain text."
.to_string(),
)
.with_tools(vec![CalculatorTool.to_param()])
}
generate_goto! {
fn entry(workflow: &mut Workflow, question: String, continuation: Continuation) -> Result<ContinuationChoice, handled::SError> {
let mut conversation = Conversation::default();
conversation
.messages
.push(MessageParam::new_with_string(question, MessageRole::User));
let req = request(&conversation);
push_env!(workflow.conversation: Conversation = conversation);
Ok(continuation.anthropic("anthropic", req, "response: Message", "receive"))
}
}
generate_goto! {
async fn receive(
workflow: &mut Workflow,
conversation: Conversation,
response: Message,
continuation: Continuation
) -> Result<ContinuationChoice, handled::SError> {
let mut conversation = conversation;
conversation.messages.push(MessageParam::from(response.clone()));
push_env!(workflow.conversation: Conversation = conversation);
match dispatch_tool_uses(continuation, &response, "tool_results: ToolResults", "after_tools") {
ToolDispatch::Tools(choice) => Ok(choice),
ToolDispatch::Done(continuation) => {
let text = response
.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
push_env!(workflow.answer: String = text);
Ok(continuation.halt())
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct ToolResults(Vec<ToolResultBlock>);
generate_goto! {
fn after_tools(
workflow: &mut Workflow,
conversation: Conversation,
tool_results: ToolResults,
continuation: Continuation
) -> Result<ContinuationChoice, handled::SError> {
let mut conversation = conversation;
let blocks: Vec<ContentBlock> = tool_results
.0
.into_iter()
.map(ContentBlock::ToolResult)
.collect();
conversation
.messages
.push(MessageParam::new_with_blocks(blocks, MessageRole::User));
let req = request(&conversation);
push_env!(workflow.conversation: Conversation = conversation);
Ok(continuation.anthropic("anthropic", req, "response: Message", "receive"))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut workflow = Workflow::new("durable-tool-call", "entry");
push_env!(workflow.question: String = "What is 21 plus 21?".to_string());
let mut trampoline = Trampoline::default();
trampoline.register("entry", entry);
trampoline.register("receive", receive);
trampoline.register("after_tools", after_tools);
trampoline.register_tool(CalculatorTool);
let anthropic = Anthropic::new(None)?;
let executor = Executor::new(trampoline).with_anthropic("anthropic", anthropic);
let workflow = executor.run_workflow(workflow).await?;
from_env!(let answer: String = workflow.lookup());
println!("{answer}");
Ok(())
}