Skip to main content

lean_ctx/core/ocla/builtin/
intent_classifier.rs

1// SPDX-License-Identifier: Apache-2.0
2//! BuiltinIntentClassifier — classifies request intent from candidates.
3//!
4//! Wraps `core/intent_engine.rs` behind the OCLA trait. Selects the
5//! highest-confidence intent from candidates.
6
7use crate::core::intent_engine;
8use crate::core::ocla::traits::{IntentClassifier, OclaService};
9use crate::core::ocla::types::{
10    IntentDecision, IntentRequest, OclaCapability, OclaCapabilityKind, OclaResult,
11};
12
13pub struct BuiltinIntentClassifier;
14
15impl BuiltinIntentClassifier {
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21impl Default for BuiltinIntentClassifier {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl OclaService for BuiltinIntentClassifier {
28    fn manifest(&self) -> crate::core::ocla::OclaResult<lean_ctx_protocol::CapabilityManifestV1> {
29        crate::core::ocla::capability_fabric::builtin_manifest(&self.capability())
30    }
31
32    fn capability(&self) -> OclaCapability {
33        OclaCapability::available(OclaCapabilityKind::IntentClassifier)
34    }
35}
36
37impl IntentClassifier for BuiltinIntentClassifier {
38    fn classify_intent(&self, request: IntentRequest) -> OclaResult<IntentDecision> {
39        let (intent, confidence) = request
40            .candidate_intents
41            .iter()
42            .filter_map(|candidate| engine_score(candidate).map(|score| (candidate, score)))
43            .max_by(|(_, left), (_, right)| left.total_cmp(right))
44            .map_or_else(
45                || fallback_decision(&request.candidate_intents),
46                |(candidate, score)| (candidate.clone(), confidence_milli(score)),
47            );
48
49        Ok(IntentDecision {
50            intent,
51            confidence_milli: confidence,
52            rationale_ref: None,
53        })
54    }
55}
56
57fn engine_score(candidate: &str) -> Option<f64> {
58    std::panic::catch_unwind(|| intent_engine::classify(candidate).confidence)
59        .ok()
60        .filter(|confidence| confidence.is_finite())
61}
62
63fn confidence_milli(confidence: f64) -> u16 {
64    (confidence.clamp(0.0, 1.0) * 1000.0).round() as u16
65}
66
67fn fallback_decision(candidates: &[String]) -> (String, u16) {
68    (
69        candidates
70            .first()
71            .cloned()
72            .unwrap_or_else(|| "unknown".to_string()),
73        0,
74    )
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::core::ocla::types::OclaRequestContext;
81
82    fn req(intents: &[&str]) -> IntentRequest {
83        IntentRequest {
84            context: OclaRequestContext {
85                request_id: "r1".into(),
86                session_id: "s1".into(),
87                agent_id: "agent-test".into(),
88                content_ref: "ref:test".into(),
89                tenant_id: None,
90                trace_id: "tr-unit".into(),
91                task_id: None,
92                parent_task_id: None,
93            },
94            candidate_intents: intents.iter().map(|s| (*s).to_string()).collect(),
95        }
96    }
97
98    #[test]
99    fn single_candidate_high_confidence() {
100        let classifier = BuiltinIntentClassifier::new();
101        let decision = classifier
102            .classify_intent(req(&["fix the bug in parser.rs"]))
103            .unwrap();
104        assert_eq!(decision.intent, "fix the bug in parser.rs");
105        assert_eq!(decision.confidence_milli, 950);
106    }
107
108    #[test]
109    fn engine_selects_highest_confidence_candidate() {
110        let classifier = BuiltinIntentClassifier::new();
111        let decision = classifier
112            .classify_intent(req(&["review the parser", "fix the bug in parser.rs"]))
113            .unwrap();
114        assert_eq!(decision.intent, "fix the bug in parser.rs");
115        assert_eq!(decision.confidence_milli, 950);
116    }
117
118    #[test]
119    fn empty_candidates_unknown() {
120        let classifier = BuiltinIntentClassifier::new();
121        let decision = classifier.classify_intent(req(&[])).unwrap();
122        assert_eq!(decision.intent, "unknown");
123        assert_eq!(decision.confidence_milli, 0);
124    }
125
126    #[test]
127    fn registry_uses_engine_backed_classifier() {
128        use crate::core::ocla::OclaRegistry;
129
130        let decision = OclaRegistry::with_builtins()
131            .intent_classifier
132            .classify_intent(req(&["explain the cache", "debug the parser crash"]))
133            .unwrap();
134
135        assert_eq!(decision.intent, "debug the parser crash");
136        assert!(
137            decision.confidence_milli > 0,
138            "engine should produce non-zero confidence"
139        );
140    }
141}