mojentic/examples/react/
formatters.rs

1//! Formatting utilities for the ReAct pattern implementation.
2//!
3//! This module provides helper functions for formatting context and tool information
4//! into human-readable strings for LLM prompts.
5
6use crate::llm::tools::LlmTool;
7
8use super::models::CurrentContext;
9
10/// Format the current context into a readable string.
11///
12/// # Arguments
13///
14/// * `context` - The current context containing query, plan, and history.
15///
16/// # Returns
17///
18/// A formatted multi-line string describing the current context.
19pub fn format_current_context(context: &CurrentContext) -> String {
20    let user_query = format!(
21        "The user has asked us to answer the following query:\n> {}\n",
22        context.user_query
23    );
24
25    let plan = if context.plan.steps.is_empty() {
26        "You have not yet made a plan.\n".to_string()
27    } else {
28        let mut output = "Current plan:\n".to_string();
29        for step in &context.plan.steps {
30            output.push_str(&format!("- {}\n", step));
31        }
32        output
33    };
34
35    let history = if context.history.is_empty() {
36        "No steps have yet been taken.\n".to_string()
37    } else {
38        let mut output = "What's been done so far:\n".to_string();
39        for (i, step) in context.history.iter().enumerate() {
40            output.push_str(&format!(
41                "{}.\n    Thought: {}\n    Action: {}\n    Observation: {}\n",
42                i + 1,
43                step.thought,
44                step.action,
45                step.observation
46            ));
47        }
48        output
49    };
50
51    format!("Current Context:\n{}{}{}\n", user_query, plan, history)
52}
53
54/// Format the available tools into a readable list.
55///
56/// # Arguments
57///
58/// * `tools` - A slice of tool references with descriptor dictionaries.
59///
60/// # Returns
61///
62/// A formatted string listing available tools and their descriptions.
63pub fn format_available_tools(tools: &[&dyn LlmTool]) -> String {
64    if tools.is_empty() {
65        return String::new();
66    }
67
68    let mut output = "Tools available:\n".to_string();
69
70    for tool in tools {
71        let descriptor = tool.descriptor();
72        output.push_str(&format!(
73            "- {}: {}\n",
74            descriptor.function.name, descriptor.function.description
75        ));
76
77        // Add parameter information
78        if let Some(params) = descriptor.function.parameters.as_object() {
79            if let Some(properties) = params.get("properties").and_then(|p| p.as_object()) {
80                output.push_str("  Parameters:\n");
81
82                let required: Vec<String> = params
83                    .get("required")
84                    .and_then(|r| r.as_array())
85                    .map(|arr| {
86                        arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()
87                    })
88                    .unwrap_or_default();
89
90                for (param_name, param_info) in properties {
91                    let param_desc =
92                        param_info.get("description").and_then(|d| d.as_str()).unwrap_or("");
93                    let is_required = required.contains(param_name);
94                    let req_str = if is_required {
95                        " (required)"
96                    } else {
97                        " (optional)"
98                    };
99                    output.push_str(&format!("    - {}{}: {}\n", param_name, req_str, param_desc));
100                }
101            }
102        }
103    }
104
105    output
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::llm::tools::simple_date_tool::SimpleDateTool;
112    use crate::llm::tools::LlmTool;
113
114    use super::super::models::{Plan, ThoughtActionObservation};
115
116    #[test]
117    fn test_format_current_context_empty() {
118        let context = CurrentContext::new("What is the weather?");
119        let formatted = format_current_context(&context);
120
121        assert!(formatted.contains("What is the weather?"));
122        assert!(formatted.contains("You have not yet made a plan"));
123        assert!(formatted.contains("No steps have yet been taken"));
124    }
125
126    #[test]
127    fn test_format_current_context_with_plan() {
128        let mut context = CurrentContext::new("Calculate total");
129        context.plan = Plan {
130            steps: vec![
131                "Step 1: Get data".to_string(),
132                "Step 2: Sum values".to_string(),
133            ],
134        };
135
136        let formatted = format_current_context(&context);
137
138        assert!(formatted.contains("Current plan:"));
139        assert!(formatted.contains("Step 1: Get data"));
140        assert!(formatted.contains("Step 2: Sum values"));
141    }
142
143    #[test]
144    fn test_format_current_context_with_history() {
145        let mut context = CurrentContext::new("Get date");
146        context.history.push(ThoughtActionObservation {
147            thought: "Need to resolve date".to_string(),
148            action: "Called resolve_date".to_string(),
149            observation: "2025-11-29".to_string(),
150        });
151
152        let formatted = format_current_context(&context);
153
154        assert!(formatted.contains("What's been done so far:"));
155        assert!(formatted.contains("Need to resolve date"));
156        assert!(formatted.contains("Called resolve_date"));
157        assert!(formatted.contains("2025-11-29"));
158    }
159
160    #[test]
161    fn test_format_available_tools_empty() {
162        let tools: Vec<&dyn LlmTool> = vec![];
163        let formatted = format_available_tools(&tools);
164
165        assert_eq!(formatted, "");
166    }
167
168    #[test]
169    fn test_format_available_tools_with_tools() {
170        let date_tool = SimpleDateTool;
171        let tools: Vec<&dyn LlmTool> = vec![&date_tool];
172
173        let formatted = format_available_tools(&tools);
174
175        assert!(formatted.contains("Tools available:"));
176        assert!(formatted.contains("resolve_date"));
177        assert!(formatted.contains("relative date"));
178        assert!(formatted.contains("Parameters:"));
179        assert!(formatted.contains("relative_date"));
180        assert!(formatted.contains("(required)"));
181    }
182
183    #[test]
184    fn test_format_available_tools_multiple() {
185        let date_tool = SimpleDateTool;
186        let tools: Vec<&dyn LlmTool> = vec![&date_tool];
187
188        let formatted = format_available_tools(&tools);
189
190        // Should list each tool with its description and parameters
191        let tool_count = formatted.matches("resolve_date").count();
192        assert_eq!(tool_count, 1);
193    }
194}