1use async_trait::async_trait;
2use std::path::PathBuf;
3use tokio::io::AsyncWriteExt;
4use tokio::sync::Mutex;
5
6use crate::{CallbackHandler, RunTree};
7use lc_schema::Message;
8
9pub enum LogFormat {
11 Plain,
13 Json,
15 JsonLines,
17}
18
19pub struct FileCallbackHandler {
21 file: Mutex<tokio::fs::File>,
22 path: PathBuf,
23 format: LogFormat,
24}
25
26impl FileCallbackHandler {
27 pub fn new(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
29 let path = path.into();
30 let std_file = std::fs::OpenOptions::new()
32 .create(true)
33 .append(true)
34 .open(&path)?;
35 let file = tokio::fs::File::from_std(std_file);
36 Ok(Self {
37 file: Mutex::new(file),
38 path,
39 format: LogFormat::JsonLines,
40 })
41 }
42
43 pub fn with_format(mut self, format: LogFormat) -> Self {
45 self.format = format;
46 self
47 }
48
49 async fn write_log(&self, event: &str, run: &RunTree, data: serde_json::Value) {
50 let mut file = self.file.lock().await;
51 match self.format {
52 LogFormat::JsonLines => {
53 let entry = serde_json::json!({
54 "timestamp": chrono::Utc::now().to_rfc3339(),
55 "event": event,
56 "run_id": run.id,
57 "run_name": run.name,
58 "run_type": run.run_type.as_str(),
59 "data": data
60 });
61 if let Err(e) = file.write_all(format!("{}\n", entry).as_bytes()).await {
63 log::warn!("FileCallbackHandler write failed: {}", e);
64 }
65 if let Err(e) = file.flush().await {
66 log::warn!("FileCallbackHandler flush failed: {}", e);
67 }
68 }
69 LogFormat::Json => {
70 let entry = serde_json::json!({
71 "timestamp": chrono::Utc::now().to_rfc3339(),
72 "event": event,
73 "run_id": run.id,
74 "run_name": run.name,
75 "run_type": run.run_type.as_str(),
76 "data": data
77 });
78 if let Err(e) = file
79 .write_all(
80 format!(
81 "{}\n",
82 serde_json::to_string_pretty(&entry).unwrap_or_default()
83 )
84 .as_bytes(),
85 )
86 .await
87 {
88 log::warn!("FileCallbackHandler write failed: {}", e);
89 }
90 if let Err(e) = file.flush().await {
91 log::warn!("FileCallbackHandler flush failed: {}", e);
92 }
93 }
94 LogFormat::Plain => {
95 let line = format!(
96 "[{}] {} - {} ({})\n",
97 chrono::Utc::now().to_rfc3339(),
98 event,
99 run.name,
100 run.run_type.as_str()
101 );
102 if let Err(e) = file.write_all(line.as_bytes()).await {
103 log::warn!("FileCallbackHandler write failed: {}", e);
104 }
105 if let Err(e) = file.flush().await {
106 log::warn!("FileCallbackHandler flush failed: {}", e);
107 }
108 }
109 }
110 }
111}
112
113#[async_trait]
114impl CallbackHandler for FileCallbackHandler {
115 async fn on_run_start(&self, run: &RunTree) {
116 self.write_log("run_start", run, serde_json::json!({"inputs": run.inputs}))
117 .await;
118 }
119
120 async fn on_run_end(&self, run: &RunTree) {
121 self.write_log(
122 "run_end",
123 run,
124 serde_json::json!({"outputs": run.outputs, "duration_ms": run.duration_ms()}),
125 )
126 .await;
127 }
128
129 async fn on_run_error(&self, run: &RunTree, error: &str) {
130 self.write_log("run_error", run, serde_json::json!({"error": error}))
131 .await;
132 }
133
134 async fn on_llm_start(&self, run: &RunTree, messages: &[Message]) {
135 self.write_log(
136 "llm_start",
137 run,
138 serde_json::json!({
139 "messages_count": messages.len(),
140 "messages": messages.iter().map(|m| m.content.clone()).collect::<Vec<_>>()
141 }),
142 )
143 .await;
144 }
145
146 async fn on_llm_end(&self, run: &RunTree, response: &str) {
147 self.write_log(
148 "llm_end",
149 run,
150 serde_json::json!({"response_length": response.len()}),
151 )
152 .await;
153 }
154
155 async fn on_llm_new_token(&self, run: &RunTree, token: &str) {
156 self.write_log("llm_new_token", run, serde_json::json!({"token": token}))
157 .await;
158 }
159
160 async fn on_llm_error(&self, run: &RunTree, error: &str) {
161 self.write_log("llm_error", run, serde_json::json!({"error": error}))
162 .await;
163 }
164
165 async fn on_chain_start(&self, run: &RunTree, inputs: &serde_json::Value) {
166 self.write_log("chain_start", run, serde_json::json!({"inputs": inputs}))
167 .await;
168 }
169
170 async fn on_chain_end(&self, run: &RunTree, outputs: &serde_json::Value) {
171 self.write_log("chain_end", run, serde_json::json!({"outputs": outputs}))
172 .await;
173 }
174
175 async fn on_chain_error(&self, run: &RunTree, error: &str) {
176 self.write_log("chain_error", run, serde_json::json!({"error": error}))
177 .await;
178 }
179
180 async fn on_tool_start(&self, run: &RunTree, tool_name: &str, input: &str) {
181 self.write_log(
182 "tool_start",
183 run,
184 serde_json::json!({"tool_name": tool_name, "input": input}),
185 )
186 .await;
187 }
188
189 async fn on_tool_end(&self, run: &RunTree, output: &str) {
190 self.write_log("tool_end", run, serde_json::json!({"output": output}))
191 .await;
192 }
193
194 async fn on_tool_error(&self, run: &RunTree, error: &str) {
195 self.write_log("tool_error", run, serde_json::json!({"error": error}))
196 .await;
197 }
198
199 async fn on_retriever_start(&self, run: &RunTree, query: &str) {
200 self.write_log("retriever_start", run, serde_json::json!({"query": query}))
201 .await;
202 }
203
204 async fn on_retriever_end(&self, run: &RunTree, documents: &[serde_json::Value]) {
205 self.write_log(
206 "retriever_end",
207 run,
208 serde_json::json!({"documents_count": documents.len()}),
209 )
210 .await;
211 }
212
213 async fn on_retriever_error(&self, run: &RunTree, error: &str) {
214 self.write_log("retriever_error", run, serde_json::json!({"error": error}))
215 .await;
216 }
217}
218
219impl std::fmt::Debug for FileCallbackHandler {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.debug_struct("FileCallbackHandler")
222 .field("path", &self.path)
223 .field(
224 "format",
225 &match self.format {
226 LogFormat::Plain => "Plain",
227 LogFormat::Json => "Json",
228 LogFormat::JsonLines => "JsonLines",
229 },
230 )
231 .finish()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use tempfile::NamedTempFile;
239
240 #[tokio::test]
241 async fn test_file_handler_creation() {
242 let temp_file = NamedTempFile::new().unwrap();
243 let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
244 assert!(handler.path.exists());
245 }
246
247 #[tokio::test]
248 async fn test_write_log_json_lines() {
249 let temp_file = NamedTempFile::new().unwrap();
250 let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
251
252 let run = RunTree::new(
253 "test_run",
254 crate::RunType::Llm,
255 serde_json::json!({"input": "test"}),
256 );
257 handler
258 .write_log("test_event", &run, serde_json::json!({"data": "value"}))
259 .await;
260
261 let content = std::fs::read_to_string(temp_file.path()).unwrap();
262 assert!(content.contains("test_event"));
263 assert!(content.contains("\"run_name\":\"test_run\""));
264 }
265
266 #[tokio::test]
267 async fn test_write_log_plain_format() {
268 let temp_file = NamedTempFile::new().unwrap();
269 let handler = FileCallbackHandler::new(temp_file.path())
270 .unwrap()
271 .with_format(LogFormat::Plain);
272
273 let run = RunTree::new(
274 "test_run",
275 crate::RunType::Llm,
276 serde_json::json!({"input": "test"}),
277 );
278 handler
279 .write_log("test_event", &run, serde_json::json!({}))
280 .await;
281
282 let content = std::fs::read_to_string(temp_file.path()).unwrap();
283 assert!(content.contains("test_event"));
284 assert!(content.contains("test_run"));
285 }
286}