tj_core/classifier/
mod.rs1use crate::event::{EventType, EvidenceStrength};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize)]
8pub struct ClassifyInput {
9 pub text: String,
10 pub author_hint: String,
11 pub recent_tasks: Vec<TaskContext>,
12}
13
14#[derive(Debug, Clone, Serialize)]
15pub struct TaskContext {
16 pub task_id: String,
17 pub title: String,
18 pub last_events: Vec<String>,
19}
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct ClassifyOutput {
23 pub event_type: EventType,
24 pub task_id_guess: Option<String>,
25 pub confidence: f64,
26 pub evidence_strength: Option<EvidenceStrength>,
27 pub suggested_text: String,
28 #[serde(default)]
35 pub artifacts: Option<crate::artifacts::Artifacts>,
36}
37
38pub trait Classifier: Send + Sync {
39 fn classify(&self, input: &ClassifyInput) -> anyhow::Result<ClassifyOutput>;
40}
41
42use crate::event::EventStatus;
43
44pub const CONFIDENCE_THRESHOLD: f64 = 0.85;
45
46pub fn decide_status(confidence: f64) -> EventStatus {
47 if confidence >= CONFIDENCE_THRESHOLD {
48 EventStatus::Confirmed
49 } else {
50 EventStatus::Suggested
51 }
52}
53
54pub mod cli;
55pub mod heuristic;
56pub mod http;
57pub mod hybrid;
58pub mod mock;
59pub mod prompt;
60pub mod telemetry;
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
70 fn tj_classifier_model_env_var_overrides_defaults_for_both_backends() {
71 let prev_model = std::env::var("TJ_CLASSIFIER_MODEL").ok();
72 let prev_key = std::env::var("ANTHROPIC_API_KEY").ok();
73
74 unsafe {
77 std::env::remove_var("TJ_CLASSIFIER_MODEL");
78 }
79
80 let cli_default = cli::ClaudeCliClassifier::default();
81 assert_eq!(cli_default.model, cli::DEFAULT_MODEL);
82
83 unsafe {
84 std::env::set_var("ANTHROPIC_API_KEY", "test-key-do-not-use");
85 }
86 let http_default = http::AnthropicClassifier::from_env().unwrap();
87 assert_eq!(http_default.model, http::DEFAULT_MODEL);
88
89 unsafe {
91 std::env::set_var("TJ_CLASSIFIER_MODEL", "sonnet-override");
92 }
93 let cli_override = cli::ClaudeCliClassifier::default();
94 assert_eq!(cli_override.model, "sonnet-override");
95
96 let http_override = http::AnthropicClassifier::from_env().unwrap();
97 assert_eq!(http_override.model, "sonnet-override");
98
99 unsafe {
101 match prev_model {
102 Some(v) => std::env::set_var("TJ_CLASSIFIER_MODEL", v),
103 None => std::env::remove_var("TJ_CLASSIFIER_MODEL"),
104 }
105 match prev_key {
106 Some(v) => std::env::set_var("ANTHROPIC_API_KEY", v),
107 None => std::env::remove_var("ANTHROPIC_API_KEY"),
108 }
109 }
110 }
111
112 #[test]
113 fn classify_input_serializes() {
114 let i = ClassifyInput {
115 text: "Adopted Rust for the journal".into(),
116 author_hint: "assistant".into(),
117 recent_tasks: vec![],
118 };
119 let s = serde_json::to_string(&i).unwrap();
120 assert!(s.contains("Adopted Rust"));
121 }
122
123 #[test]
124 fn decide_status_high_confidence_is_confirmed() {
125 assert_eq!(decide_status(0.95), EventStatus::Confirmed);
126 assert_eq!(decide_status(0.85), EventStatus::Confirmed);
127 }
128
129 #[test]
130 fn decide_status_low_confidence_is_suggested() {
131 assert_eq!(decide_status(0.84), EventStatus::Suggested);
132 assert_eq!(decide_status(0.0), EventStatus::Suggested);
133 }
134}