lean_ctx/core/ocla/builtin/
intent_classifier.rs1use 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}