aimx/inference/
response.rs

1//! Inference response parsing and conversion.
2//!
3//! Parses model responses into structured items keyed by label and converts them to
4//! [`Value`]s using [`Typedef`]s. Supports inline key-value pairs and multiline blocks
5//! with common prefixes and checkbox-style task items. Permissive by design so that
6//! strict validation can be applied in later stages.
7
8use crate::{
9    aim::Typedef,
10    inference::{Item, Suffix, parse_inline_item, parse_item, parse_key},
11    literals::{Literal, parse_date, parse_number},
12    values::Value,
13};
14use anyhow::{Result, anyhow};
15use std::{collections::HashMap, sync::Arc};
16
17/// Parsed inference response for a single key.
18///
19/// Either a single item or a multiline block extracted from model output.
20#[derive(Debug, PartialEq, Clone)]
21pub enum Response {
22    /// Single response item, e.g. from an inline `KEY: value`.
23    Single(Item),
24    /// Multiline response with items sharing a common prefix/pattern.
25    Multiline(Vec<Item>),
26}
27
28/// Parse formatted model output into a map from key to [`Response`].
29///
30/// Returns `None` if no valid keys or items are found.
31pub fn parse_response(input: &str) -> Option<HashMap<Arc<str>, Response>> {
32    let mut response_map: HashMap<Arc<str>, Response> = HashMap::new();
33    let mut current_list: Vec<Item> = Vec::new();
34    let mut current_key: Option<Arc<str>> = None;
35    let mut first_item: Option<Item> = None;
36
37    // Process each line
38    for line in input.lines() {
39        // Check if the line is a new key
40        if let Ok((input, (key, suffix))) = parse_key(line) {
41            // This marks a new response so insert any pending response
42            if let (Some(key), Some(item)) = (current_key, first_item) {
43                response_map.insert(key, Response::new(&item, &current_list));
44            }
45            match suffix {
46                Suffix::Colon => {
47                    // Store key and try to parse inline item
48                    if let Ok((_, item)) = parse_inline_item(input) {
49                        response_map.insert(key, Response::Single(item));
50                    }
51                    current_key = None;
52                }
53                Suffix::Eol | Suffix::ColonEol => {
54                    // Store the new key which changes the state to multiline
55                    current_key = Some(key);
56                }
57            }
58            first_item = None;
59            current_list.clear();
60
61        // Check for multiline state
62        } else if let Some(key) = &current_key {
63            // Check if we have a first item
64            if let Some(item) = &first_item {
65                if let Ok((_, result)) = parse_item(line) {
66                    // Check the prefixes match
67                    let prefixes_match = match (&item, &result) {
68                        (Item::Value(prefix1, _), Item::Value(prefix2, _)) => prefix1 == prefix2,
69                        (Item::Task(prefix1, _, _), Item::Task(prefix2, _, _)) => {
70                            prefix1 == prefix2
71                        }
72                        _ => false, // Different item types
73                    };
74                    if prefixes_match {
75                        current_list.push(result);
76                    } else {
77                        response_map.insert(key.clone(), Response::new(item, &current_list));
78                        current_key = None;
79                        first_item = None;
80                        current_list.clear();
81                    }
82                } else {
83                    response_map.insert(key.clone(), Response::new(item, &current_list));
84                    current_key = None;
85                    first_item = None;
86                    current_list.clear();
87                }
88            }
89            // Parse the first item
90            else if let Ok((_, result)) = parse_item(line) {
91                current_list.push(result.clone());
92                first_item = Some(result);
93            }
94            // No result
95            else {
96                current_key = None;
97            }
98        }
99    }
100    // This marks the end of the input so insert any pending response
101    if let (Some(key), Some(item)) = (current_key, first_item) {
102        response_map.insert(key, Response::new(&item, &current_list));
103    }
104
105    // Only return the map if we successfully parsed at least one item
106    if response_map.is_empty() {
107        None
108    } else {
109        Some(response_map)
110    }
111}
112
113/// Parse a single value using [`Typedef`] shape hints.
114///
115/// Supports number, date, and text typedefs; fails for incompatible typedefs or
116/// empty input.
117fn from_single(input: &str, typedef: &Typedef) -> Result<Literal> {
118    if input.trim().is_empty() {
119        return Err(anyhow!("Nothing to assign"));
120    }
121    if typedef.is_number() {
122        match parse_number(input) {
123            Ok((_, number)) => return Ok(Literal::from_number(number)),
124            Err(e) => return Err(anyhow!("Parse error: {}", e)),
125        }
126    }
127    if typedef.is_date() {
128        match parse_date(input) {
129            Ok((_, date)) => return Ok(Literal::from_date(date)),
130            Err(e) => return Err(anyhow!("Parse error: {}", e)),
131        }
132    }
133    if typedef.is_text() {
134        return Ok(Literal::from_text(Arc::from(input)));
135    }
136    Err(anyhow!("Type definition mismatch"))
137}
138
139/// Parse a task-style item using [`Typedef`] hints.
140///
141/// Uses task typedefs when available; otherwise falls back to [`from_single`].
142/// Fails on empty input.
143fn from_task(input: &str, typedef: &Typedef, status: &Option<bool>) -> Result<Literal> {
144    if input.trim().is_empty() {
145        return Err(anyhow!("Nothing to assign"));
146    }
147    if typedef.is_task() {
148        return Ok(Literal::from_task(*status, Arc::from(input)));
149    }
150    // fall back to single
151    from_single(input, typedef)
152}
153
154/// Convert a multiline block into a [`Value`] using [`Typedef`] hints.
155///
156/// For array typedefs collects all successfully parsed items; otherwise returns the
157/// first successfully parsed item.
158fn from_multiline(multiline: &[Item], typedef: &Typedef) -> Result<Arc<Value>> {
159    if multiline.is_empty() {
160        return Err(anyhow!("Nothing to assign"));
161    }
162    let mut array: Vec<Arc<Value>> = Vec::new();
163    // Iterate multiline response
164    for item in multiline.iter() {
165        // Parse each item
166        let result = match item {
167            Item::Value(_, input) => from_single(input, typedef),
168            Item::Task(_, status, input) => from_task(input, typedef, status),
169        };
170        // Accept valid type safe results
171        if let Ok(literal) = result {
172            // Build the array
173            if typedef.is_array() {
174                array.push(Arc::new(Value::Literal(literal)));
175            // Or return first value
176            } else {
177                return Ok(Arc::new(Value::Literal(literal)));
178            }
179        }
180    }
181    if !array.is_empty() {
182        return Ok(Arc::new(Value::Array(Arc::new(array))));
183    }
184    Err(anyhow!("Type definition mismatch"))
185}
186
187impl Response {
188    /// Create a [`Response`] from the collected items for a key.
189    ///
190    /// Uses [`Response::Single`] when `list` has one item, [`Response::Multiline`] otherwise.
191    pub fn new(item: &Item, list: &[Item]) -> Self {
192        if list.len() > 1 {
193            Response::Multiline(list.to_vec())
194        } else {
195            Response::Single(item.clone())
196        }
197    }
198
199    /// Convert this response to a [`Value`] using the given [`Typedef`].
200    ///
201    /// Respects array/task typedefs; errors on type mismatch or invalid content.
202    pub fn to_value(&self, typedef: &Typedef) -> Result<Arc<Value>> {
203        match self {
204            Response::Single(item) => {
205                let literal = match item {
206                    Item::Value(_, input) => from_single(input, typedef)?,
207                    Item::Task(_, status, input) => from_task(input, typedef, status)?,
208                };
209                // Convert to array if typedef is array
210                if typedef.is_array() {
211                    let array: Vec<Arc<Value>> = vec![Arc::new(Value::Literal(literal))];
212                    return Ok(Arc::new(Value::Array(Arc::new(array))));
213                }
214                Ok(Arc::new(Value::Literal(literal)))
215            }
216            Response::Multiline(multiline) => from_multiline(multiline, typedef),
217        }
218    }
219}