agent_chain_core/callbacks/
file.rs1use std::collections::HashMap;
7use std::fs::{File, OpenOptions};
8use std::io::{self, BufWriter, Write};
9use std::path::Path;
10
11use uuid::Uuid;
12
13use super::base::{
14 BaseCallbackHandler, CallbackManagerMixin, ChainManagerMixin, LLMManagerMixin,
15 RetrieverManagerMixin, RunManagerMixin, ToolManagerMixin,
16};
17
18#[derive(Debug)]
35pub struct FileCallbackHandler {
36 filename: String,
38 mode: String,
40 pub color: Option<String>,
42 file: Option<BufWriter<File>>,
45}
46
47impl FileCallbackHandler {
48 pub fn new<P: AsRef<Path>>(filename: P, append: bool) -> io::Result<Self> {
59 let mode = if append { "a" } else { "w" };
60 Self::with_mode(filename, mode)
61 }
62
63 pub fn with_mode<P: AsRef<Path>>(filename: P, mode: &str) -> io::Result<Self> {
76 let file = match mode {
77 "w" => File::create(filename.as_ref())?,
78 "a" => OpenOptions::new()
79 .create(true)
80 .append(true)
81 .open(filename.as_ref())?,
82 "x" => OpenOptions::new()
83 .create_new(true)
84 .write(true)
85 .open(filename.as_ref())?,
86 _ => {
87 return Err(io::Error::new(
88 io::ErrorKind::InvalidInput,
89 format!("Unsupported file mode: {}", mode),
90 ));
91 }
92 };
93
94 Ok(Self {
95 filename: filename.as_ref().to_string_lossy().to_string(),
96 mode: mode.to_string(),
97 color: None,
98 file: Some(BufWriter::new(file)),
99 })
100 }
101
102 pub fn with_color<P: AsRef<Path>>(
110 filename: P,
111 mode: &str,
112 color: impl Into<String>,
113 ) -> io::Result<Self> {
114 let mut handler = Self::with_mode(filename, mode)?;
115 handler.color = Some(color.into());
116 Ok(handler)
117 }
118
119 pub fn filename(&self) -> &str {
121 &self.filename
122 }
123
124 pub fn mode(&self) -> &str {
126 &self.mode
127 }
128
129 pub fn close(&mut self) {
134 if let Some(mut writer) = self.file.take() {
135 let _ = writer.flush();
136 }
138 }
139
140 fn write(&mut self, text: &str, end: &str) {
147 if let Some(ref mut writer) = self.file {
148 let _ = write!(writer, "{}{}", text, end);
149 let _ = writer.flush();
150 }
151 }
152
153 pub fn flush(&mut self) -> io::Result<()> {
155 if let Some(ref mut writer) = self.file {
156 writer.flush()
157 } else {
158 Ok(())
159 }
160 }
161}
162
163impl Drop for FileCallbackHandler {
164 fn drop(&mut self) {
165 self.close();
166 }
167}
168
169impl LLMManagerMixin for FileCallbackHandler {}
170impl RetrieverManagerMixin for FileCallbackHandler {}
171
172impl ToolManagerMixin for FileCallbackHandler {
173 fn on_tool_end(
175 &mut self,
176 output: &str,
177 _run_id: Uuid,
178 _parent_run_id: Option<Uuid>,
179 _color: Option<&str>,
180 observation_prefix: Option<&str>,
181 llm_prefix: Option<&str>,
182 ) {
183 if let Some(prefix) = observation_prefix {
185 self.write(&format!("\n{}", prefix), "");
186 }
187 self.write(output, "");
188 if let Some(prefix) = llm_prefix {
190 self.write(&format!("\n{}", prefix), "");
191 }
192 }
193}
194
195impl RunManagerMixin for FileCallbackHandler {
196 fn on_text(
198 &mut self,
199 text: &str,
200 _run_id: Uuid,
201 _parent_run_id: Option<Uuid>,
202 _color: Option<&str>,
203 end: &str,
204 ) {
205 self.write(text, end);
206 }
207}
208
209impl CallbackManagerMixin for FileCallbackHandler {
210 fn on_chain_start(
211 &mut self,
212 serialized: &HashMap<String, serde_json::Value>,
213 _inputs: &HashMap<String, serde_json::Value>,
214 _run_id: Uuid,
215 _parent_run_id: Option<Uuid>,
216 _tags: Option<&[String]>,
217 metadata: Option<&HashMap<String, serde_json::Value>>,
218 ) {
219 let name = metadata
222 .and_then(|m| m.get("name"))
223 .and_then(|v| v.as_str())
224 .or_else(|| {
225 if !serialized.is_empty() {
226 serialized.get("name").and_then(|v| v.as_str()).or_else(|| {
227 serialized.get("id").and_then(|v| {
228 v.as_array()
229 .and_then(|arr| arr.last())
230 .and_then(|v| v.as_str())
231 })
232 })
233 } else {
234 None
235 }
236 })
237 .unwrap_or("<unknown>");
238
239 self.write(&format!("\n\n> Entering new {} chain...", name), "\n");
240 }
241}
242
243impl ChainManagerMixin for FileCallbackHandler {
244 fn on_chain_end(
245 &mut self,
246 _outputs: &HashMap<String, serde_json::Value>,
247 _run_id: Uuid,
248 _parent_run_id: Option<Uuid>,
249 ) {
250 self.write("\n> Finished chain.", "\n");
251 }
252
253 fn on_agent_action(
255 &mut self,
256 action: &serde_json::Value,
257 _run_id: Uuid,
258 _parent_run_id: Option<Uuid>,
259 _color: Option<&str>,
260 ) {
261 if let Some(log) = action.get("log").and_then(|v| v.as_str()) {
262 self.write(log, "");
263 }
264 }
265
266 fn on_agent_finish(
268 &mut self,
269 finish: &serde_json::Value,
270 _run_id: Uuid,
271 _parent_run_id: Option<Uuid>,
272 _color: Option<&str>,
273 ) {
274 if let Some(log) = finish.get("log").and_then(|v| v.as_str()) {
275 self.write(log, "\n");
276 }
277 }
278}
279
280impl BaseCallbackHandler for FileCallbackHandler {
281 fn name(&self) -> &str {
282 "FileCallbackHandler"
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use std::fs;
290 use tempfile::tempdir;
291
292 #[test]
293 fn test_file_handler_creation() {
294 let dir = tempdir().unwrap();
295 let file_path = dir.path().join("test_output.txt");
296
297 let handler = FileCallbackHandler::new(&file_path, false);
298 assert!(handler.is_ok());
299
300 let handler = handler.unwrap();
301 assert_eq!(handler.name(), "FileCallbackHandler");
302 assert!(handler.color.is_none());
303 assert_eq!(handler.mode(), "w");
304 }
305
306 #[test]
307 fn test_file_handler_with_mode() {
308 let dir = tempdir().unwrap();
309 let file_path = dir.path().join("test_mode.txt");
310
311 let handler = FileCallbackHandler::with_mode(&file_path, "w");
313 assert!(handler.is_ok());
314 let handler = handler.unwrap();
315 assert_eq!(handler.mode(), "w");
316
317 let handler = FileCallbackHandler::with_mode(&file_path, "a");
319 assert!(handler.is_ok());
320 let handler = handler.unwrap();
321 assert_eq!(handler.mode(), "a");
322
323 let handler = FileCallbackHandler::with_mode(&file_path, "x");
325 assert!(handler.is_err());
326
327 let handler = FileCallbackHandler::with_mode(&file_path, "r");
329 assert!(handler.is_err());
330 }
331
332 #[test]
333 fn test_file_handler_with_color() {
334 let dir = tempdir().unwrap();
335 let file_path = dir.path().join("test_color.txt");
336
337 let handler = FileCallbackHandler::with_color(&file_path, "a", "green");
338 assert!(handler.is_ok());
339
340 let handler = handler.unwrap();
341 assert_eq!(handler.color, Some("green".to_string()));
342 }
343
344 #[test]
345 fn test_file_handler_write() {
346 let dir = tempdir().unwrap();
347 let file_path = dir.path().join("test_write.txt");
348
349 {
350 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
351 handler.write("Hello, World!", "\n");
352 handler.flush().unwrap();
353 }
354
355 let content = fs::read_to_string(&file_path).unwrap();
356 assert_eq!(content, "Hello, World!\n");
357 }
358
359 #[test]
360 fn test_file_handler_append() {
361 let dir = tempdir().unwrap();
362 let file_path = dir.path().join("test_append.txt");
363
364 {
365 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
366 handler.write("First line", "\n");
367 handler.flush().unwrap();
368 }
369
370 {
371 let mut handler = FileCallbackHandler::new(&file_path, true).unwrap();
372 handler.write("Second line", "\n");
373 handler.flush().unwrap();
374 }
375
376 let content = fs::read_to_string(&file_path).unwrap();
377 assert_eq!(content, "First line\nSecond line\n");
378 }
379
380 #[test]
381 fn test_file_handler_close() {
382 let dir = tempdir().unwrap();
383 let file_path = dir.path().join("test_close.txt");
384
385 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
386 handler.write("Before close", "\n");
387
388 handler.close();
390
391 handler.write("After close", "\n");
393
394 handler.close();
396
397 let content = fs::read_to_string(&file_path).unwrap();
398 assert_eq!(content, "Before close\n");
399 }
400
401 #[test]
402 fn test_file_handler_chain_callbacks() {
403 let dir = tempdir().unwrap();
404 let file_path = dir.path().join("test_chain.txt");
405
406 {
407 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
408
409 let mut serialized = HashMap::new();
410 serialized.insert(
411 "name".to_string(),
412 serde_json::Value::String("TestChain".to_string()),
413 );
414
415 let run_id = Uuid::new_v4();
416 handler.on_chain_start(&serialized, &HashMap::new(), run_id, None, None, None);
417 handler.on_chain_end(&HashMap::new(), run_id, None);
418 handler.flush().unwrap();
419 }
420
421 let content = fs::read_to_string(&file_path).unwrap();
422 assert!(content.contains("Entering new TestChain chain"));
423 assert!(content.contains("Finished chain"));
424 }
425
426 #[test]
427 fn test_file_handler_agent_callbacks() {
428 let dir = tempdir().unwrap();
429 let file_path = dir.path().join("test_agent.txt");
430
431 {
432 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
433 let run_id = Uuid::new_v4();
434
435 let action = serde_json::json!({
437 "log": "Agent thinking...",
438 "tool": "search",
439 "tool_input": "query"
440 });
441 handler.on_agent_action(&action, run_id, None, None);
442
443 let finish = serde_json::json!({
445 "log": "Agent finished.",
446 "return_values": {"output": "result"}
447 });
448 handler.on_agent_finish(&finish, run_id, None, None);
449
450 handler.flush().unwrap();
451 }
452
453 let content = fs::read_to_string(&file_path).unwrap();
454 assert!(content.contains("Agent thinking..."));
455 assert!(content.contains("Agent finished."));
456 }
457
458 #[test]
459 fn test_file_handler_tool_and_text_callbacks() {
460 let dir = tempdir().unwrap();
461 let file_path = dir.path().join("test_tool_text.txt");
462
463 {
464 let mut handler = FileCallbackHandler::new(&file_path, false).unwrap();
465 let run_id = Uuid::new_v4();
466
467 handler.on_tool_end("Tool output here", run_id, None, None, None, None);
469
470 handler.on_text("Some text output", run_id, None, None, "");
472
473 handler.flush().unwrap();
474 }
475
476 let content = fs::read_to_string(&file_path).unwrap();
477 assert!(content.contains("Tool output here"));
478 assert!(content.contains("Some text output"));
479 }
480}