agent_chain_core/messages/
human.rs

1//! Human message type.
2//!
3//! This module contains the `HumanMessage` and `HumanMessageChunk` types which represent
4//! messages from the user. Mirrors `langchain_core.messages.human`.
5
6use crate::utils::uuid7;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[cfg(feature = "specta")]
11use specta::Type;
12
13use super::base::merge_content;
14use super::content::{ContentPart, ImageSource, MessageContent};
15
16/// A human message in the conversation.
17///
18/// Human messages support both simple text content and multimodal content
19/// with images. Use [`HumanMessage::new`] for simple text messages and
20/// [`HumanMessage::with_content`] for multimodal messages.
21///
22/// This corresponds to `HumanMessage` in LangChain Python.
23#[cfg_attr(feature = "specta", derive(Type))]
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct HumanMessage {
26    /// The message content (text or multipart)
27    content: MessageContent,
28    /// Optional unique identifier
29    id: Option<String>,
30    /// Optional name for the message
31    #[serde(skip_serializing_if = "Option::is_none")]
32    name: Option<String>,
33    /// Additional metadata
34    #[serde(default)]
35    additional_kwargs: HashMap<String, serde_json::Value>,
36}
37
38impl HumanMessage {
39    /// Create a new human message with simple text content.
40    pub fn new(content: impl Into<String>) -> Self {
41        Self {
42            content: MessageContent::Text(content.into()),
43            id: Some(uuid7(None).to_string()),
44            name: None,
45            additional_kwargs: HashMap::new(),
46        }
47    }
48
49    /// Create a new human message with simple text content and an explicit ID.
50    ///
51    /// Use this when deserializing or reconstructing messages where the ID must be preserved.
52    pub fn with_id(id: impl Into<String>, content: impl Into<String>) -> Self {
53        Self {
54            content: MessageContent::Text(content.into()),
55            id: Some(id.into()),
56            name: None,
57            additional_kwargs: HashMap::new(),
58        }
59    }
60
61    /// Create a new human message with multipart content.
62    ///
63    /// # Example
64    ///
65    /// ```ignore
66    /// use agent_chain_core::messages::{HumanMessage, ContentPart, ImageSource};
67    ///
68    /// let msg = HumanMessage::with_content(vec![
69    ///     ContentPart::Text { text: "What's in this image?".into() },
70    ///     ContentPart::Image {
71    ///         source: ImageSource::Url {
72    ///             url: "https://example.com/image.jpg".into(),
73    ///         },
74    ///         detail: None,
75    ///     },
76    /// ]);
77    /// ```
78    pub fn with_content(parts: Vec<ContentPart>) -> Self {
79        Self {
80            content: MessageContent::Parts(parts),
81            id: Some(uuid7(None).to_string()),
82            name: None,
83            additional_kwargs: HashMap::new(),
84        }
85    }
86
87    /// Create a new human message with multipart content and an explicit ID.
88    ///
89    /// Use this when deserializing or reconstructing messages where the ID must be preserved.
90    pub fn with_id_and_content(id: impl Into<String>, parts: Vec<ContentPart>) -> Self {
91        Self {
92            content: MessageContent::Parts(parts),
93            id: Some(id.into()),
94            name: None,
95            additional_kwargs: HashMap::new(),
96        }
97    }
98
99    /// Create a human message with text and a single image from a URL.
100    pub fn with_image_url(text: impl Into<String>, url: impl Into<String>) -> Self {
101        Self::with_content(vec![
102            ContentPart::Text { text: text.into() },
103            ContentPart::Image {
104                source: ImageSource::Url { url: url.into() },
105                detail: None,
106            },
107        ])
108    }
109
110    /// Create a human message with text and a single base64-encoded image.
111    pub fn with_image_base64(
112        text: impl Into<String>,
113        media_type: impl Into<String>,
114        data: impl Into<String>,
115    ) -> Self {
116        Self::with_content(vec![
117            ContentPart::Text { text: text.into() },
118            ContentPart::Image {
119                source: ImageSource::Base64 {
120                    media_type: media_type.into(),
121                    data: data.into(),
122                },
123                detail: None,
124            },
125        ])
126    }
127
128    /// Set the name for this message.
129    pub fn with_name(mut self, name: impl Into<String>) -> Self {
130        self.name = Some(name.into());
131        self
132    }
133
134    /// Get the message content as text.
135    ///
136    /// For multipart messages, this returns an empty string.
137    /// Use [`message_content()`](Self::message_content) to access the full content.
138    pub fn content(&self) -> &str {
139        match &self.content {
140            MessageContent::Text(s) => s,
141            MessageContent::Parts(_) => "",
142        }
143    }
144
145    /// Get the full message content (text or multipart).
146    pub fn message_content(&self) -> &MessageContent {
147        &self.content
148    }
149
150    /// Check if this message contains images.
151    pub fn has_images(&self) -> bool {
152        self.content.has_images()
153    }
154
155    /// Get the message ID.
156    pub fn id(&self) -> Option<&str> {
157        self.id.as_deref()
158    }
159
160    /// Get the message name.
161    pub fn name(&self) -> Option<&str> {
162        self.name.as_deref()
163    }
164
165    /// Get additional kwargs.
166    pub fn additional_kwargs(&self) -> &HashMap<String, serde_json::Value> {
167        &self.additional_kwargs
168    }
169}
170
171/// Human message chunk (yielded when streaming).
172///
173/// This corresponds to `HumanMessageChunk` in LangChain Python.
174#[cfg_attr(feature = "specta", derive(Type))]
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176pub struct HumanMessageChunk {
177    /// The message content (may be partial during streaming)
178    content: MessageContent,
179    /// Optional unique identifier
180    id: Option<String>,
181    /// Optional name for the message
182    #[serde(skip_serializing_if = "Option::is_none")]
183    name: Option<String>,
184    /// Additional metadata
185    #[serde(default)]
186    additional_kwargs: HashMap<String, serde_json::Value>,
187    /// Response metadata
188    #[serde(default)]
189    response_metadata: HashMap<String, serde_json::Value>,
190}
191
192impl HumanMessageChunk {
193    /// Create a new human message chunk with text content.
194    pub fn new(content: impl Into<String>) -> Self {
195        Self {
196            content: MessageContent::Text(content.into()),
197            id: None,
198            name: None,
199            additional_kwargs: HashMap::new(),
200            response_metadata: HashMap::new(),
201        }
202    }
203
204    /// Create a new human message chunk with an ID.
205    pub fn with_id(id: impl Into<String>, content: impl Into<String>) -> Self {
206        Self {
207            content: MessageContent::Text(content.into()),
208            id: Some(id.into()),
209            name: None,
210            additional_kwargs: HashMap::new(),
211            response_metadata: HashMap::new(),
212        }
213    }
214
215    /// Get the message content as text.
216    pub fn content(&self) -> &str {
217        match &self.content {
218            MessageContent::Text(s) => s,
219            MessageContent::Parts(_) => "",
220        }
221    }
222
223    /// Get the full message content.
224    pub fn message_content(&self) -> &MessageContent {
225        &self.content
226    }
227
228    /// Get the message ID.
229    pub fn id(&self) -> Option<&str> {
230        self.id.as_deref()
231    }
232
233    /// Get the message name.
234    pub fn name(&self) -> Option<&str> {
235        self.name.as_deref()
236    }
237
238    /// Get additional kwargs.
239    pub fn additional_kwargs(&self) -> &HashMap<String, serde_json::Value> {
240        &self.additional_kwargs
241    }
242
243    /// Get response metadata.
244    pub fn response_metadata(&self) -> &HashMap<String, serde_json::Value> {
245        &self.response_metadata
246    }
247
248    /// Concatenate this chunk with another chunk.
249    pub fn concat(&self, other: &HumanMessageChunk) -> HumanMessageChunk {
250        let content = match (&self.content, &other.content) {
251            (MessageContent::Text(a), MessageContent::Text(b)) => {
252                MessageContent::Text(merge_content(a, b))
253            }
254            (MessageContent::Parts(a), MessageContent::Parts(b)) => {
255                let mut parts = a.clone();
256                parts.extend(b.clone());
257                MessageContent::Parts(parts)
258            }
259            (MessageContent::Text(a), MessageContent::Parts(b)) => {
260                let mut parts = vec![ContentPart::Text { text: a.clone() }];
261                parts.extend(b.clone());
262                MessageContent::Parts(parts)
263            }
264            (MessageContent::Parts(a), MessageContent::Text(b)) => {
265                let mut parts = a.clone();
266                parts.push(ContentPart::Text { text: b.clone() });
267                MessageContent::Parts(parts)
268            }
269        };
270
271        // Merge additional_kwargs
272        let mut additional_kwargs = self.additional_kwargs.clone();
273        for (k, v) in &other.additional_kwargs {
274            additional_kwargs.insert(k.clone(), v.clone());
275        }
276
277        // Merge response_metadata
278        let mut response_metadata = self.response_metadata.clone();
279        for (k, v) in &other.response_metadata {
280            response_metadata.insert(k.clone(), v.clone());
281        }
282
283        HumanMessageChunk {
284            content,
285            id: self.id.clone().or_else(|| other.id.clone()),
286            name: self.name.clone().or_else(|| other.name.clone()),
287            additional_kwargs,
288            response_metadata,
289        }
290    }
291
292    /// Convert this chunk to a complete HumanMessage.
293    pub fn to_message(&self) -> HumanMessage {
294        HumanMessage {
295            content: self.content.clone(),
296            id: self.id.clone(),
297            name: self.name.clone(),
298            additional_kwargs: self.additional_kwargs.clone(),
299        }
300    }
301}
302
303impl std::ops::Add for HumanMessageChunk {
304    type Output = HumanMessageChunk;
305
306    fn add(self, other: HumanMessageChunk) -> HumanMessageChunk {
307        self.concat(&other)
308    }
309}