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, ¤t_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) = ¤t_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, ¤t_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, ¤t_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, ¤t_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}