ai-lib-core 0.9.6

AI-Protocol execution runtime core (protocol, client, pipeline, transport)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! JSONPath evaluator and path mapper for frame selection and field extraction
//!
//! Inspired by ai-lib's PathMapper implementation, with support for:
//! - Nested path access (e.g., "a.b.c")
//! - Array indexing (e.g., "choices[0].delta.content")
//! - Condition evaluation (exists, ==, !=, in, &&, ||, >, <, >=, <=)
//! - Regular expression matching

use serde_json::{json, Value};
use std::collections::HashMap;

/// Path mapper error
#[derive(Debug, thiserror::Error)]
pub enum PathMapperError {
    #[error("Invalid path: {0}")]
    InvalidPath(String),

    #[error("Cannot set value at path: {0}")]
    CannotSetValue(String),
}

/// Path mapper for extracting and setting values in JSON using dot-notation paths
pub struct PathMapper;

impl PathMapper {
    /// Get value from JSON using dot-notation path (supports array indexing)
    ///
    /// Examples:
    /// - "choices[0].delta.content"
    /// - "input.temperature"
    /// - "delta.text"
    pub fn get_path<'a>(obj: &'a Value, path: &str) -> Option<&'a Value> {
        if path.is_empty() {
            return None;
        }

        // Remove leading "$." if present (JSONPath style)
        let normalized = path.trim().trim_start_matches("$.").to_string();
        let parts: Vec<&str> = normalized.split('.').collect();
        let mut current = obj;

        for part in parts {
            if part.is_empty() {
                return None;
            }

            // Check if part contains array index, e.g., "choices[0]"
            if let Some(bracket_pos) = part.find('[') {
                // Extract key and index
                let key = &part[..bracket_pos];
                let idx_str = part[bracket_pos + 1..].trim_end_matches(']');

                // First access the object key
                if !key.is_empty() {
                    match current {
                        Value::Object(map) => {
                            current = map.get(key)?;
                        }
                        _ => return None,
                    }
                }

                // Then access the array index
                if let Ok(idx) = idx_str.parse::<usize>() {
                    match current {
                        Value::Array(arr) => {
                            current = arr.get(idx)?;
                        }
                        _ => return None,
                    }
                } else if idx_str == "*" {
                    // Wildcard: get first element
                    match current {
                        Value::Array(arr) => {
                            current = arr.first()?;
                        }
                        _ => return None,
                    }
                } else {
                    return None;
                }
            } else {
                // Simple key access OR dot-index access (e.g. "choices.0.delta")
                match current {
                    Value::Object(map) => {
                        current = map.get(part)?;
                    }
                    Value::Array(arr) => {
                        // Support "0" / "1" style index segments (common in some JSONPath variants)
                        if let Ok(idx) = part.parse::<usize>() {
                            current = arr.get(idx)?;
                        } else if part == "*" {
                            current = arr.first()?;
                        } else {
                            return None;
                        }
                    }
                    _ => return None,
                }
            }
        }

        Some(current)
    }

    /// Get string value from path (converts number to string if needed, null returns None)
    pub fn get_string(obj: &Value, path: &str) -> Option<String> {
        Self::get_path(obj, path).and_then(|v| {
            if v.is_null() {
                return None;
            }
            if v.is_string() {
                v.as_str().map(|s| s.to_string())
            } else {
                serde_json::to_string(v).ok()
            }
        })
    }

    /// Set value at nested path in JSON object
    ///
    /// Examples:
    /// - "input.temperature" -> sets obj["input"]["temperature"]
    /// - "generationConfig.maxOutputTokens" -> sets obj["generationConfig"]["maxOutputTokens"]
    pub fn set_path(obj: &mut Value, path: &str, value: Value) -> Result<(), PathMapperError> {
        if path.is_empty() {
            return Err(PathMapperError::InvalidPath("Empty path".to_string()));
        }

        // Remove leading "$." if present
        let normalized = path.trim().trim_start_matches("$.").to_string();
        let parts: Vec<&str> = normalized.split('.').collect();

        if parts.is_empty() {
            return Err(PathMapperError::InvalidPath("Empty path parts".to_string()));
        }

        // Ensure root object is Object
        if !obj.is_object() {
            *obj = json!({});
        }

        let mut current = obj
            .as_object_mut()
            .ok_or_else(|| PathMapperError::CannotSetValue("Root is not an object".to_string()))?;

        // Process all but the last path segment
        for (idx, part) in parts.iter().enumerate().take(parts.len() - 1) {
            if part.is_empty() {
                return Err(PathMapperError::InvalidPath(format!(
                    "Empty path part at index {}",
                    idx
                )));
            }

            // If path doesn't exist or is not an object, create new object
            if !current.contains_key(*part) || !current[*part].is_object() {
                current.insert(part.to_string(), json!({}));
            }

            // Move to next level
            current = current[*part].as_object_mut().ok_or_else(|| {
                PathMapperError::CannotSetValue(format!("Cannot access object at path: {}", part))
            })?;
        }

        // Set the last path segment's value
        let last_part = parts
            .last()
            .ok_or_else(|| PathMapperError::InvalidPath("No last part".to_string()))?;

        if last_part.is_empty() {
            return Err(PathMapperError::InvalidPath(
                "Last path part is empty".to_string(),
            ));
        }

        current.insert(last_part.to_string(), value);
        Ok(())
    }

    /// Batch set multiple paths
    pub fn set_paths(
        obj: &mut Value,
        paths: &HashMap<String, Value>,
    ) -> Result<(), PathMapperError> {
        for (path, value) in paths {
            Self::set_path(obj, path, value.clone())?;
        }
        Ok(())
    }
}

