Skip to main content

agentsight_capture/
text.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4use serde_json::Value;
5
6pub fn sanitize_ascii_identifier(value: &str) -> String {
7    value
8        .chars()
9        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
10        .collect()
11}
12
13pub fn truncate_text(text: &str, max: usize) -> String {
14    if text.chars().count() <= max {
15        text.to_string()
16    } else {
17        text.chars().take(max.saturating_sub(1)).collect()
18    }
19}
20
21pub fn truncate_with_ellipsis(text: &str, max: usize) -> String {
22    if text.chars().count() <= max {
23        text.to_string()
24    } else {
25        format!(
26            "{}...",
27            text.chars().take(max.saturating_sub(3)).collect::<String>()
28        )
29    }
30}
31
32pub fn clean_prompt_text(text: &str) -> Option<String> {
33    let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
34    let text = text
35        .strip_prefix("<session>")
36        .and_then(|text| text.strip_suffix("</session>"))
37        .unwrap_or(&text)
38        .trim();
39    (!text.is_empty()).then(|| text.to_string())
40}
41
42pub fn extract_prompt_text(value: &Value) -> Option<String> {
43    if let Some(prompt) = value.get("prompt").and_then(Value::as_str) {
44        return clean_prompt_text(prompt);
45    }
46    let mut parts = Vec::new();
47    for key in ["messages", "contents", "input"] {
48        if let Some(items) = value.get(key).and_then(Value::as_array) {
49            for item in items {
50                collect_content_text(item.get("content").unwrap_or(item), &mut parts);
51            }
52        }
53    }
54    clean_prompt_text(&parts.join(" "))
55}
56
57fn collect_content_text(value: &Value, out: &mut Vec<String>) {
58    match value {
59        Value::String(text) => out.push(text.clone()),
60        Value::Array(items) => items
61            .iter()
62            .for_each(|item| collect_content_text(item, out)),
63        Value::Object(obj) => {
64            for key in ["text", "content", "parts"] {
65                if let Some(value) = obj.get(key) {
66                    collect_content_text(value, out);
67                }
68            }
69        }
70        _ => {}
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::extract_prompt_text;
77    use serde_json::json;
78
79    #[test]
80    fn extracts_openai_responses_input_text() {
81        let request = json!({
82            "model": "gpt-agentsight-mock",
83            "input": [
84                {
85                    "role": "user",
86                    "content": [
87                        {"type": "input_text", "text": "agentsight mock prompt collect this exact text"}
88                    ]
89                }
90            ]
91        });
92
93        assert_eq!(
94            extract_prompt_text(&request).as_deref(),
95            Some("agentsight mock prompt collect this exact text")
96        );
97    }
98}