agent_diva_core/
error_context.rs1use std::collections::HashMap;
6
7const MAX_CONTEXT_LENGTH: usize = 500;
9
10const MAX_TOKEN_LENGTH: usize = 100;
12
13#[derive(Debug, Clone)]
15pub struct ErrorContext {
16 pub operation: String,
18 pub error_message: String,
20 pub problematic_content: Option<String>,
22 pub metadata: HashMap<String, String>,
24}
25
26impl ErrorContext {
27 pub fn new(operation: impl Into<String>, error_message: impl Into<String>) -> Self {
29 Self {
30 operation: operation.into(),
31 error_message: error_message.into(),
32 problematic_content: None,
33 metadata: HashMap::new(),
34 }
35 }
36
37 pub fn with_content(mut self, content: impl Into<String>) -> Self {
39 self.problematic_content = Some(truncate_content(&content.into(), MAX_CONTEXT_LENGTH));
40 self
41 }
42
43 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
45 self.metadata.insert(key.into(), value.into());
46 self
47 }
48
49 pub fn to_detailed_string(&self) -> String {
51 let mut parts = vec![format!("[{}] {}", self.operation, self.error_message)];
52
53 if let Some(ref content) = self.problematic_content {
54 parts.push(format!("Problematic content: {}", content));
55 }
56
57 for (key, value) in &self.metadata {
58 parts.push(format!("{}: {}", key, value));
59 }
60
61 parts.join("\n ")
62 }
63}
64
65fn truncate_content(s: &str, max_len: usize) -> String {
67 if s.len() <= max_len {
68 s.to_string()
69 } else {
70 let mut end = max_len.saturating_sub(3);
71 while !s.is_char_boundary(end) && end > 0 {
72 end -= 1;
73 }
74 format!("{}...", &s[..end])
75 }
76}
77
78pub fn find_problematic_chars(content: &str) -> Vec<String> {
81 let mut problems = Vec::new();
82
83 for (i, c) in content.char_indices() {
84 let cp = c as u32;
85
86 if cp < 0x20 && cp != 0x09 && cp != 0x0A && cp != 0x0D {
88 let context = get_char_context(content, i, MAX_TOKEN_LENGTH);
89 problems.push(format!(
90 "Control char U+{:04X} at position {}: '{}'",
91 cp,
92 i,
93 escape_for_display(&context)
94 ));
95 }
96
97 if cp == 0x7F {
99 let context = get_char_context(content, i, MAX_TOKEN_LENGTH);
100 problems.push(format!(
101 "DEL char (U+007F) at position {}: '{}'",
102 i,
103 escape_for_display(&context)
104 ));
105 }
106
107 if c == '\u{FFFD}' {
109 let context = get_char_context(content, i, MAX_TOKEN_LENGTH);
110 problems.push(format!(
111 "Replacement char (U+FFFD) at position {}: '{}'",
112 i,
113 escape_for_display(&context)
114 ));
115 }
116 }
117
118 problems
119}
120
121fn get_char_context(content: &str, pos: usize, max_len: usize) -> String {
123 let start = pos.saturating_sub(max_len / 2);
124 let end = (pos + max_len / 2).min(content.len());
125
126 let start = find_char_boundary(content, start);
128 let end = find_char_boundary_back(content, end);
129
130 content[start..end].to_string()
131}
132
133fn find_char_boundary(s: &str, pos: usize) -> usize {
135 if pos >= s.len() {
136 return s.len();
137 }
138 let mut p = pos;
139 while p < s.len() && !s.is_char_boundary(p) {
140 p += 1;
141 }
142 p
143}
144
145fn find_char_boundary_back(s: &str, pos: usize) -> usize {
147 if pos == 0 {
148 return 0;
149 }
150 let mut p = pos;
151 while p > 0 && !s.is_char_boundary(p) {
152 p -= 1;
153 }
154 p
155}
156
157fn escape_for_display(s: &str) -> String {
159 s.chars()
160 .map(|c| match c {
161 '\n' => "\\n".to_string(),
162 '\r' => "\\r".to_string(),
163 '\t' => "\\t".to_string(),
164 c if c.is_control() => format!("\\u{:04X}", c as u32),
165 c => c.to_string(),
166 })
167 .collect()
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_error_context_basic() {
176 let ctx = ErrorContext::new("test_op", "test error");
177 assert_eq!(ctx.operation, "test_op");
178 assert_eq!(ctx.error_message, "test error");
179 assert!(ctx.problematic_content.is_none());
180 }
181
182 #[test]
183 fn test_error_context_with_content() {
184 let ctx = ErrorContext::new("test", "error").with_content("problematic content");
185 assert_eq!(
186 ctx.problematic_content,
187 Some("problematic content".to_string())
188 );
189 }
190
191 #[test]
192 fn test_error_context_truncation() {
193 let long_content = "x".repeat(1000);
194 let ctx = ErrorContext::new("test", "error").with_content(long_content.clone());
195 assert!(ctx.problematic_content.as_ref().unwrap().len() < long_content.len());
196 assert!(ctx.problematic_content.as_ref().unwrap().ends_with("..."));
197 }
198
199 #[test]
200 fn test_find_problematic_chars_control() {
201 let content = "hello\x01world"; let problems = find_problematic_chars(content);
203 assert!(!problems.is_empty());
204 assert!(problems[0].contains("U+0001"));
205 assert!(problems[0].contains("position 5"));
206 }
207
208 #[test]
209 fn test_find_problematic_chars_clean() {
210 let content = "hello world\nthis is normal";
211 let problems = find_problematic_chars(content);
212 assert!(problems.is_empty());
213 }
214
215 #[test]
216 fn test_escape_for_display() {
217 assert_eq!(escape_for_display("hello\nworld"), "hello\\nworld");
218 assert_eq!(escape_for_display("tab\there"), "tab\\there");
219 assert_eq!(escape_for_display("\x01"), "\\u0001");
220 }
221}