Skip to main content

lean_ctx/core/ocla/builtin/
intent_classifier.rs

1//! BuiltinIntentClassifier — classifies request intent from candidates.
2//!
3//! Wraps `core/intent_engine.rs` behind the OCLA trait. Emits IntentClassified
4//! events to OclaBus. Selects the highest-confidence intent from candidates.
5
6use crate::core::intent_engine;
7use crate::core::ocla::traits::{IntentClassifier, OclaService};
8use crate::core::ocla::types::{
9    IntentDecision, IntentRequest, OclaCapability, OclaCapabilityKind, OclaResult,
10};
11use crate::core::ocla_bus::{self, OclaEvent};
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 capability(&self) -> OclaCapability {
29        OclaCapability::available(OclaCapabilityKind::IntentClassifier)
30    }
31}
32
33impl IntentClassifier for BuiltinIntentClassifier {
34    fn classify_intent(&self, request: IntentRequest) -> OclaResult<IntentDecision> {
35        let (intent, confidence) = request
36            .candidate_intents
37            .iter()
38            .filter_map(|candidate| engine_score(candidate).map(|score| (candidate, score)))
39            .max_by(|(_, left), (_, right)| left.total_cmp(right))
40            .map_or_else(
41                || fallback_decision(&request.candidate_intents),
42                |(candidate, score)| (candidate.clone(), confidence_milli(score)),
43            );
44
45        ocla_bus::emit(OclaEvent::IntentClassified {
46            tier: intent.clone(),
47            confidence: f64::from(confidence) / 1000.0,
48            reasoning: format!("builtin:{}", request.context.request_id),
49        });
50
51        Ok(IntentDecision {
52            intent,
53            confidence_milli: confidence,
54            rationale_ref: None,
55        })
56    }
57}
58
59fn engine_score(candidate: &str) -> Option<f64> {
60    std::panic::catch_unwind(|| intent_engine::classify(candidate).confidence)
61        .ok()
62        .filter(|confidence| confidence.is_finite())
63}
64
65fn confidence_milli(confidence: f64) -> u16 {
66    (confidence.clamp(0.0, 1.0) * 1000.0).round() as u16
67}
68
69fn fallback_decision(candidates: &[String]) -> (String, u16) {
70    (
71        candidates
72            .first()
73            .cloned()
74            .unwrap_or_else(|| "unknown".to_string()),
75        0,
76    )
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::core::ocla::types::OclaRequestContext;
83
84    fn req(intents: &[&str]) -> IntentRequest {
85        IntentRequest {
86            context: OclaRequestContext {
87                request_id: "r1".into(),
88                session_id: "s1".into(),
89                agent_id: "agent-test".into(),
90                content_ref: "ref:test".into(),
91                tenant_id: None,
92                trace_id: "tr-unit".into(),
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}