agents_runtime/providers/
anthropic.rs

1use agents_core::llm::{LanguageModel, LlmRequest, LlmResponse};
2use agents_core::messaging::{AgentMessage, MessageContent, MessageRole};
3use agents_core::tools::ToolSchema;
4use async_trait::async_trait;
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Clone)]
10pub struct AnthropicConfig {
11    pub api_key: String,
12    pub model: String,
13    pub max_output_tokens: u32,
14    pub api_url: Option<String>,
15    pub api_version: Option<String>,
16}
17
18pub struct AnthropicMessagesModel {
19    client: Client,
20    config: AnthropicConfig,
21}
22
23impl AnthropicMessagesModel {
24    pub fn new(config: AnthropicConfig) -> anyhow::Result<Self> {
25        Ok(Self {
26            client: Client::builder()
27                .user_agent("rust-deep-agents-sdk/0.1")
28                .build()?,
29            config,
30        })
31    }
32}
33
34#[derive(Serialize)]
35struct AnthropicRequest {
36    model: String,
37    max_tokens: u32,
38    system: String,
39    messages: Vec<AnthropicMessage>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    tools: Option<Vec<AnthropicTool>>,
42}
43
44#[derive(Serialize)]
45struct AnthropicTool {
46    name: String,
47    description: String,
48    input_schema: Value,
49}
50
51#[derive(Serialize)]
52struct AnthropicMessage {
53    role: String,
54    content: Vec<AnthropicContentBlock>,
55}
56
57#[derive(Serialize)]
58struct AnthropicContentBlock {
59    #[serde(rename = "type")]
60    kind: &'static str,
61    text: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    cache_control: Option<AnthropicCacheControl>,
64}
65
66#[derive(Serialize)]
67struct AnthropicCacheControl {
68    #[serde(rename = "type")]
69    cache_type: String,
70}
71
72#[derive(Deserialize)]
73struct AnthropicResponse {
74    content: Vec<AnthropicResponseBlock>,
75}
76
77#[derive(Deserialize)]
78struct AnthropicResponseBlock {
79    #[serde(rename = "type")]
80    kind: String,
81    text: Option<String>,
82    #[allow(dead_code)]
83    id: Option<String>,
84    name: Option<String>,
85    input: Option<Value>,
86}
87
88fn to_anthropic_messages(request: &LlmRequest) -> (String, Vec<AnthropicMessage>) {
89    let mut system_prompt = request.system_prompt.clone();
90    let mut messages = Vec::new();
91
92    for message in &request.messages {
93        let text = match &message.content {
94            MessageContent::Text(text) => text.clone(),
95            MessageContent::Json(value) => value.to_string(),
96        };
97
98        // Handle system messages specially - they should be part of the system prompt
99        if matches!(message.role, MessageRole::System) {
100            if !system_prompt.is_empty() {
101                system_prompt.push_str("\n\n");
102            }
103            system_prompt.push_str(&text);
104            continue;
105        }
106
107        let role = match message.role {
108            MessageRole::User => "user",
109            MessageRole::Agent => "assistant",
110            MessageRole::Tool => "user",
111            MessageRole::System => unreachable!(), // Handled above
112        };
113
114        // Convert cache control if present
115        let cache_control = message
116            .metadata
117            .as_ref()
118            .and_then(|meta| meta.cache_control.as_ref())
119            .map(|cc| AnthropicCacheControl {
120                cache_type: cc.cache_type.clone(),
121            });
122
123        messages.push(AnthropicMessage {
124            role: role.to_string(),
125            content: vec![AnthropicContentBlock {
126                kind: "text",
127                text,
128                cache_control,
129            }],
130        });
131    }
132
133    (system_prompt, messages)
134}
135
136/// Convert tool schemas to Anthropic tool format
137fn to_anthropic_tools(tools: &[ToolSchema]) -> Option<Vec<AnthropicTool>> {
138    if tools.is_empty() {
139        return None;
140    }
141
142    Some(
143        tools
144            .iter()
145            .map(|tool| AnthropicTool {
146                name: tool.name.clone(),
147                description: tool.description.clone(),
148                input_schema: serde_json::to_value(&tool.parameters)
149                    .unwrap_or_else(|_| serde_json::json!({})),
150            })
151            .collect(),
152    )
153}
154
155#[async_trait]
156impl LanguageModel for AnthropicMessagesModel {
157    async fn generate(&self, request: LlmRequest) -> anyhow::Result<LlmResponse> {
158        let (system_prompt, messages) = to_anthropic_messages(&request);
159        let tools = to_anthropic_tools(&request.tools);
160
161        // Debug logging
162        tracing::debug!(
163            "Anthropic request: model={}, messages={}, tools={}",
164            self.config.model,
165            messages.len(),
166            tools.as_ref().map(|t| t.len()).unwrap_or(0)
167        );
168
169        let body = AnthropicRequest {
170            model: self.config.model.clone(),
171            max_tokens: self.config.max_output_tokens,
172            system: system_prompt,
173            messages,
174            tools,
175        };
176
177        let url = self
178            .config
179            .api_url
180            .as_deref()
181            .unwrap_or("https://api.anthropic.com/v1/messages");
182        let version = self.config.api_version.as_deref().unwrap_or("2023-06-01");
183
184        let response = self
185            .client
186            .post(url)
187            .header("x-api-key", &self.config.api_key)
188            .header("anthropic-version", version)
189            .json(&body)
190            .send()
191            .await?
192            .error_for_status()?;
193
194        let data: AnthropicResponse = response.json().await?;
195
196        // Check if response contains tool_use blocks
197        let tool_uses: Vec<_> = data
198            .content
199            .iter()
200            .filter(|block| block.kind == "tool_use")
201            .collect();
202
203        if !tool_uses.is_empty() {
204            // Convert Anthropic tool_use format to our JSON format
205            let tool_calls: Vec<_> = tool_uses
206                .iter()
207                .filter_map(|block| {
208                    Some(serde_json::json!({
209                        "name": block.name.as_ref()?,
210                        "args": block.input.as_ref()?
211                    }))
212                })
213                .collect();
214
215            tracing::debug!("Anthropic response contains {} tool uses", tool_calls.len());
216
217            return Ok(LlmResponse {
218                message: AgentMessage {
219                    role: MessageRole::Agent,
220                    content: MessageContent::Json(serde_json::json!({
221                        "tool_calls": tool_calls
222                    })),
223                    metadata: None,
224                },
225            });
226        }
227
228        // Regular text response
229        let text = data
230            .content
231            .into_iter()
232            .find_map(|block| (block.kind == "text").then(|| block.text.unwrap_or_default()))
233            .unwrap_or_default();
234
235        Ok(LlmResponse {
236            message: AgentMessage {
237                role: MessageRole::Agent,
238                content: MessageContent::Text(text),
239                metadata: None,
240            },
241        })
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn anthropic_message_conversion_includes_system_prompt() {
251        let request = LlmRequest::new(
252            "You are helpful",
253            vec![AgentMessage {
254                role: MessageRole::User,
255                content: MessageContent::Text("Hello".into()),
256                metadata: None,
257            }],
258        );
259        let (system, messages) = to_anthropic_messages(&request);
260        assert_eq!(system, "You are helpful");
261        assert_eq!(messages.len(), 1);
262        assert_eq!(messages[0].role, "user");
263        assert_eq!(messages[0].content[0].text, "Hello");
264    }
265}