Skip to main content

agentik_sdk/resources/
messages.rs

1use crate::client::Anthropic;
2use crate::http::streaming::{StreamConfig, StreamRequestBuilder};
3use crate::streaming::MessageStream;
4use crate::types::errors::{AnthropicError, Result};
5use crate::types::messages::*;
6
7/// Messages API resource for interacting with Claude
8pub struct MessagesResource<'a> {
9    client: &'a Anthropic,
10}
11
12impl<'a> MessagesResource<'a> {
13    /// Create a new Messages resource
14    pub fn new(client: &'a Anthropic) -> Self {
15        Self { client }
16    }
17
18    /// Create a message with Claude
19    ///
20    /// Send a structured list of input messages with text and/or image content,
21    /// and Claude will generate the next message in the conversation.
22    ///
23    /// # Example
24    ///
25    /// ```rust,no_run
26    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27    /// use agentik_sdk::{Anthropic, types::MessageCreateBuilder};
28    ///
29    /// let client = Anthropic::from_env()?;
30    ///
31    /// let message = client.messages().create(
32    ///     MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
33    ///         .user("Hello, Claude!")
34    ///         .build()
35    /// ).await?;
36    ///
37    /// println!("Claude responded: {:?}", message.content);
38    /// # Ok(())
39    /// # }
40    /// ```
41    pub async fn create(&self, params: MessageCreateParams) -> Result<Message> {
42        let url = self.client.http_client().build_url("/v1/messages");
43
44        let request = self
45            .client
46            .http_client()
47            .post(&url)
48            .json(&params)
49            .build()
50            .map_err(|e| AnthropicError::Connection {
51                message: e.to_string(),
52            })?;
53
54        let response = self.client.http_client().send(request).await?;
55
56        // Extract request ID from headers
57        let request_id = self.client.http_client().extract_request_id(&response);
58
59        let status = response.status().as_u16();
60        let body = response.text().await.map_err(|e| AnthropicError::from_status(status, format!(
61            "failed to read response body: {e}"
62        )))?;
63
64        let mut message: Message = serde_json::from_str(&body)
65            .map_err(|e| AnthropicError::from_status(status, format!(
66                "failed to parse response as JSON: {e}, body: {}",
67                body.chars().take(500).collect::<String>()
68            )))?;
69
70        message.request_id = request_id;
71
72        Ok(message)
73    }
74
75    /// Create a streaming message with Claude
76    ///
77    /// Send a message request and receive a real-time stream of the response.
78    /// This allows you to process Claude's response as it's being generated.
79    ///
80    /// # Example
81    ///
82    /// ```ignore
83    /// use agentik_sdk::{Anthropic, MessageCreateBuilder};
84    /// use futures::StreamExt;
85    ///
86    /// let client = Anthropic::from_env()?;
87    ///
88    /// let stream = client.messages().create_stream(
89    ///     MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
90    ///         .user("Write a story about AI")
91    ///         .stream(true)
92    ///         .build()
93    /// ).await?;
94    ///
95    /// // Option 1: Use callbacks
96    /// let final_message = stream
97    ///     .on_text(|delta, _| print!("{}", delta))
98    ///     .on_error(|error| eprintln!("Error: {}", error))
99    ///     .final_message().await?;
100    ///
101    /// // Option 2: Manual iteration
102    /// while let Some(event) = stream.next().await {
103    ///     // Process each event as needed
104    /// }
105    /// ```
106    pub async fn create_stream(&self, mut params: MessageCreateParams) -> Result<MessageStream> {
107        // Ensure streaming is enabled
108        params.stream = Some(true);
109
110        // Create authorization header - use Bearer for most cases including custom gateways
111        let auth_header = format!("Bearer {}", self.client.config().api_key);
112
113        // Build the streaming request with proper authentication
114        let stream_builder = StreamRequestBuilder::new(
115            self.client.http_client().client().clone(),
116            self.client.config().base_url.clone(),
117        )
118        .header("Authorization", &auth_header)
119        .header("Content-Type", "application/json")
120        .header("anthropic-version", "2023-06-01")
121        .config(StreamConfig::default());
122
123        // Make the streaming request to get the real HTTP stream
124        let http_stream = stream_builder.post_stream("v1/messages", &params).await?;
125
126        // Create MessageStream that processes the real HTTP stream events
127        let message_stream = MessageStream::from_http_stream(http_stream)?;
128
129        Ok(message_stream)
130    }
131
132    /// Create a streaming message using the builder pattern
133    ///
134    /// This is a convenience method that provides an ergonomic API for creating streaming messages.
135    ///
136    /// # Example
137    ///
138    /// ```ignore
139    /// use agentik_sdk::Anthropic;
140    ///
141    /// let client = Anthropic::from_env()?;
142    ///
143    /// let final_message = client.messages()
144    ///     .create_with_builder("claude-3-5-sonnet-latest", 1024)
145    ///     .user("Write a poem about the ocean")
146    ///     .system("You are a creative poet.")
147    ///     .temperature(0.8)
148    ///     .stream()
149    ///     .await?
150    ///     .on_text(|delta, _| print!("{}", delta))
151    ///     .final_message()
152    ///     .await?;
153    /// ```
154    pub async fn stream(&self, params: MessageCreateParams) -> Result<MessageStream> {
155        self.create_stream(params).await
156    }
157
158    /// Create a message using the builder pattern
159    ///
160    /// This is a convenience method that provides an ergonomic API for creating messages.
161    ///
162    /// # Example
163    ///
164    /// ```rust,no_run
165    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
166    /// use agentik_sdk::Anthropic;
167    ///
168    /// let client = Anthropic::from_env()?;
169    ///
170    /// let message = client.messages()
171    ///     .create_with_builder("claude-3-5-sonnet-latest", 1024)
172    ///     .user("What is the capital of France?")
173    ///     .system("You are a helpful geography assistant.")
174    ///     .temperature(0.3)
175    ///     .send()
176    ///     .await?;
177    ///
178    /// println!("Response: {:?}", message.content);
179    /// # Ok(())
180    /// # }
181    /// ```
182    pub fn create_with_builder(
183        &'a self,
184        model: impl Into<String>,
185        max_tokens: u32,
186    ) -> MessageCreateBuilderWithClient<'a> {
187        MessageCreateBuilderWithClient {
188            resource: self,
189            builder: MessageCreateBuilder::new(model, max_tokens),
190        }
191    }
192}
193
194/// A message builder with a client reference for sending requests
195pub struct MessageCreateBuilderWithClient<'a> {
196    resource: &'a MessagesResource<'a>,
197    builder: MessageCreateBuilder,
198}
199
200impl<'a> MessageCreateBuilderWithClient<'a> {
201    /// Add a message to the conversation
202    pub fn message(mut self, role: Role, content: impl Into<MessageContent>) -> Self {
203        self.builder = self.builder.message(role, content);
204        self
205    }
206
207    /// Add a user message
208    pub fn user(mut self, content: impl Into<MessageContent>) -> Self {
209        self.builder = self.builder.user(content);
210        self
211    }
212
213    /// Add an assistant message
214    pub fn assistant(mut self, content: impl Into<MessageContent>) -> Self {
215        self.builder = self.builder.assistant(content);
216        self
217    }
218
219    /// Set the system prompt
220    pub fn system(mut self, system: impl Into<String>) -> Self {
221        self.builder = self.builder.system(system);
222        self
223    }
224
225    /// Set the temperature
226    pub fn temperature(mut self, temperature: f32) -> Self {
227        self.builder = self.builder.temperature(temperature);
228        self
229    }
230
231    /// Set top_p
232    pub fn top_p(mut self, top_p: f32) -> Self {
233        self.builder = self.builder.top_p(top_p);
234        self
235    }
236
237    /// Set top_k
238    pub fn top_k(mut self, top_k: u32) -> Self {
239        self.builder = self.builder.top_k(top_k);
240        self
241    }
242
243    /// Set custom stop sequences
244    pub fn stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
245        self.builder = self.builder.stop_sequences(stop_sequences);
246        self
247    }
248
249    /// Enable streaming
250    pub fn stream(mut self, stream: bool) -> Self {
251        self.builder = self.builder.stream(stream);
252        self
253    }
254
255    /// Send the message request
256    pub async fn send(self) -> Result<Message> {
257        self.resource.create(self.builder.build()).await
258    }
259
260    /// Send the message request as a stream
261    ///
262    /// This enables streaming mode and returns a MessageStream for real-time processing.
263    ///
264    /// # Example
265    ///
266    /// ```ignore
267    /// let stream = client.messages()
268    ///     .create_with_builder("claude-3-5-sonnet-latest", 1024)
269    ///     .user("Tell me a story")
270    ///     .stream_send()
271    ///     .await?;
272    ///
273    /// let final_message = stream
274    ///     .on_text(|delta, _| print!("{}", delta))
275    ///     .final_message()
276    ///     .await?;
277    /// ```
278    pub async fn stream_send(self) -> Result<MessageStream> {
279        let params = self.builder.stream(true).build();
280        self.resource.create_stream(params).await
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::types::messages::{ContentBlockParam, MessageContent};
288
289    #[test]
290    fn test_message_create_params_serialization() {
291        let params = MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
292            .user("Hello, world!")
293            .system("You are helpful")
294            .temperature(0.7)
295            .build();
296
297        let json = serde_json::to_value(&params).unwrap();
298
299        assert_eq!(json["model"], "claude-3-5-sonnet-latest");
300        assert_eq!(json["max_tokens"], 1024);
301        assert_eq!(json["messages"].as_array().unwrap().len(), 1);
302        assert_eq!(json["system"], "You are helpful");
303
304        // Handle floating point precision by checking if the value is close to 0.7
305        let temperature = json["temperature"].as_f64().unwrap();
306        assert!(
307            (temperature - 0.7).abs() < 0.001,
308            "Temperature should be close to 0.7, got {}",
309            temperature
310        );
311    }
312
313    #[test]
314    fn test_complex_message_content() {
315        let content = MessageContent::Blocks(vec![
316            ContentBlockParam::text("Here's an image:"),
317            ContentBlockParam::image_base64("image/jpeg", "base64data"),
318        ]);
319
320        let params = MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
321            .user(content)
322            .build();
323
324        let json = serde_json::to_value(&params).unwrap();
325        let message_content = &json["messages"][0]["content"];
326
327        assert!(message_content.is_array());
328        assert_eq!(message_content.as_array().unwrap().len(), 2);
329        assert_eq!(message_content[0]["type"], "text");
330        assert_eq!(message_content[1]["type"], "image");
331    }
332
333    #[test]
334    fn test_multi_message_conversation() {
335        let params = MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
336            .user("Hello!")
337            .assistant("Hi there! How can I help you?")
338            .user("What's the weather like?")
339            .build();
340
341        assert_eq!(params.messages.len(), 3);
342        assert_eq!(params.messages[0].role, Role::User);
343        assert_eq!(params.messages[1].role, Role::Assistant);
344        assert_eq!(params.messages[2].role, Role::User);
345    }
346}
347