/// JSONPath evaluator for condition matching
/// Supports: exists, ==, !=, in, &&, ||, >, <, >=, <=, regex
#[derive(Clone)]
pub struct JsonPathEvaluator {
    expression: String,
}

impl JsonPathEvaluator {
    pub fn new(expression: &str) -> Result<Self, String> {
        if expression.is_empty() {
            return Err("Empty expression".to_string());
        }
        Ok(Self {
            expression: expression.to_string(),
        })
    }

    /// Check if expression matches the JSON value
    ///
    /// Supports:
    /// - exists($.path) - check if path exists
    /// - $.path == "value" - equality check
    /// - $.path != "value" - inequality check
    /// - $.path in ['value1', 'value2'] - list membership
    /// - $.path != null / $.path == null - null check
    /// - $.path > 10 / $.path < 10 - numeric comparison
    /// - $.path >= 10 / $.path <= 10 - numeric comparison
    /// - $.path =~ /pattern/ - regex matching
    /// - && and || for logical combination
    pub fn matches(&self, value: &Value) -> bool {
        Self::evaluate_match(&self.expression, value)
    }

    /// Extract string value from JSON using path
    pub fn extract_string(&self, value: &Value) -> Option<String> {
        // If expression is a simple path, extract it
        if self.expression.starts_with("$.") {
            return PathMapper::get_string(value, &self.expression);
        }
        None
    }

