a3s-code-core 5.2.2

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
use crate::hooks::PreContextPerceptionEvent;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct IntentDetectionResult {
    pub(super) detected_intent: String,
    pub(super) confidence: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(super) target_hints: Option<TargetHints>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct TargetHints {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(super) target_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(super) target_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(super) domain: Option<String>,
}

pub(super) fn detect_local_context_perception_intent(
    prompt: &str,
    session_id: &str,
    workspace: &str,
) -> Option<PreContextPerceptionEvent> {
    let lower = prompt.to_lowercase();

    let intents: &[(&[&str], &str)] = &[
        (
            &[
                "where is",
                "where are",
                "find the file",
                "find all",
                "find files",
                "who wrote",
                "locate",
                "search for",
                "look for",
                "search",
            ],
            "locate",
        ),
        (
            &[
                "how does",
                "what does",
                "explain",
                "understand",
                "what is this",
                "how does this work",
            ],
            "understand",
        ),
        (
            &[
                "remember",
                "earlier",
                "before",
                "previously",
                "last time",
                "past",
                "previous",
            ],
            "retrieve",
        ),
        (
            &[
                "how is organized",
                "project structure",
                "what files",
                "show me the structure",
                "explore",
            ],
            "explore",
        ),
        (
            &[
                "why did",
                "why is",
                "cause",
                "reason",
                "what happened",
                "why does",
            ],
            "reason",
        ),
        (
            &["is this correct", "verify", "validate", "check if", "debug"],
            "validate",
        ),
        (
            &[
                "difference between",
                "compare",
                "versus",
                " vs ",
                "different from",
            ],
            "compare",
        ),
        (
            &[
                "status",
                "progress",
                "how far",
                "history",
                "what's the current",
            ],
            "track",
        ),
    ];

    let target_type = if lower.contains("function") || lower.contains("method") {
        "function"
    } else if lower.contains("file") || lower.contains("config") {
        "file"
    } else if lower.contains("class") {
        "entity"
    } else if lower.contains("module") || lower.contains("package") {
        "module"
    } else if lower.contains("test") {
        "test"
    } else {
        "unknown"
    };

    let matched_intent = intents
        .iter()
        .find(|(patterns, _)| patterns.iter().any(|p| lower.contains(p)));

    matched_intent.map(|(patterns, intent)| PreContextPerceptionEvent {
        session_id: session_id.to_string(),
        intent: intent.to_string(),
        target_type: target_type.to_string(),
        target_name: extract_target_name_from_prompt(prompt, patterns),
        domain: detect_domain_from_prompt(prompt),
        query: Some(prompt.to_string()),
        working_directory: workspace.to_string(),
        urgency: "normal".to_string(),
    })
}

pub(super) fn build_pre_context_perception_from_intent(
    result: IntentDetectionResult,
    prompt: &str,
    session_id: &str,
    workspace: &str,
) -> PreContextPerceptionEvent {
    let target_hints = result.target_hints;
    PreContextPerceptionEvent {
        session_id: session_id.to_string(),
        intent: result.detected_intent,
        target_type: target_hints
            .as_ref()
            .and_then(|h| h.target_type.clone())
            .unwrap_or_else(|| "unknown".to_string()),
        target_name: target_hints
            .as_ref()
            .and_then(|h| h.target_name.clone())
            .unwrap_or_else(|| extract_target_name_from_prompt(prompt, &[])),
        domain: target_hints
            .as_ref()
            .and_then(|h| h.domain.clone())
            .unwrap_or_else(|| detect_domain_from_prompt(prompt)),
        query: Some(prompt.to_string()),
        working_directory: workspace.to_string(),
        urgency: "normal".to_string(),
    }
}

pub(super) fn detect_language_hint(prompt: &str) -> Option<String> {
    if prompt
        .chars()
        .any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
    {
        return Some("zh".to_string());
    }
    if prompt
        .chars()
        .any(|c| ('\u{3040}'..='\u{309f}').contains(&c) || ('\u{30a0}'..='\u{30ff}').contains(&c))
    {
        return Some("ja".to_string());
    }
    if prompt
        .chars()
        .any(|c| ('\u{ac00}'..='\u{d7af}').contains(&c))
    {
        return Some("ko".to_string());
    }
    if prompt
        .chars()
        .any(|c| ('\u{0600}'..='\u{06ff}').contains(&c))
    {
        return Some("ar".to_string());
    }
    if prompt
        .chars()
        .any(|c| ('\u{0400}'..='\u{04ff}').contains(&c))
    {
        return Some("ru".to_string());
    }
    None
}

fn extract_target_name_from_prompt(prompt: &str, _patterns: &[&str]) -> String {
    if let Some(start) = prompt.find('"') {
        if let Some(end) = prompt[start + 1..].find('"') {
            return prompt[start + 1..start + 1 + end].to_string();
        }
    }

    if let Some(start) = prompt.find('\'') {
        if let Some(end) = prompt[start + 1..].find('\'') {
            return prompt[start + 1..start + 1 + end].to_string();
        }
    }

    if let Some(start) = prompt.find('`') {
        if let Some(end) = prompt[start + 1..].find('`') {
            return prompt[start + 1..start + 1 + end].to_string();
        }
    }

    let words: Vec<&str> = prompt.split_whitespace().collect();
    if words.len() > 2 {
        for word in words.iter() {
            if word.len() > 3
                && !["where", "what", "find", "the", "how", "is", "are"].contains(word)
            {
                return word.to_string();
            }
        }
    }

    String::new()
}

fn detect_domain_from_prompt(prompt: &str) -> String {
    let lower = prompt.to_lowercase();

    if lower.contains("rust") || lower.contains("cargo") || lower.contains(".rs") {
        "rust".to_string()
    } else if lower.contains("javascript")
        || lower.contains("typescript")
        || lower.contains("node")
        || lower.contains(".js")
        || lower.contains(".ts")
    {
        "javascript".to_string()
    } else if lower.contains("python") || lower.contains(".py") {
        "python".to_string()
    } else if lower.contains("go") || lower.contains(".go") {
        "go".to_string()
    } else if lower.contains("java") || lower.contains(".java") {
        "java".to_string()
    } else if lower.contains("docker") || lower.contains("container") {
        "docker".to_string()
    } else if lower.contains("kubernetes") || lower.contains("k8s") {
        "kubernetes".to_string()
    } else if lower.contains("sql")
        || lower.contains("database")
        || lower.contains("postgres")
        || lower.contains("mysql")
    {
        "database".to_string()
    } else if lower.contains("api") || lower.contains("rest") || lower.contains("grpc") {
        "api".to_string()
    } else if lower.contains("auth")
        || lower.contains("login")
        || lower.contains("password")
        || lower.contains("token")
    {
        "security".to_string()
    } else if lower.contains("test") || lower.contains("spec") || lower.contains("mock") {
        "testing".to_string()
    } else {
        "general".to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn local_detection_extracts_intent_and_target() {
        let event = detect_local_context_perception_intent(
            "find the file `src/agent.rs`",
            "s1",
            "/workspace",
        )
        .unwrap();

        assert_eq!(event.intent, "locate");
        assert_eq!(event.target_type, "file");
        assert_eq!(event.target_name, "src/agent.rs");
        assert_eq!(event.domain, "rust");
    }

    #[test]
    fn harness_intent_hints_override_local_fallbacks() {
        let event = build_pre_context_perception_from_intent(
            IntentDetectionResult {
                detected_intent: "validate".to_string(),
                confidence: 0.9,
                target_hints: Some(TargetHints {
                    target_type: Some("module".to_string()),
                    target_name: Some("runtime".to_string()),
                    domain: Some("agent".to_string()),
                }),
            },
            "debug runtime",
            "s2",
            "/workspace",
        );

        assert_eq!(event.intent, "validate");
        assert_eq!(event.target_type, "module");
        assert_eq!(event.target_name, "runtime");
        assert_eq!(event.domain, "agent");
    }

    #[test]
    fn language_hint_detects_cjk_prompts() {
        assert_eq!(detect_language_hint("继续优化"), Some("zh".to_string()));
        assert_eq!(detect_language_hint("plain ascii"), None);
    }
}