lean_ctx/core/triage/
semantic_analyzer.rs1use std::path::{Path, PathBuf};
4
5use super::{
6 ProfileHypothesis, TaskAnalysisInput, TaskAnalyzer, TriageBackendLocal, TriageError,
7 model_loader::ModelLoader, profile::TaskProfileLocal,
8};
9
10#[derive(Debug)]
11pub struct SemanticAnalyzer {
12 model_path: PathBuf,
13 model: Option<ModelLoader>,
14}
15
16impl SemanticAnalyzer {
17 pub fn from_data_dir(data_dir: &Path) -> Self {
19 Self::from_model_path(super::model_loader::model_path(data_dir))
20 }
21
22 pub fn from_model_path(model_path: impl Into<PathBuf>) -> Self {
23 let model_path = model_path.into();
24 Self {
25 model: ModelLoader::from_model_path(&model_path).ok().flatten(),
26 model_path,
27 }
28 }
29
30 pub fn model_path(&self) -> &Path {
31 &self.model_path
32 }
33
34 pub fn model_available(&self) -> bool {
35 self.model.is_some()
36 }
37
38 fn fallback(&self) -> ProfileHypothesis {
39 let profile = TaskProfileLocal {
40 task_class: "coding".into(),
41 intent: "needs_semantic_model".into(),
42 confidence_milli: 0,
43 ..Default::default()
44 };
45 ProfileHypothesis {
46 profile,
47 confidence_milli: 0,
48 backend: TriageBackendLocal::Semantic,
49 }
50 }
51}
52
53impl TaskAnalyzer for SemanticAnalyzer {
54 fn analyze(&self, input: &TaskAnalysisInput) -> Result<ProfileHypothesis, TriageError> {
55 Ok(self
56 .model
57 .as_ref()
58 .and_then(|model| model.infer(input).ok())
59 .unwrap_or_else(|| self.fallback()))
60 }
61
62 fn name(&self) -> &'static str {
63 "semantic"
64 }
65
66 fn shadow_enabled(&self) -> bool {
67 self.model_available()
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn missing_model_returns_low_confidence_fallback() {
77 let analyzer = SemanticAnalyzer::from_model_path("missing-triage-model.onnx");
78 let result = analyzer.analyze(&TaskAnalysisInput::default()).unwrap();
79
80 assert!(!analyzer.model_available());
81 assert_eq!(result.confidence_milli, 0);
82 assert_eq!(result.profile.intent, "needs_semantic_model");
83 }
84
85 #[test]
86 fn data_dir_constructor_uses_managed_model_path() {
87 let data = tempfile::tempdir().unwrap();
88 let analyzer = SemanticAnalyzer::from_data_dir(data.path());
89 assert_eq!(
90 analyzer.model_path(),
91 data.path().join("models/triage-v1.onnx")
92 );
93 assert!(!analyzer.model_available());
94 }
95}