Skip to main content

simple_agent_type/
coercion.rs

1//! Coercion types for tracking response healing.
2//!
3//! Provides transparency into how LLM responses were transformed.
4
5use serde::{Deserialize, Serialize};
6
7/// Flag indicating a specific coercion/healing operation.
8///
9/// These flags provide full transparency into how a response was modified
10/// to make it parseable or conform to expected types.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum CoercionFlag {
14    /// Stripped markdown code fences
15    StrippedMarkdown,
16
17    /// Fixed trailing comma in JSON
18    FixedTrailingComma,
19
20    /// Fixed mismatched quotes
21    FixedQuotes,
22
23    /// Matched field name using fuzzy matching
24    FuzzyFieldMatch {
25        /// Expected field name
26        expected: String,
27        /// Actual field name found
28        found: String,
29    },
30
31    /// Coerced value from one type to another
32    TypeCoercion {
33        /// Source type
34        from: String,
35        /// Target type
36        to: String,
37    },
38
39    /// Used default value for missing field
40    UsedDefaultValue {
41        /// Field name
42        field: String,
43    },
44
45    /// Truncated malformed JSON
46    TruncatedJson,
47
48    /// Fixed unquoted object keys
49    FixedUnquotedKeys,
50
51    /// Fixed control characters
52    FixedControlCharacters,
53
54    /// Removed BOM (byte order mark)
55    RemovedBom,
56}
57
58impl CoercionFlag {
59    /// Get a human-readable description of this coercion.
60    ///
61    /// # Example
62    /// ```
63    /// use simple_agent_type::coercion::CoercionFlag;
64    ///
65    /// let flag = CoercionFlag::StrippedMarkdown;
66    /// assert_eq!(flag.description(), "Stripped markdown code fences");
67    ///
68    /// let flag = CoercionFlag::TypeCoercion {
69    ///     from: "string".to_string(),
70    ///     to: "number".to_string(),
71    /// };
72    /// assert!(flag.description().contains("string"));
73    /// assert!(flag.description().contains("number"));
74    /// ```
75    pub fn description(&self) -> String {
76        match self {
77            Self::StrippedMarkdown => "Stripped markdown code fences".to_string(),
78            Self::FixedTrailingComma => "Fixed trailing comma in JSON".to_string(),
79            Self::FixedQuotes => "Fixed mismatched quotes".to_string(),
80            Self::FuzzyFieldMatch { expected, found } => {
81                format!("Matched field '{}' as '{}'", found, expected)
82            }
83            Self::TypeCoercion { from, to } => {
84                format!("Coerced type from {} to {}", from, to)
85            }
86            Self::UsedDefaultValue { field } => {
87                format!("Used default value for field '{}'", field)
88            }
89            Self::TruncatedJson => "Truncated malformed JSON".to_string(),
90            Self::FixedUnquotedKeys => "Fixed unquoted object keys".to_string(),
91            Self::FixedControlCharacters => "Fixed control characters".to_string(),
92            Self::RemovedBom => "Removed byte order mark (BOM)".to_string(),
93        }
94    }
95
96    /// Check if this coercion is considered "major" (potentially changes semantics).
97    pub fn is_major(&self) -> bool {
98        matches!(
99            self,
100            Self::TypeCoercion { .. }
101                | Self::UsedDefaultValue { .. }
102                | Self::TruncatedJson
103                | Self::FuzzyFieldMatch { .. }
104        )
105    }
106}
107
108/// Result of a coercion operation with transparency.
109///
110/// Tracks the value, all coercions applied, and confidence score.
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct CoercionResult<T> {
113    /// The coerced value
114    pub value: T,
115    /// All coercion flags applied
116    pub flags: Vec<CoercionFlag>,
117    /// Confidence score (0.0-1.0)
118    pub confidence: f32,
119}
120
121impl<T> CoercionResult<T> {
122    /// Create a new coercion result with perfect confidence.
123    ///
124    /// # Example
125    /// ```
126    /// use simple_agent_type::coercion::CoercionResult;
127    ///
128    /// let result = CoercionResult::new(42);
129    /// assert_eq!(result.value, 42);
130    /// assert_eq!(result.confidence, 1.0);
131    /// assert!(result.flags.is_empty());
132    /// ```
133    pub fn new(value: T) -> Self {
134        Self {
135            value,
136            flags: Vec::new(),
137            confidence: 1.0,
138        }
139    }
140
141    /// Create a result with a specific confidence.
142    pub fn with_confidence(value: T, confidence: f32) -> Self {
143        Self {
144            value,
145            flags: Vec::new(),
146            confidence: confidence.clamp(0.0, 1.0),
147        }
148    }
149
150    /// Set the confidence score (builder pattern).
151    pub fn set_confidence(mut self, confidence: f32) -> Self {
152        self.confidence = confidence.clamp(0.0, 1.0);
153        self
154    }
155
156    /// Add a coercion flag.
157    pub fn with_flag(mut self, flag: CoercionFlag) -> Self {
158        self.flags.push(flag);
159        self
160    }
161
162    /// Add multiple coercion flags.
163    pub fn with_flags(mut self, flags: Vec<CoercionFlag>) -> Self {
164        self.flags.extend(flags);
165        self
166    }
167
168    /// Check if any coercions were applied.
169    ///
170    /// # Example
171    /// ```
172    /// use simple_agent_type::coercion::{CoercionResult, CoercionFlag};
173    ///
174    /// let result = CoercionResult::new(42);
175    /// assert!(!result.was_coerced());
176    ///
177    /// let result = result.with_flag(CoercionFlag::StrippedMarkdown);
178    /// assert!(result.was_coerced());
179    /// ```
180    pub fn was_coerced(&self) -> bool {
181        !self.flags.is_empty()
182    }
183
184    /// Check if confidence meets a threshold.
185    ///
186    /// # Example
187    /// ```
188    /// use simple_agent_type::coercion::CoercionResult;
189    ///
190    /// let result = CoercionResult::with_confidence(42, 0.8);
191    /// assert!(result.is_confident(0.7));
192    /// assert!(!result.is_confident(0.9));
193    /// ```
194    pub fn is_confident(&self, threshold: f32) -> bool {
195        self.confidence >= threshold
196    }
197
198    /// Check if any major coercions were applied.
199    pub fn has_major_coercions(&self) -> bool {
200        self.flags.iter().any(|f| f.is_major())
201    }
202
203    /// Map the value while preserving flags and confidence.
204    pub fn map<U, F>(self, f: F) -> CoercionResult<U>
205    where
206        F: FnOnce(T) -> U,
207    {
208        CoercionResult {
209            value: f(self.value),
210            flags: self.flags,
211            confidence: self.confidence,
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_coercion_flag_description() {
222        let flag = CoercionFlag::StrippedMarkdown;
223        assert!(!flag.description().is_empty());
224
225        let flag = CoercionFlag::TypeCoercion {
226            from: "string".to_string(),
227            to: "number".to_string(),
228        };
229        assert!(flag.description().contains("string"));
230        assert!(flag.description().contains("number"));
231    }
232
233    #[test]
234    fn test_coercion_flag_is_major() {
235        assert!(!CoercionFlag::StrippedMarkdown.is_major());
236        assert!(!CoercionFlag::FixedTrailingComma.is_major());
237        assert!(!CoercionFlag::FixedQuotes.is_major());
238
239        assert!(CoercionFlag::TypeCoercion {
240            from: "string".to_string(),
241            to: "number".to_string(),
242        }
243        .is_major());
244
245        assert!(CoercionFlag::UsedDefaultValue {
246            field: "test".to_string()
247        }
248        .is_major());
249
250        assert!(CoercionFlag::TruncatedJson.is_major());
251    }
252
253    #[test]
254    fn test_coercion_result_new() {
255        let result = CoercionResult::new(42);
256        assert_eq!(result.value, 42);
257        assert_eq!(result.confidence, 1.0);
258        assert!(result.flags.is_empty());
259        assert!(!result.was_coerced());
260    }
261
262    #[test]
263    fn test_coercion_result_with_confidence() {
264        let result = CoercionResult::with_confidence(42, 0.8);
265        assert_eq!(result.value, 42);
266        assert_eq!(result.confidence, 0.8);
267        assert!(result.is_confident(0.7));
268        assert!(!result.is_confident(0.9));
269    }
270
271    #[test]
272    fn test_coercion_result_confidence_clamped() {
273        let result = CoercionResult::with_confidence(42, 1.5);
274        assert_eq!(result.confidence, 1.0);
275
276        let result = CoercionResult::with_confidence(42, -0.5);
277        assert_eq!(result.confidence, 0.0);
278    }
279
280    #[test]
281    fn test_coercion_result_with_flag() {
282        let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
283        assert!(result.was_coerced());
284        assert_eq!(result.flags.len(), 1);
285    }
286
287    #[test]
288    fn test_coercion_result_with_flags() {
289        let flags = vec![
290            CoercionFlag::StrippedMarkdown,
291            CoercionFlag::FixedTrailingComma,
292        ];
293        let result = CoercionResult::new(42).with_flags(flags);
294        assert_eq!(result.flags.len(), 2);
295    }
296
297    #[test]
298    fn test_coercion_result_has_major_coercions() {
299        let result = CoercionResult::new(42).with_flag(CoercionFlag::StrippedMarkdown);
300        assert!(!result.has_major_coercions());
301
302        let result = CoercionResult::new(42).with_flag(CoercionFlag::TruncatedJson);
303        assert!(result.has_major_coercions());
304    }
305
306    #[test]
307    fn test_coercion_result_map() {
308        let result = CoercionResult::new(42)
309            .with_flag(CoercionFlag::StrippedMarkdown)
310            .set_confidence(0.8);
311
312        let mapped = result.map(|x: i32| x.to_string());
313        assert_eq!(mapped.value, "42");
314        assert_eq!(mapped.flags.len(), 1);
315        assert_eq!(mapped.confidence, 0.8);
316    }
317
318    #[test]
319    fn test_coercion_flag_serialization() {
320        let flag = CoercionFlag::StrippedMarkdown;
321        let json = serde_json::to_string(&flag).unwrap();
322        let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
323        assert_eq!(flag, parsed);
324
325        let flag = CoercionFlag::TypeCoercion {
326            from: "string".to_string(),
327            to: "number".to_string(),
328        };
329        let json = serde_json::to_string(&flag).unwrap();
330        let parsed: CoercionFlag = serde_json::from_str(&json).unwrap();
331        assert_eq!(flag, parsed);
332    }
333
334    #[test]
335    fn test_coercion_result_serialization() {
336        let result = CoercionResult::new(42)
337            .with_flag(CoercionFlag::StrippedMarkdown)
338            .set_confidence(0.8);
339
340        let json = serde_json::to_string(&result).unwrap();
341        let parsed: CoercionResult<i32> = serde_json::from_str(&json).unwrap();
342        assert_eq!(result.value, parsed.value);
343        assert_eq!(result.flags, parsed.flags);
344        assert_eq!(result.confidence, parsed.confidence);
345    }
346}