    /// Evaluate match expression with support for numeric comparisons and regex
    fn evaluate_match(expr: &str, root: &Value) -> bool {
        // Split by OR
        let or_parts: Vec<&str> = expr.split("||").collect();
        for or_part in or_parts {
            let mut ok = true;
            // Split by AND
            let and_parts: Vec<&str> = or_part.split("&&").collect();
            for part in and_parts {
                let cond = part.trim();
                if cond.is_empty() {
                    continue;
                }

                // exists() check — null values are treated as non-existent
                if cond.starts_with("exists(") && cond.ends_with(')') {
                    let path = cond.trim_start_matches("exists(").trim_end_matches(')');
                    match PathMapper::get_path(root, path) {
                        Some(v) if !v.is_null() => {}
                        _ => {
                            ok = false;
                            break;
                        }
                    }
                    continue;
                }

                // Regex matching: $.path =~ /pattern/
                if let Some(idx) = cond.find("=~") {
                    let (path, rest) = cond.split_at(idx);
                    let path = path.trim();
                    let pattern_str = rest.trim_start_matches("=~").trim();

                    // Extract pattern from /pattern/ or "pattern"
                    let pattern = pattern_str
                        .trim_start_matches('/')
                        .trim_end_matches('/')
                        .trim_matches('"')
                        .trim_matches('\'');

                    if let Some(actual) = PathMapper::get_string(root, path) {
                        // Simple regex matching (for full implementation, use regex crate)
                        // For now, support basic wildcard patterns
                        if !Self::simple_regex_match(&actual, pattern) {
                            ok = false;
                            break;
                        }
                    } else {
                        ok = false;
                        break;
                    }
                    continue;
                }

                // Numeric comparisons: >, <, >=, <=
                for op in &[">=", "<=", ">", "<"] {
                    if let Some(idx) = cond.find(op) {
                        let (path, rest) = cond.split_at(idx);
                        let path = path.trim();
                        let target_str = rest
                            .trim_start_matches(op)
                            .trim()
                            .trim_matches('"')
                            .trim_matches('\'');

                        if let Ok(target_num) = target_str.parse::<f64>() {
                            if let Some(actual_val) = PathMapper::get_path(root, path) {
                                let actual_num = actual_val.as_f64().or_else(|| {
                                    actual_val.as_str().and_then(|s| s.parse::<f64>().ok())
                                });

                                if let Some(actual) = actual_num {
                                    let matches = match *op {
                                        ">" => actual > target_num,
                                        "<" => actual < target_num,
                                        ">=" => actual >= target_num,
                                        "<=" => actual <= target_num,
                                        _ => false,
                                    };
                                    if !matches {
                                        ok = false;
                                        break;
                                    }
                                    continue;
                                }
                            }
                        }
                        ok = false;
                        break;
                    }
                }

                // "in" list check
                if let Some(idx) = cond.find(" in ") {
                    let (path, rest) = cond.split_at(idx);
                    let path = path.trim();
                    let list_str = rest.trim_start_matches(" in ").trim();
                    let list_str = list_str.trim_start_matches('[').trim_end_matches(']');
                    let values: Vec<String> = list_str
                        .split(',')
                        .filter_map(|v| v.trim().trim_matches('\'').trim_matches('"').parse().ok())
                        .collect();
                    let actual = PathMapper::get_string(root, path);
                    if !actual.map(|a| values.contains(&a)).unwrap_or(false) {
                        ok = false;
                        break;
                    }
                    continue;
                }

                // "!= null" check
                if let Some(idx) = cond.find("!= null") {
                    let path = cond[..idx].trim();
                    let val = PathMapper::get_path(root, path);
                    if val.is_none() || val == Some(&Value::Null) {
                        ok = false;
                        break;
                    }
                    continue;
                }

                // "== null" check
                if let Some(idx) = cond.find("== null") {
                    let path = cond[..idx].trim();
                    let val = PathMapper::get_path(root, path);
                    if val.is_some() && val != Some(&Value::Null) {
                        ok = false;
                        break;
                    }
                    continue;
                }

                // "==" equality check
                if let Some(idx) = cond.find("==") {
                    let (path, value_part) = cond.split_at(idx);
                    let path = path.trim();
                    let target = value_part
                        .trim_start_matches("==")
                        .trim()
                        .trim_matches('\'')
                        .trim_matches('"');
                    let actual = PathMapper::get_string(root, path);
                    if actual.as_deref() != Some(target) {
                        ok = false;
                        break;
                    }
                    continue;
                }

                // "!=" inequality check
                if let Some(idx) = cond.find("!=") {
                    let (path, value_part) = cond.split_at(idx);
                    let path = path.trim();
                    let target = value_part
                        .trim_start_matches("!=")
                        .trim()
                        .trim_matches('\'')
                        .trim_matches('"');
                    let actual = PathMapper::get_string(root, path);
                    if actual.as_deref() == Some(target) {
                        ok = false;
                        break;
                    }
                    continue;
                }
            }
            if ok {
                return true;
            }
        }
        false
    }

    /// Simple regex matching (supports basic wildcards)
    /// For full regex support, use the `regex` crate
    fn simple_regex_match(text: &str, pattern: &str) -> bool {
        // Simple wildcard matching: * matches any sequence, ? matches any character
        if pattern.contains('*') || pattern.contains('?') {
            // Basic wildcard implementation
            let mut text_chars = text.chars();
            let mut pattern_chars = pattern.chars();

            while let Some(p) = pattern_chars.next() {
                match p {
                    '*' => {
                        // Match zero or more characters
                        if let Some(next_p) = pattern_chars.next() {
                            // Find next character in pattern
                            for t in text_chars.by_ref() {
                                if t == next_p {
                                    break;
                                }
                            }
                        } else {
                            // * at end matches rest
                            return true;
                        }
                    }
                    '?' => {
                        // Match any single character
                        if text_chars.next().is_none() {
                            return false;
                        }
                    }
                    c => {
                        if text_chars.next() != Some(c) {
                            return false;
                        }
                    }
                }
            }
            text_chars.next().is_none()
        } else {
            // Simple substring match
            text.contains(pattern)
        }
    }
}