agent_chain_core/tracers/
stdout.rs

1//! Tracers that print to the console.
2//!
3//! This module provides tracers that output run information to the console.
4//! Mirrors `langchain_core.tracers.stdout`.
5
6use std::collections::HashMap;
7
8use serde_json::Value;
9use uuid::Uuid;
10
11use crate::tracers::base::BaseTracer;
12use crate::tracers::core::{TracerCore, TracerCoreConfig};
13use crate::tracers::schemas::Run;
14use crate::utils::input::{get_bolded_text, get_colored_text};
15
16/// Milliseconds in a second.
17const MILLISECONDS_IN_SECOND: f64 = 1000.0;
18
19/// Try to stringify an object to JSON.
20///
21/// # Arguments
22///
23/// * `obj` - The object to stringify.
24/// * `fallback` - The fallback string to return if stringification fails.
25///
26/// # Returns
27///
28/// A JSON string if successful, otherwise the fallback string.
29pub fn try_json_stringify(obj: &Value, fallback: &str) -> String {
30    serde_json::to_string_pretty(obj).unwrap_or_else(|_| fallback.to_string())
31}
32
33/// Get the elapsed time of a run.
34///
35/// # Arguments
36///
37/// * `run` - The run to get the elapsed time of.
38///
39/// # Returns
40///
41/// A string with the elapsed time in seconds or milliseconds.
42pub fn elapsed(run: &Run) -> String {
43    if let Some(end_time) = run.end_time {
44        let duration = end_time.signed_duration_since(run.start_time);
45        let seconds = duration.num_milliseconds() as f64 / MILLISECONDS_IN_SECOND;
46        if seconds < 1.0 {
47            format!("{:.0}ms", seconds * MILLISECONDS_IN_SECOND)
48        } else {
49            format!("{:.2}s", seconds)
50        }
51    } else {
52        "N/A".to_string()
53    }
54}
55
56/// Tracer that calls a function with a single string parameter.
57#[derive(Debug)]
58pub struct FunctionCallbackHandler<F>
59where
60    F: Fn(&str) + Send + Sync,
61{
62    /// The function to call.
63    function_callback: F,
64    /// The tracer configuration.
65    config: TracerCoreConfig,
66    /// The run map.
67    run_map: HashMap<String, Run>,
68    /// The order map.
69    order_map: HashMap<Uuid, (Uuid, String)>,
70}
71
72impl<F> FunctionCallbackHandler<F>
73where
74    F: Fn(&str) + Send + Sync,
75{
76    /// Create a new FunctionCallbackHandler.
77    ///
78    /// # Arguments
79    ///
80    /// * `function` - The callback function to call.
81    pub fn new(function: F) -> Self {
82        Self {
83            function_callback: function,
84            config: TracerCoreConfig::default(),
85            run_map: HashMap::new(),
86            order_map: HashMap::new(),
87        }
88    }
89
90    /// Get the parents of a run.
91    ///
92    /// # Arguments
93    ///
94    /// * `run` - The run to get the parents of.
95    ///
96    /// # Returns
97    ///
98    /// A list of parent runs.
99    pub fn get_parents(&self, run: &Run) -> Vec<Run> {
100        let mut parents = Vec::new();
101        let mut current_run = run.clone();
102
103        while let Some(parent_run_id) = current_run.parent_run_id {
104            if let Some(parent) = self.run_map.get(&parent_run_id.to_string()) {
105                parents.push(parent.clone());
106                current_run = parent.clone();
107            } else {
108                break;
109            }
110        }
111
112        parents
113    }
114
115    /// Get the breadcrumbs of a run.
116    ///
117    /// # Arguments
118    ///
119    /// * `run` - The run to get the breadcrumbs of.
120    ///
121    /// # Returns
122    ///
123    /// A string with the breadcrumbs of the run.
124    pub fn get_breadcrumbs(&self, run: &Run) -> String {
125        let parents: Vec<Run> = self.get_parents(run).into_iter().rev().collect();
126        let mut all_runs = parents;
127        all_runs.push(run.clone());
128
129        all_runs
130            .iter()
131            .map(|r| format!("{}:{}", r.run_type, r.name))
132            .collect::<Vec<_>>()
133            .join(" > ")
134    }
135}
136
137impl<F> TracerCore for FunctionCallbackHandler<F>
138where
139    F: Fn(&str) + Send + Sync + std::fmt::Debug,
140{
141    fn config(&self) -> &TracerCoreConfig {
142        &self.config
143    }
144
145    fn config_mut(&mut self) -> &mut TracerCoreConfig {
146        &mut self.config
147    }
148
149    fn run_map(&self) -> &HashMap<String, Run> {
150        &self.run_map
151    }
152
153    fn run_map_mut(&mut self) -> &mut HashMap<String, Run> {
154        &mut self.run_map
155    }
156
157    fn order_map(&self) -> &HashMap<Uuid, (Uuid, String)> {
158        &self.order_map
159    }
160
161    fn order_map_mut(&mut self) -> &mut HashMap<Uuid, (Uuid, String)> {
162        &mut self.order_map
163    }
164
165    fn persist_run(&mut self, _run: &Run) {}
166
167    fn on_chain_start(&mut self, run: &Run) {
168        let crumbs = self.get_breadcrumbs(run);
169        let run_type = capitalize_first(&run.run_type);
170        let inputs = serde_json::to_value(&run.inputs).unwrap_or_default();
171        (self.function_callback)(&format!(
172            "{} {}{}",
173            get_colored_text("[chain/start]", "green"),
174            get_bolded_text(&format!(
175                "[{}] Entering {} run with input:\n",
176                crumbs, run_type
177            )),
178            try_json_stringify(&inputs, "[inputs]")
179        ));
180    }
181
182    fn on_chain_end(&mut self, run: &Run) {
183        let crumbs = self.get_breadcrumbs(run);
184        let run_type = capitalize_first(&run.run_type);
185        let outputs = run
186            .outputs
187            .as_ref()
188            .map(|o| serde_json::to_value(o).unwrap_or_default())
189            .unwrap_or_default();
190        (self.function_callback)(&format!(
191            "{} {}{}",
192            get_colored_text("[chain/end]", "blue"),
193            get_bolded_text(&format!(
194                "[{}] [{}] Exiting {} run with output:\n",
195                crumbs,
196                elapsed(run),
197                run_type
198            )),
199            try_json_stringify(&outputs, "[outputs]")
200        ));
201    }
202
203    fn on_chain_error(&mut self, run: &Run) {
204        let crumbs = self.get_breadcrumbs(run);
205        let run_type = capitalize_first(&run.run_type);
206        let error = run
207            .error
208            .as_ref()
209            .map(|e| Value::String(e.clone()))
210            .unwrap_or_default();
211        (self.function_callback)(&format!(
212            "{} {}{}",
213            get_colored_text("[chain/error]", "red"),
214            get_bolded_text(&format!(
215                "[{}] [{}] {} run errored with error:\n",
216                crumbs,
217                elapsed(run),
218                run_type
219            )),
220            try_json_stringify(&error, "[error]")
221        ));
222    }
223
224    fn on_llm_start(&mut self, run: &Run) {
225        let crumbs = self.get_breadcrumbs(run);
226        let inputs = if let Some(Value::Array(arr)) = run.inputs.get("prompts") {
227            let trimmed: Vec<Value> = arr
228                .iter()
229                .map(|p| {
230                    if let Value::String(s) = p {
231                        Value::String(s.trim().to_string())
232                    } else {
233                        p.clone()
234                    }
235                })
236                .collect();
237            serde_json::json!({ "prompts": trimmed })
238        } else {
239            serde_json::to_value(&run.inputs).unwrap_or_default()
240        };
241
242        (self.function_callback)(&format!(
243            "{} {}{}",
244            get_colored_text("[llm/start]", "green"),
245            get_bolded_text(&format!("[{}] Entering LLM run with input:\n", crumbs)),
246            try_json_stringify(&inputs, "[inputs]")
247        ));
248    }
249
250    fn on_llm_end(&mut self, run: &Run) {
251        let crumbs = self.get_breadcrumbs(run);
252        let outputs = run
253            .outputs
254            .as_ref()
255            .map(|o| serde_json::to_value(o).unwrap_or_default())
256            .unwrap_or_default();
257        (self.function_callback)(&format!(
258            "{} {}{}",
259            get_colored_text("[llm/end]", "blue"),
260            get_bolded_text(&format!(
261                "[{}] [{}] Exiting LLM run with output:\n",
262                crumbs,
263                elapsed(run)
264            )),
265            try_json_stringify(&outputs, "[response]")
266        ));
267    }
268
269    fn on_llm_error(&mut self, run: &Run) {
270        let crumbs = self.get_breadcrumbs(run);
271        let error = run
272            .error
273            .as_ref()
274            .map(|e| Value::String(e.clone()))
275            .unwrap_or_default();
276        (self.function_callback)(&format!(
277            "{} {}{}",
278            get_colored_text("[llm/error]", "red"),
279            get_bolded_text(&format!(
280                "[{}] [{}] LLM run errored with error:\n",
281                crumbs,
282                elapsed(run)
283            )),
284            try_json_stringify(&error, "[error]")
285        ));
286    }
287
288    fn on_tool_start(&mut self, run: &Run) {
289        let crumbs = self.get_breadcrumbs(run);
290        let input = run
291            .inputs
292            .get("input")
293            .and_then(|v| v.as_str())
294            .unwrap_or("")
295            .trim();
296        (self.function_callback)(&format!(
297            "{} {}\"{}\"",
298            get_colored_text("[tool/start]", "green"),
299            get_bolded_text(&format!("[{}] Entering Tool run with input:\n", crumbs)),
300            input
301        ));
302    }
303
304    fn on_tool_end(&mut self, run: &Run) {
305        let crumbs = self.get_breadcrumbs(run);
306        if let Some(outputs) = &run.outputs
307            && let Some(output) = outputs.get("output")
308        {
309            let output_str = match output {
310                Value::String(s) => s.trim().to_string(),
311                _ => output.to_string(),
312            };
313            (self.function_callback)(&format!(
314                "{} {}\"{}\"",
315                get_colored_text("[tool/end]", "blue"),
316                get_bolded_text(&format!(
317                    "[{}] [{}] Exiting Tool run with output:\n",
318                    crumbs,
319                    elapsed(run)
320                )),
321                output_str
322            ));
323        }
324    }
325
326    fn on_tool_error(&mut self, run: &Run) {
327        let crumbs = self.get_breadcrumbs(run);
328        let error = run.error.as_deref().unwrap_or("");
329        (self.function_callback)(&format!(
330            "{} {}Tool run errored with error:\n{}",
331            get_colored_text("[tool/error]", "red"),
332            get_bolded_text(&format!("[{}] [{}] ", crumbs, elapsed(run))),
333            error
334        ));
335    }
336}
337
338impl<F> BaseTracer for FunctionCallbackHandler<F>
339where
340    F: Fn(&str) + Send + Sync + std::fmt::Debug,
341{
342    fn persist_run_impl(&mut self, _run: &Run) {}
343}
344
345/// Helper function to capitalize the first letter of a string.
346fn capitalize_first(s: &str) -> String {
347    let mut chars = s.chars();
348    match chars.next() {
349        None => String::new(),
350        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
351    }
352}
353
354/// Tracer that prints to the console.
355#[derive(Debug)]
356pub struct ConsoleCallbackHandler {
357    /// The inner function callback handler.
358    inner: FunctionCallbackHandler<fn(&str)>,
359}
360
361impl ConsoleCallbackHandler {
362    /// Create a new ConsoleCallbackHandler.
363    pub fn new() -> Self {
364        fn print_fn(s: &str) {
365            println!("{}", s);
366        }
367        Self {
368            inner: FunctionCallbackHandler::new(print_fn as fn(&str)),
369        }
370    }
371}
372
373impl Default for ConsoleCallbackHandler {
374    fn default() -> Self {
375        Self::new()
376    }
377}
378
379impl TracerCore for ConsoleCallbackHandler {
380    fn config(&self) -> &TracerCoreConfig {
381        self.inner.config()
382    }
383
384    fn config_mut(&mut self) -> &mut TracerCoreConfig {
385        self.inner.config_mut()
386    }
387
388    fn run_map(&self) -> &HashMap<String, Run> {
389        self.inner.run_map()
390    }
391
392    fn run_map_mut(&mut self) -> &mut HashMap<String, Run> {
393        self.inner.run_map_mut()
394    }
395
396    fn order_map(&self) -> &HashMap<Uuid, (Uuid, String)> {
397        self.inner.order_map()
398    }
399
400    fn order_map_mut(&mut self) -> &mut HashMap<Uuid, (Uuid, String)> {
401        self.inner.order_map_mut()
402    }
403
404    fn persist_run(&mut self, run: &Run) {
405        self.inner.persist_run(run)
406    }
407
408    fn on_chain_start(&mut self, run: &Run) {
409        self.inner.on_chain_start(run)
410    }
411
412    fn on_chain_end(&mut self, run: &Run) {
413        self.inner.on_chain_end(run)
414    }
415
416    fn on_chain_error(&mut self, run: &Run) {
417        self.inner.on_chain_error(run)
418    }
419
420    fn on_llm_start(&mut self, run: &Run) {
421        self.inner.on_llm_start(run)
422    }
423
424    fn on_llm_end(&mut self, run: &Run) {
425        self.inner.on_llm_end(run)
426    }
427
428    fn on_llm_error(&mut self, run: &Run) {
429        self.inner.on_llm_error(run)
430    }
431
432    fn on_tool_start(&mut self, run: &Run) {
433        self.inner.on_tool_start(run)
434    }
435
436    fn on_tool_end(&mut self, run: &Run) {
437        self.inner.on_tool_end(run)
438    }
439
440    fn on_tool_error(&mut self, run: &Run) {
441        self.inner.on_tool_error(run)
442    }
443}
444
445impl BaseTracer for ConsoleCallbackHandler {
446    fn persist_run_impl(&mut self, run: &Run) {
447        self.inner.persist_run_impl(run)
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_try_json_stringify() {
457        let obj = serde_json::json!({"key": "value"});
458        let result = try_json_stringify(&obj, "fallback");
459        assert!(result.contains("key"));
460        assert!(result.contains("value"));
461    }
462
463    #[test]
464    fn test_try_json_stringify_fallback() {
465        let obj = serde_json::json!(null);
466        let result = try_json_stringify(&obj, "fallback");
467        assert_eq!(result, "null");
468    }
469
470    #[test]
471    fn test_capitalize_first() {
472        assert_eq!(capitalize_first("hello"), "Hello");
473        assert_eq!(capitalize_first("HELLO"), "HELLO");
474        assert_eq!(capitalize_first(""), "");
475        assert_eq!(capitalize_first("chain"), "Chain");
476    }
477
478    #[test]
479    fn test_elapsed() {
480        let mut run = Run::default();
481        assert_eq!(elapsed(&run), "N/A");
482
483        run.end_time = Some(run.start_time + chrono::Duration::milliseconds(500));
484        assert!(elapsed(&run).contains("ms"));
485
486        run.end_time = Some(run.start_time + chrono::Duration::seconds(2));
487        assert!(elapsed(&run).contains("s"));
488    }
489
490    #[test]
491    fn test_console_callback_handler_creation() {
492        let handler = ConsoleCallbackHandler::new();
493        assert!(handler.run_map().is_empty());
494    }
495
496    #[test]
497    fn test_console_callback_handler_get_breadcrumbs() {
498        let handler = ConsoleCallbackHandler::new();
499
500        let run = Run::new(
501            Uuid::new_v4(),
502            "test_run",
503            "chain",
504            HashMap::new(),
505            HashMap::new(),
506        );
507
508        let breadcrumbs = handler.inner.get_breadcrumbs(&run);
509        assert_eq!(breadcrumbs, "chain:test_run");
510    }
511}