Skip to main content

agentik_sdk/types/
messages.rs

1use serde::{Deserialize, Serialize};
2use crate::types::shared::{RequestId, Usage};
3use crate::files::{File, FileError};
4
5/// A message from Claude
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct Message {
8    /// Unique object identifier
9    pub id: String,
10    
11    /// Object type - always "message" for Messages
12    #[serde(rename = "type")]
13    pub type_: String,
14    
15    /// Conversational role - always "assistant" for responses
16    pub role: Role,
17    
18    /// Content generated by the model
19    #[serde(default)]
20    pub content: Vec<ContentBlock>,
21    
22    /// The model that completed the prompt
23    pub model: String,
24    
25    /// The reason that generation stopped
26    pub stop_reason: Option<StopReason>,
27    
28    /// Which custom stop sequence was generated, if any
29    pub stop_sequence: Option<String>,
30    
31    /// Billing and rate-limit usage
32    pub usage: Usage,
33    
34    /// Request ID for tracking (extracted from headers)
35    #[serde(skip)]
36    pub request_id: Option<RequestId>,
37}
38
39/// Conversational role
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41#[serde(rename_all = "lowercase")]
42pub enum Role {
43    User,
44    Assistant,
45}
46
47/// Content blocks that can appear in messages
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(tag = "type")]
50pub enum ContentBlock {
51    #[serde(rename = "text")]
52    Text { text: String },
53
54    #[serde(rename = "thinking")]
55    Thinking {
56        thinking: String,
57        signature: String,
58    },
59
60    #[serde(rename = "image")]
61    Image { source: ImageSource },
62
63    #[serde(rename = "tool_use")]
64    ToolUse {
65        id: String,
66        name: String,
67        input: serde_json::Value,
68    },
69
70    #[serde(rename = "tool_result")]
71    ToolResult {
72        tool_use_id: String,
73        content: Option<String>,
74        is_error: Option<bool>,
75    },
76}
77
78/// Image source for image content blocks
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80#[serde(tag = "type")]
81pub enum ImageSource {
82    #[serde(rename = "base64")]
83    Base64 {
84        media_type: String,
85        data: String,
86    },
87    
88    #[serde(rename = "url")]
89    Url {
90        url: String,
91    },
92}
93
94/// Reasons why the model stopped generating
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum StopReason {
98    EndTurn,
99    MaxTokens,
100    StopSequence,
101    ToolUse,
102}
103
104/// Parameters for creating a new message
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct MessageCreateParams {
107    /// The model to use for completion
108    pub model: String,
109    
110    /// Maximum number of tokens to generate
111    pub max_tokens: u32,
112    
113    /// Input messages for the conversation
114    pub messages: Vec<MessageParam>,
115    
116    /// System prompt (optional)
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub system: Option<String>,
119    
120    /// Amount of randomness (0.0 to 1.0)
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub temperature: Option<f32>,
123    
124    /// Use nucleus sampling
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub top_p: Option<f32>,
127    
128    /// Only sample from top K options
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub top_k: Option<u32>,
131    
132    /// Custom stop sequences
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub stop_sequences: Option<Vec<String>>,
135    
136    /// Whether to stream the response
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub stream: Option<bool>,
139    
140    /// Tools available for the model to use
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub tools: Option<Vec<crate::types::Tool>>,
143    
144    /// Tool choice strategy
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub tool_choice: Option<crate::types::ToolChoice>,
147    
148    /// Additional metadata
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub metadata: Option<std::collections::HashMap<String, String>>,
151}
152
153/// A single message in the conversation
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct MessageParam {
156    pub role: Role,
157    pub content: MessageContent,
158}
159
160/// Content for a message parameter  
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(untagged)]
163pub enum MessageContent {
164    /// Simple text content
165    Text(String),
166    /// Array of content blocks (text, images, etc.)
167    Blocks(Vec<ContentBlockParam>),
168}
169
170/// Content block parameters for input messages
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(tag = "type")]
173pub enum ContentBlockParam {
174    #[serde(rename = "text")]
175    Text { text: String },
176
177    #[serde(rename = "thinking")]
178    Thinking {
179        thinking: String,
180        signature: String,
181    },
182
183    #[serde(rename = "image")]
184    Image { source: ImageSource },
185
186    #[serde(rename = "tool_use")]
187    ToolUse {
188        id: String,
189        name: String,
190        input: serde_json::Value,
191    },
192
193    #[serde(rename = "tool_result")]
194    ToolResult {
195        tool_use_id: String,
196        content: Option<String>,
197        is_error: Option<bool>,
198    },
199}
200
201/// Builder for creating message requests ergonomically
202#[derive(Debug, Clone)]
203pub struct MessageCreateBuilder {
204    params: MessageCreateParams,
205}
206
207impl MessageCreateBuilder {
208    /// Create a new message builder
209    pub fn new(model: impl Into<String>, max_tokens: u32) -> Self {
210        Self {
211            params: MessageCreateParams {
212                model: model.into(),
213                max_tokens,
214                messages: Vec::new(),
215                system: None,
216                temperature: None,
217                top_p: None,
218                top_k: None,
219                stop_sequences: None,
220                stream: None,
221                tools: None,
222                tool_choice: None,
223                metadata: None,
224            },
225        }
226    }
227    
228    /// Add a message to the conversation
229    pub fn message(mut self, role: Role, content: impl Into<MessageContent>) -> Self {
230        self.params.messages.push(MessageParam {
231            role,
232            content: content.into(),
233        });
234        self
235    }
236    
237    /// Add a user message
238    pub fn user(self, content: impl Into<MessageContent>) -> Self {
239        self.message(Role::User, content)
240    }
241    
242    /// Add an assistant message
243    pub fn assistant(self, content: impl Into<MessageContent>) -> Self {
244        self.message(Role::Assistant, content)
245    }
246    
247    /// Set the system prompt
248    pub fn system(mut self, system: impl Into<String>) -> Self {
249        self.params.system = Some(system.into());
250        self
251    }
252    
253    /// Set the temperature
254    pub fn temperature(mut self, temperature: f32) -> Self {
255        self.params.temperature = Some(temperature);
256        self
257    }
258    
259    /// Set top_p
260    pub fn top_p(mut self, top_p: f32) -> Self {
261        self.params.top_p = Some(top_p);
262        self
263    }
264    
265    /// Set top_k
266    pub fn top_k(mut self, top_k: u32) -> Self {
267        self.params.top_k = Some(top_k);
268        self
269    }
270    
271    /// Set custom stop sequences
272    pub fn stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
273        self.params.stop_sequences = Some(stop_sequences);
274        self
275    }
276    
277    /// Enable streaming
278    pub fn stream(mut self, stream: bool) -> Self {
279        self.params.stream = Some(stream);
280        self
281    }
282    
283    /// Set tools available for the model to use
284    pub fn tools(mut self, tools: Vec<crate::types::Tool>) -> Self {
285        self.params.tools = Some(tools);
286        self
287    }
288    
289    /// Set tool choice strategy
290    pub fn tool_choice(mut self, tool_choice: crate::types::ToolChoice) -> Self {
291        self.params.tool_choice = Some(tool_choice);
292        self
293    }
294    
295    /// Set metadata
296    pub fn metadata(mut self, metadata: std::collections::HashMap<String, String>) -> Self {
297        self.params.metadata = Some(metadata);
298        self
299    }
300    
301    /// Build the message creation parameters
302    pub fn build(self) -> MessageCreateParams {
303        self.params
304    }
305}
306
307// Convenient conversions for MessageContent
308impl From<String> for MessageContent {
309    fn from(text: String) -> Self {
310        Self::Text(text)
311    }
312}
313
314impl From<&str> for MessageContent {
315    fn from(text: &str) -> Self {
316        Self::Text(text.to_string())
317    }
318}
319
320impl From<Vec<ContentBlockParam>> for MessageContent {
321    fn from(blocks: Vec<ContentBlockParam>) -> Self {
322        Self::Blocks(blocks)
323    }
324}
325
326// Helper constructors for ContentBlockParam
327impl ContentBlockParam {
328    /// Create a text content block
329    pub fn text(text: impl Into<String>) -> Self {
330        Self::Text { text: text.into() }
331    }
332    
333    /// Create an image content block from base64 data
334    pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
335        Self::Image {
336            source: ImageSource::Base64 {
337                media_type: media_type.into(),
338                data: data.into(),
339            },
340        }
341    }
342    
343    /// Create an image content block from URL
344    pub fn image_url(url: impl Into<String>) -> Self {
345        Self::Image {
346            source: ImageSource::Url {
347                url: url.into(),
348            },
349        }
350    }
351
352    /// Create an image content block from a File
353    pub async fn image_file(file: File) -> Result<Self, FileError> {
354        if !file.is_image() {
355            return Err(FileError::InvalidMimeType {
356                mime_type: file.mime_type.to_string(),
357                allowed: vec!["image/*".to_string()],
358            });
359        }
360
361        let base64_data = file.to_base64().await?;
362        Ok(Self::Image {
363            source: ImageSource::Base64 {
364                media_type: file.mime_type.to_string(),
365                data: base64_data,
366            },
367        })
368    }
369
370    /// Create an image content block from a File (convenience method)
371    pub async fn from_file(file: File) -> Result<Self, FileError> {
372        Self::image_file(file).await
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_message_builder() {
382        let params = MessageCreateBuilder::new("claude-3-5-sonnet-latest", 1024)
383            .user("Hello, Claude!")
384            .system("You are a helpful assistant.")
385            .temperature(0.7)
386            .build();
387
388        assert_eq!(params.model, "claude-3-5-sonnet-latest");
389        assert_eq!(params.max_tokens, 1024);
390        assert_eq!(params.messages.len(), 1);
391        assert_eq!(params.messages[0].role, Role::User);
392        assert_eq!(params.system, Some("You are a helpful assistant.".to_string()));
393        assert_eq!(params.temperature, Some(0.7));
394    }
395
396    #[test]
397    fn test_content_block_creation() {
398        let text_block = ContentBlockParam::text("Hello world");
399        match text_block {
400            ContentBlockParam::Text { text } => assert_eq!(text, "Hello world"),
401            _ => panic!("Expected text block"),
402        }
403
404        let image_block = ContentBlockParam::image_base64("image/jpeg", "base64data");
405        match image_block {
406            ContentBlockParam::Image { source } => match source {
407                ImageSource::Base64 { media_type, data } => {
408                    assert_eq!(media_type, "image/jpeg");
409                    assert_eq!(data, "base64data");
410                },
411                _ => panic!("Expected base64 image source"),
412            },
413            _ => panic!("Expected image block"),
414        }
415    }
416
417    #[test]
418    fn test_message_content_from_string() {
419        let content: MessageContent = "Hello".into();
420        match content {
421            MessageContent::Text(text) => assert_eq!(text, "Hello"),
422            _ => panic!("Expected text content"),
423        }
424    }
425}