agent_chain_core/messages/
utils.rs

1//! Message utility types and functions.
2//!
3//! This module contains utility types like `AnyMessage` and helper functions
4//! for working with messages. Mirrors `langchain_core.messages.utils`.
5
6use super::ai::AIMessage;
7use super::base::{BaseMessage, BaseMessageChunk};
8use super::chat::ChatMessage;
9use super::function::FunctionMessage;
10use super::human::HumanMessage;
11use super::modifier::RemoveMessage;
12use super::system::SystemMessage;
13use super::tool::ToolMessage;
14
15/// Type alias for any message type, matching LangChain's AnyMessage.
16/// This is equivalent to BaseMessage but provides naming consistency with Python.
17pub type AnyMessage = BaseMessage;
18
19/// A type representing the various ways a message can be represented.
20///
21/// This corresponds to `MessageLikeRepresentation` in LangChain Python.
22pub type MessageLikeRepresentation = serde_json::Value;
23
24/// Convert a sequence of messages to a buffer string.
25///
26/// This concatenates messages with role prefixes for display.
27///
28/// # Arguments
29///
30/// * `messages` - The messages to convert.
31/// * `human_prefix` - The prefix to prepend to human messages (default: "Human").
32/// * `ai_prefix` - The prefix to prepend to AI messages (default: "AI").
33///
34/// # Returns
35///
36/// A single string concatenation of all input messages.
37pub fn get_buffer_string(messages: &[BaseMessage], human_prefix: &str, ai_prefix: &str) -> String {
38    messages
39        .iter()
40        .map(|m| {
41            let role = match m {
42                BaseMessage::Human(_) => human_prefix,
43                BaseMessage::System(_) => "System",
44                BaseMessage::AI(_) => ai_prefix,
45                BaseMessage::Tool(_) => "Tool",
46                BaseMessage::Chat(c) => c.role(),
47                BaseMessage::Function(_) => "Function",
48                BaseMessage::Remove(_) => "Remove",
49            };
50            format!("{}: {}", role, m.content())
51        })
52        .collect::<Vec<_>>()
53        .join("\n")
54}
55
56/// Convert a message to a dictionary representation.
57///
58/// This corresponds to `message_to_dict` in LangChain Python.
59pub fn message_to_dict(message: &BaseMessage) -> serde_json::Value {
60    serde_json::json!({
61        "type": message.message_type(),
62        "data": {
63            "content": message.content(),
64            "id": message.id(),
65            "name": message.name(),
66        }
67    })
68}
69
70/// Convert a sequence of messages to a list of dictionaries.
71///
72/// This corresponds to `messages_to_dict` in LangChain Python.
73pub fn messages_to_dict(messages: &[BaseMessage]) -> Vec<serde_json::Value> {
74    messages.iter().map(message_to_dict).collect()
75}
76
77/// Convert a dictionary to a message.
78///
79/// This corresponds to `_message_from_dict` in LangChain Python.
80pub fn message_from_dict(message: &serde_json::Value) -> Result<BaseMessage, String> {
81    let msg_type = message
82        .get("type")
83        .and_then(|t| t.as_str())
84        .ok_or_else(|| "Message dict must contain 'type' key".to_string())?;
85
86    let data = message
87        .get("data")
88        .ok_or_else(|| "Message dict must contain 'data' key".to_string())?;
89
90    let content = data.get("content").and_then(|c| c.as_str()).unwrap_or("");
91
92    let id = data.get("id").and_then(|i| i.as_str());
93
94    match msg_type {
95        "human" => {
96            let msg = match id {
97                Some(id) => HumanMessage::with_id(id, content),
98                None => HumanMessage::new(content),
99            };
100            Ok(BaseMessage::Human(msg))
101        }
102        "ai" => {
103            let msg = match id {
104                Some(id) => AIMessage::with_id(id, content),
105                None => AIMessage::new(content),
106            };
107            Ok(BaseMessage::AI(msg))
108        }
109        "system" => {
110            let msg = match id {
111                Some(id) => SystemMessage::with_id(id, content),
112                None => SystemMessage::new(content),
113            };
114            Ok(BaseMessage::System(msg))
115        }
116        "tool" => {
117            let tool_call_id = data
118                .get("tool_call_id")
119                .and_then(|t| t.as_str())
120                .unwrap_or("");
121            let msg = match id {
122                Some(id) => ToolMessage::with_id(id, content, tool_call_id),
123                None => ToolMessage::new(content, tool_call_id),
124            };
125            Ok(BaseMessage::Tool(msg))
126        }
127        "chat" => {
128            let role = data.get("role").and_then(|r| r.as_str()).unwrap_or("chat");
129            let msg = match id {
130                Some(id) => ChatMessage::with_id(id, role, content),
131                None => ChatMessage::new(role, content),
132            };
133            Ok(BaseMessage::Chat(msg))
134        }
135        "function" => {
136            let name = data.get("name").and_then(|n| n.as_str()).unwrap_or("");
137            let msg = match id {
138                Some(id) => FunctionMessage::with_id(id, name, content),
139                None => FunctionMessage::new(name, content),
140            };
141            Ok(BaseMessage::Function(msg))
142        }
143        "remove" => {
144            let id = id.ok_or_else(|| "RemoveMessage requires an id".to_string())?;
145            Ok(BaseMessage::Remove(RemoveMessage::new(id)))
146        }
147        _ => Err(format!("Unknown message type: {}", msg_type)),
148    }
149}
150
151/// Convert a sequence of message dicts to messages.
152///
153/// This corresponds to `messages_from_dict` in LangChain Python.
154pub fn messages_from_dict(messages: &[serde_json::Value]) -> Result<Vec<BaseMessage>, String> {
155    messages.iter().map(message_from_dict).collect()
156}
157
158/// Convert message-like representations to messages.
159///
160/// This function can convert from:
161/// - BaseMessage (returned as-is)
162/// - 2-tuple of (role, content) as serde_json::Value
163/// - dict with "role"/"type" and "content" keys
164/// - string (converted to HumanMessage)
165///
166/// This corresponds to `convert_to_messages` in LangChain Python.
167pub fn convert_to_messages(messages: &[serde_json::Value]) -> Result<Vec<BaseMessage>, String> {
168    let mut result = Vec::new();
169
170    for message in messages {
171        if let Some(_msg_type) = message.get("type").and_then(|t| t.as_str()) {
172            // Already a message dict
173            result.push(message_from_dict(message)?);
174        } else if let Some(role) = message.get("role").and_then(|r| r.as_str()) {
175            // OpenAI-style dict with "role" and "content"
176            let content = message
177                .get("content")
178                .and_then(|c| c.as_str())
179                .unwrap_or("");
180            let msg = create_message_from_role(role, content)?;
181            result.push(msg);
182        } else if let Some(s) = message.as_str() {
183            // Plain string -> HumanMessage
184            result.push(BaseMessage::Human(HumanMessage::new(s)));
185        } else if let Some(arr) = message.as_array() {
186            // 2-tuple: [role, content]
187            if arr.len() == 2 {
188                let role = arr[0].as_str().ok_or("First element must be role string")?;
189                let content = arr[1]
190                    .as_str()
191                    .ok_or("Second element must be content string")?;
192                let msg = create_message_from_role(role, content)?;
193                result.push(msg);
194            } else {
195                return Err(
196                    "Array message must have exactly 2 elements [role, content]".to_string()
197                );
198            }
199        } else {
200            return Err(format!("Cannot convert to message: {:?}", message));
201        }
202    }
203
204    Ok(result)
205}
206
207/// Create a message from a role string and content.
208fn create_message_from_role(role: &str, content: &str) -> Result<BaseMessage, String> {
209    match role {
210        "human" | "user" => Ok(BaseMessage::Human(HumanMessage::new(content))),
211        "ai" | "assistant" => Ok(BaseMessage::AI(AIMessage::new(content))),
212        "system" | "developer" => Ok(BaseMessage::System(SystemMessage::new(content))),
213        "function" => Err("Function messages require a name".to_string()),
214        "tool" => Err("Tool messages require a tool_call_id".to_string()),
215        _ => Ok(BaseMessage::Chat(ChatMessage::new(role, content))),
216    }
217}
218
219/// Filter messages based on name, type, or ID.
220///
221/// This corresponds to `filter_messages` in LangChain Python.
222pub fn filter_messages(
223    messages: &[BaseMessage],
224    include_names: Option<&[&str]>,
225    exclude_names: Option<&[&str]>,
226    include_types: Option<&[&str]>,
227    exclude_types: Option<&[&str]>,
228    include_ids: Option<&[&str]>,
229    exclude_ids: Option<&[&str]>,
230) -> Vec<BaseMessage> {
231    messages
232        .iter()
233        .filter(|msg| {
234            // Check exclusions first
235            if let Some(exclude_names) = exclude_names
236                && let Some(name) = msg.name()
237                && exclude_names.contains(&name)
238            {
239                return false;
240            }
241
242            if let Some(exclude_types) = exclude_types
243                && exclude_types.contains(&msg.message_type())
244            {
245                return false;
246            }
247
248            if let Some(exclude_ids) = exclude_ids
249                && let Some(id) = msg.id()
250                && exclude_ids.contains(&id)
251            {
252                return false;
253            }
254
255            // Check inclusions (default to including if no criteria given)
256            let include_by_name = include_names
257                .is_none_or(|names| msg.name().is_some_and(|name| names.contains(&name)));
258
259            let include_by_type =
260                include_types.is_none_or(|types| types.contains(&msg.message_type()));
261
262            let include_by_id =
263                include_ids.is_none_or(|ids| msg.id().is_some_and(|id| ids.contains(&id)));
264
265            // If any inclusion criteria is specified, at least one must match
266            let any_include_specified =
267                include_names.is_some() || include_types.is_some() || include_ids.is_some();
268
269            if any_include_specified {
270                include_by_name || include_by_type || include_by_id
271            } else {
272                true
273            }
274        })
275        .cloned()
276        .collect()
277}
278
279/// Merge consecutive messages of the same type.
280///
281/// Note: ToolMessages are not merged, as each has a distinct tool call ID.
282///
283/// This corresponds to `merge_message_runs` in LangChain Python.
284pub fn merge_message_runs(messages: &[BaseMessage], chunk_separator: &str) -> Vec<BaseMessage> {
285    if messages.is_empty() {
286        return Vec::new();
287    }
288
289    let mut merged: Vec<BaseMessage> = Vec::new();
290
291    for msg in messages {
292        if merged.is_empty() {
293            merged.push(msg.clone());
294            continue;
295        }
296
297        let last = merged.last().expect("merged is not empty");
298
299        // Don't merge ToolMessages or messages of different types
300        if matches!(msg, BaseMessage::Tool(_))
301            || std::mem::discriminant(last) != std::mem::discriminant(msg)
302        {
303            merged.push(msg.clone());
304        } else {
305            // Same type, merge content
306            let last = merged.pop().expect("merged is not empty");
307            let merged_content = format!("{}{}{}", last.content(), chunk_separator, msg.content());
308
309            let new_msg = match (last, msg) {
310                (BaseMessage::Human(_), BaseMessage::Human(_)) => {
311                    BaseMessage::Human(HumanMessage::new(&merged_content))
312                }
313                (BaseMessage::AI(_), BaseMessage::AI(_)) => {
314                    BaseMessage::AI(AIMessage::new(&merged_content))
315                }
316                (BaseMessage::System(_), BaseMessage::System(_)) => {
317                    BaseMessage::System(SystemMessage::new(&merged_content))
318                }
319                (BaseMessage::Chat(c), BaseMessage::Chat(_)) => {
320                    BaseMessage::Chat(ChatMessage::new(c.role(), &merged_content))
321                }
322                (BaseMessage::Function(f), BaseMessage::Function(_)) => {
323                    BaseMessage::Function(FunctionMessage::new(f.name(), &merged_content))
324                }
325                _ => {
326                    // Shouldn't happen due to discriminant check, but handle gracefully
327                    merged.push(msg.clone());
328                    continue;
329                }
330            };
331
332            merged.push(new_msg);
333        }
334    }
335
336    merged
337}
338
339/// Convert a message chunk to a complete message.
340///
341/// This corresponds to `message_chunk_to_message` in LangChain Python.
342pub fn message_chunk_to_message(chunk: &BaseMessageChunk) -> BaseMessage {
343    chunk.to_message()
344}