aimx/inference/
response.rs

1//! Inference response parsing and conversion
2//!
3//! This module provides functionality for parsing inference model responses and converting them
4//! into structured data according to type definitions. It handles both single-line and multi-line
5//! responses with various formatting patterns.
6//!
7//! # Overview
8//!
9//! The response parser processes text output from inference models and extracts structured
10//! key-value pairs according to AIM response format conventions. It supports:
11//! - Single-line responses with inline values
12//! - Multi-line list responses with common prefixes
13//! - Task status indicators (checkboxes)
14//! - Type-safe conversion to [`Value`] and [`Literal`] types
15//!
16//! # Response Format
17//!
18//! The parser expects responses in the following format:
19//! ```text
20//! KEY1: inline value
21//! KEY2
22//! - Checkbox item 1 [X]
23//! - Checkbox item 2 [ ]
24//! KEY3
25//! 1. Ordered item 1
26//! 2. Ordered item 2
27//! ```
28//!
29//! # Examples
30//!
31//! ```rust
32//! use aimx::{parse_response, inference::Response};
33//! use aimx::typedef::Typedef;
34//! 
35//! let response_text = "ANSWER: 42";
36//! if let Some(response_map) = parse_response(response_text) {
37//!     let typedef = Typedef::Number;
38//!     if let Some(response) = response_map.get("ANSWER") {
39//!         match response.to_value(&typedef) {
40//!             Ok(value) => println!("Parsed value: {:?}", value),
41//!             Err(e) => eprintln!("Error: {}", e),
42//!         }
43//!     }
44//! }
45//! ```
46
47use std::collections::HashMap;
48use crate::{
49    Literal,
50    inference::{Item, parse_key, parse_item, parse_inline_item, Suffix},
51    literals::{parse_date, parse_number},
52    Value,
53    Typedef,
54};
55use anyhow::{anyhow, Result};
56
57/// Represents a parsed inference response
58/// 
59/// This enum can represent either a single response item or a multi-line list of items.
60/// The parser determines the response type based on the formatting pattern found in the input.
61#[derive(Debug, PartialEq, Clone)]
62pub enum Response {
63    /// Single response item, typically from an inline key-value pair like "KEY: value"
64    Single(Item),
65    /// Multi-line response containing a list of items with common prefix patterns
66    Multiline(Vec<Item>),
67}
68
69/// Parse a formatted inference response and extract key/item data
70/// 
71/// This function processes text output from inference models and extracts structured
72/// key-value pairs according to AIM response format conventions. It handles both
73/// single-line responses (with inline values) and multi-line list responses.
74/// 
75/// # Parameters
76/// 
77/// - `input`: The raw text response from an inference model
78/// 
79/// # Returns
80/// 
81/// - `Some(HashMap<String, Response>)`: A map of response keys to parsed responses
82/// - `None`: If no valid responses were found in the input
83/// 
84/// # Examples
85/// 
86/// ```rust
87/// use aimx::parse_response;
88/// 
89/// let response = "ANSWER: 42\nLIST:\n- Item 1\n- Item 2";
90/// if let Some(parsed) = parse_response(response) {
91///     assert!(parsed.contains_key("ANSWER"));
92///     assert!(parsed.contains_key("LIST"));
93/// }
94/// ```
95pub fn parse_response(input: &str) -> Option<HashMap<String, Response>> {
96    let mut response_map: HashMap<String, Response> = HashMap::new();
97    let mut current_list: Vec<Item> = Vec::new(); 
98    let mut current_key: Option<String> = None;
99    let mut first_item: Option<Item> = None;
100    
101    // Process each line
102    for line in input.lines() {
103
104        // Check if the line is a new key
105        if let Ok((input, (key, suffix))) = parse_key(line) {
106            // This marks a new response so insert any pending response
107            if let (Some(key), Some(item)) = (current_key, first_item) {
108                response_map.insert(key, Response::new(&item, &current_list));
109            }
110            match suffix {
111                Suffix::Colon => {
112                    // Store key and try to parse inline item
113                    if let Ok((_, item)) = parse_inline_item(input) {
114                        response_map.insert(key, Response::Single(item));
115                    }
116                    current_key = None;
117                },
118                Suffix::Eol | Suffix::ColonEol => {
119                    // Store the new key which changes the state to multiline
120                    current_key = Some(key);
121                }
122            }
123            first_item = None;
124            current_list.clear();
125
126        // Check for multiline state
127        } else if let Some(key) = &current_key {
128            // Check if we have a first item
129            if let Some(item) = &first_item {
130                if let Ok((_, result)) = parse_item(line) {
131                    // Check the prefixes match
132                    let prefixes_match = match (&item, &result) {
133                        (Item::Value(prefix1, _), Item::Value(prefix2, _)) => prefix1 == prefix2,
134                        (Item::Task(prefix1, _, _), Item::Task(prefix2, _, _)) => prefix1 == prefix2,
135                        _ => false, // Different item types
136                    };
137                    if prefixes_match {
138                        current_list.push(result);
139                    }
140                    else {
141                        response_map.insert(key.clone(), Response::new(&item, &current_list));
142                        current_key = None;
143                        first_item = None;
144                        current_list.clear();
145                    }
146                }
147                else {
148                    response_map.insert(key.clone(), Response::new(&item, &current_list));
149                    current_key = None;
150                    first_item = None;
151                    current_list.clear();
152                }
153            }
154            // Parse the first item
155            else if let Ok((_, result)) = parse_item(line) {
156                current_list.push(result.clone());
157                first_item = Some(result);
158            }
159            // No result
160            else {
161                current_key = None;
162            }
163        }
164    }
165    // This marks the end of the input so insert any pending response
166    if let (Some(key), Some(item)) = (current_key, first_item) {
167        response_map.insert(key, Response::new(&item, &current_list));
168    }
169     
170    // Only return the map if we successfully parsed at least one item
171    if response_map.is_empty() {
172        None
173    } else {
174        Some(response_map)
175    }
176}
177
178/// Convert a single input string to a Literal based on type definition
179/// 
180/// This function performs type-safe parsing of single values according to the
181/// provided type definition. It supports number, date, and text types.
182/// 
183/// # Parameters
184/// 
185/// - `input`: The input string to parse
186/// - `typedef`: The type definition specifying expected data type
187/// 
188/// # Returns
189/// 
190/// - `Ok(Literal)`: Successfully parsed literal value
191/// - `Err`: If parsing fails or type mismatch occurs
192fn from_single(input: &str, typedef: &Typedef) -> Result<Literal> {
193    if input.trim().is_empty() {
194        return Err(anyhow!("Nothing to assign"));
195    }
196    if typedef.is_number() {
197        match parse_number(input) {
198            Ok((_, number)) => {
199                return Ok(Literal::from_number(number))
200            }
201            Err(e) => {
202                return Err(anyhow!("Parse error: {}", e))
203            }
204        }
205    }
206    if typedef.is_date() {
207        match parse_date(input) {
208            Ok((_, date)) => {
209                return Ok(Literal::from_date(date))
210            }
211            Err(e) => {
212                return Err(anyhow!("Parse error: {}", e))
213            }
214        }        
215    }
216    if typedef.is_text() {
217        return Ok(Literal::from_text(input.to_string()))
218    }
219    Err(anyhow!("Type definition mismatch"))
220}
221
222/// Convert a task input string to a Literal based on type definition
223/// 
224/// This function handles task-specific parsing with status indicators.
225/// If the type definition doesn't specify a task, it falls back to regular parsing.
226/// 
227/// # Parameters
228/// 
229/// - `input`: The input string to parse
230/// - `typedef`: The type definition specifying expected data type
231/// - `status`: Optional task completion status
232/// 
233/// # Returns
234/// 
235/// - `Ok(Literal)`: Successfully parsed literal value
236/// - `Err`: If parsing fails or type mismatch occurs
237fn from_task(input: &str, typedef: &Typedef, status: &Option<bool>) -> Result<Literal> {
238    if input.trim().is_empty() {
239        return Err(anyhow!("Nothing to assign"));
240    }
241    if typedef.is_task() {
242        return Ok(Literal::from_task(*status, input.to_string()))
243    }
244    // fall back to single
245    from_single(input, typedef)
246}
247
248/// Convert a multi-line response to a Value based on type definition
249/// 
250/// This function processes a list of items from a multi-line response.
251/// It can return either an array (if typedef specifies array) or the first valid value.
252/// 
253/// # Parameters
254/// 
255/// - `multiline`: Vector of items from multi-line response
256/// - `typedef`: The type definition specifying expected data type
257/// 
258/// # Returns
259/// 
260/// - `Ok(Value)`: Successfully converted value (array or single value)
261/// - `Err`: If no valid items found or type mismatch occurs
262fn from_multiline(multiline: &Vec<Item>, typedef: &Typedef) -> Result<Value> {
263    if multiline.is_empty() {
264        return Err(anyhow!("Nothing to assign"));
265    }
266    let mut array: Vec<Box<Value>> = Vec::new();
267    // Iterate multiline response
268    for item in multiline.iter() {
269        // Parse each item
270        let result = match item {
271            Item::Value(_, input) => {
272                from_single(input, typedef)
273            }
274            Item::Task(_, status, input) => {
275                from_task(input, typedef, status)
276            }
277        };
278        // Accept valid type safe results
279        if let Ok(literal) = result {
280            // Build the array
281            if typedef.is_array() {
282                array.push(Box::new(Value::Literal(literal)));
283            // Or return first value
284            } else {
285                return Ok(Value::Literal(literal));
286            }
287        }
288    }
289    if array.len() > 0 {
290        return Ok(Value::Array(array));
291    }
292    Err(anyhow!("Type definition mismatch"))
293}
294
295impl Response {
296    /// Create a new Response instance from an item and list
297    /// 
298    /// This constructor determines whether to create a Single or Multiline response
299    /// based on the length of the provided list.
300    /// 
301    /// # Parameters
302    /// 
303    /// - `item`: The first item in the response
304    /// - `list`: The complete list of items (including the first item)
305    /// 
306    /// # Returns
307    /// 
308    /// - `Response::Single`: If the list contains only one item
309    /// - `Response::Multiline`: If the list contains multiple items
310    pub fn new(item: &Item, list: &Vec<Item>) -> Self {
311        if list.len() > 1 {
312            Response::Multiline(list.clone())
313        } else {
314            Response::Single(item.clone())
315        }
316    }
317
318    /// Convert the response to a Value based on type definition
319    /// 
320    /// This method performs type-safe conversion of the response content
321    /// according to the provided type definition. It handles both single
322    /// and multi-line responses appropriately.
323    /// 
324    /// # Parameters
325    /// 
326    /// - `typedef`: The type definition specifying expected data type
327    /// 
328    /// # Returns
329    /// 
330    /// - `Ok(Value)`: Successfully converted value
331    /// - `Err`: If conversion fails due to type mismatch or parsing errors
332    /// 
333    /// # Examples
334    /// 
335    /// ```rust
336    /// use aimx::inference::{Response, Item};
337    /// use aimx::typedef::Typedef;
338    /// use aimx::{Value, Literal, Prefix};
339    /// 
340    /// let item = Item::Value(Prefix::None, "42".to_string());
341    /// let response = Response::Single(item);
342    /// let typedef = Typedef::Number;
343    /// 
344    /// match response.to_value(&typedef) {
345    ///     Ok(Value::Literal(Literal::Number(n))) => assert_eq!(n, 42.0),
346    ///     _ => panic!("Unexpected result"),
347    /// }
348    /// ```
349    pub fn to_value(&self, typedef: &Typedef) -> Result<Value> {
350        match self {
351            Response::Single(item) => {
352                let literal = match item {
353                    Item::Value(_, input) => {
354                        from_single(input, typedef)?
355                    }
356                    Item::Task(_, status, input) => {
357                        from_task(input, typedef, status)?
358                    }
359                };
360                // Convert to array if typedef is array
361                if typedef.is_array() {
362                    let mut array: Vec<Box<Value>> = Vec::new();
363                    array.push(Box::new(Value::Literal(literal)));
364                    return Ok(Value::Array(array));
365                }
366                Ok(Value::Literal(literal))
367            }
368            Response::Multiline(multiline) => {
369                from_multiline(multiline, typedef)
370            }
371        }
372    }
373}