1use super::traits::{ChatRequest, TaskClassifier, TaskProfile, TaskType};
11
12struct KeywordPattern {
14 keywords: &'static [&'static str],
15 task_type: TaskType,
16}
17
18const PATTERNS: &[KeywordPattern] = &[
23 KeywordPattern {
25 keywords: &[
26 "code",
27 "function",
28 "implement",
29 "debug",
30 "fix",
31 "program",
32 "script",
33 "compile",
34 "refactor",
35 "class",
36 "struct",
37 "module",
38 ],
39 task_type: TaskType::CodeGeneration,
40 },
41 KeywordPattern {
43 keywords: &["review", "check", "audit", "lint", "inspect"],
44 task_type: TaskType::CodeReview,
45 },
46 KeywordPattern {
48 keywords: &[
49 "search", "find", "research", "look up", "lookup", "discover",
50 ],
51 task_type: TaskType::Research,
52 },
53 KeywordPattern {
55 keywords: &[
56 "write",
57 "story",
58 "poem",
59 "creative",
60 "compose",
61 "draft",
62 "narrative",
63 ],
64 task_type: TaskType::Creative,
65 },
66 KeywordPattern {
68 keywords: &[
69 "analyze",
70 "explain",
71 "summarize",
72 "compare",
73 "evaluate",
74 "assess",
75 ],
76 task_type: TaskType::Analysis,
77 },
78 KeywordPattern {
80 keywords: &["use tool", "run tool", "execute", "call function"],
81 task_type: TaskType::ToolUse,
82 },
83];
84
85pub struct KeywordClassifier;
97
98impl KeywordClassifier {
99 pub fn new() -> Self {
101 Self
102 }
103}
104
105impl Default for KeywordClassifier {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl TaskClassifier for KeywordClassifier {
112 fn classify(&self, request: &ChatRequest) -> TaskProfile {
113 let text = last_user_message(request);
115 let text_lower = text.to_lowercase();
116
117 let mut matched_type = TaskType::Chat;
119 let mut matched_keywords: Vec<String> = Vec::new();
120
121 for pattern in PATTERNS {
122 let hits: Vec<String> = pattern
123 .keywords
124 .iter()
125 .filter(|kw| text_lower.contains(**kw))
126 .map(|kw| (*kw).to_string())
127 .collect();
128
129 if !hits.is_empty() && matched_keywords.is_empty() {
130 matched_type = pattern.task_type.clone();
132 matched_keywords = hits;
133 } else if !hits.is_empty() {
134 matched_keywords.extend(hits);
136 }
137 }
138
139 let word_count = text_lower.split_whitespace().count().max(1) as f32;
141 let keyword_density = matched_keywords.len() as f32 / word_count;
142 let raw_complexity = keyword_density + request.complexity_boost;
143 let complexity = raw_complexity.clamp(0.1, 0.9);
144
145 TaskProfile {
146 task_type: matched_type,
147 complexity,
148 keywords: matched_keywords,
149 }
150 }
151}
152
153fn last_user_message(request: &ChatRequest) -> String {
155 request
156 .messages
157 .iter()
158 .rev()
159 .find(|m| m.role == "user")
160 .map(|m| m.content.clone())
161 .unwrap_or_default()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::pipeline::traits::LlmMessage;
168
169 fn make_request(user_content: &str) -> ChatRequest {
170 ChatRequest {
171 messages: vec![LlmMessage {
172 role: "user".into(),
173 content: user_content.into(),
174 tool_call_id: None,
175 tool_calls: None,
176 }],
177 tools: vec![],
178 model: None,
179 max_tokens: None,
180 temperature: None,
181 auth_context: None,
182 complexity_boost: 0.0,
183 }
184 }
185
186 fn make_request_with_system(system: &str, user: &str) -> ChatRequest {
187 ChatRequest {
188 messages: vec![
189 LlmMessage {
190 role: "system".into(),
191 content: system.into(),
192 tool_call_id: None,
193 tool_calls: None,
194 },
195 LlmMessage {
196 role: "user".into(),
197 content: user.into(),
198 tool_call_id: None,
199 tool_calls: None,
200 },
201 ],
202 tools: vec![],
203 model: None,
204 max_tokens: None,
205 temperature: None,
206 auth_context: None,
207 complexity_boost: 0.0,
208 }
209 }
210
211 #[test]
212 fn classify_code_generation() {
213 let classifier = KeywordClassifier::new();
214 let profile = classifier.classify(&make_request("Please implement a sorting function"));
215 assert_eq!(profile.task_type, TaskType::CodeGeneration);
216 assert!(profile.keywords.contains(&"implement".to_string()));
217 assert!(profile.keywords.contains(&"function".to_string()));
218 }
219
220 #[test]
221 fn classify_code_generation_debug() {
222 let classifier = KeywordClassifier::new();
223 let profile = classifier.classify(&make_request("Can you debug this code for me?"));
224 assert_eq!(profile.task_type, TaskType::CodeGeneration);
225 assert!(profile.keywords.contains(&"debug".to_string()));
226 assert!(profile.keywords.contains(&"code".to_string()));
227 }
228
229 #[test]
230 fn classify_code_review() {
231 let classifier = KeywordClassifier::new();
232 let profile = classifier.classify(&make_request("Please review my pull request"));
233 assert_eq!(profile.task_type, TaskType::CodeReview);
234 assert!(profile.keywords.contains(&"review".to_string()));
235 }
236
237 #[test]
238 fn classify_research() {
239 let classifier = KeywordClassifier::new();
240 let profile = classifier.classify(&make_request("Research the best database options"));
241 assert_eq!(profile.task_type, TaskType::Research);
242 assert!(profile.keywords.contains(&"research".to_string()));
243 }
244
245 #[test]
246 fn classify_research_lookup() {
247 let classifier = KeywordClassifier::new();
248 let profile = classifier.classify(&make_request("Can you look up the API docs?"));
249 assert_eq!(profile.task_type, TaskType::Research);
250 assert!(profile.keywords.contains(&"look up".to_string()));
251 }
252
253 #[test]
254 fn classify_creative() {
255 let classifier = KeywordClassifier::new();
256 let profile = classifier.classify(&make_request("Write me a short poem about Rust"));
257 assert_eq!(profile.task_type, TaskType::Creative);
258 assert!(profile.keywords.contains(&"poem".to_string()));
259 }
260
261 #[test]
262 fn classify_analysis() {
263 let classifier = KeywordClassifier::new();
264 let profile = classifier.classify(&make_request("Explain how tokio works"));
265 assert_eq!(profile.task_type, TaskType::Analysis);
266 assert!(profile.keywords.contains(&"explain".to_string()));
267 }
268
269 #[test]
270 fn classify_analysis_summarize() {
271 let classifier = KeywordClassifier::new();
272 let profile = classifier.classify(&make_request("Summarize this document for me"));
273 assert_eq!(profile.task_type, TaskType::Analysis);
274 assert!(profile.keywords.contains(&"summarize".to_string()));
275 }
276
277 #[test]
278 fn classify_tool_use() {
279 let classifier = KeywordClassifier::new();
280 let profile = classifier.classify(&make_request("Use tool to fetch the data"));
281 assert_eq!(profile.task_type, TaskType::ToolUse);
282 assert!(profile.keywords.contains(&"use tool".to_string()));
283 }
284
285 #[test]
286 fn classify_defaults_to_chat() {
287 let classifier = KeywordClassifier::new();
288 let profile = classifier.classify(&make_request("Hello, how are you today?"));
289 assert_eq!(profile.task_type, TaskType::Chat);
290 assert!(profile.keywords.is_empty());
291 }
292
293 #[test]
294 fn classify_empty_message() {
295 let classifier = KeywordClassifier::new();
296 let profile = classifier.classify(&make_request(""));
297 assert_eq!(profile.task_type, TaskType::Chat);
298 assert!(profile.keywords.is_empty());
299 }
300
301 #[test]
302 fn classify_no_user_messages() {
303 let classifier = KeywordClassifier::new();
304 let req = ChatRequest {
305 messages: vec![LlmMessage {
306 role: "system".into(),
307 content: "You are helpful. Implement code.".into(),
308 tool_call_id: None,
309 tool_calls: None,
310 }],
311 tools: vec![],
312 model: None,
313 max_tokens: None,
314 temperature: None,
315 auth_context: None,
316 complexity_boost: 0.0,
317 };
318 let profile = classifier.classify(&req);
320 assert_eq!(profile.task_type, TaskType::Chat);
321 }
322
323 #[test]
324 fn classify_case_insensitive() {
325 let classifier = KeywordClassifier::new();
326 let profile = classifier.classify(&make_request("IMPLEMENT a FUNCTION in Rust"));
327 assert_eq!(profile.task_type, TaskType::CodeGeneration);
328 }
329
330 #[test]
331 fn classify_uses_last_user_message() {
332 let classifier = KeywordClassifier::new();
333 let req = ChatRequest {
334 messages: vec![
335 LlmMessage {
336 role: "user".into(),
337 content: "Research Rust crates".into(),
338 tool_call_id: None,
339 tool_calls: None,
340 },
341 LlmMessage {
342 role: "assistant".into(),
343 content: "Here are some crates...".into(),
344 tool_call_id: None,
345 tool_calls: None,
346 },
347 LlmMessage {
348 role: "user".into(),
349 content: "Now implement the function".into(),
350 tool_call_id: None,
351 tool_calls: None,
352 },
353 ],
354 tools: vec![],
355 model: None,
356 max_tokens: None,
357 temperature: None,
358 auth_context: None,
359 complexity_boost: 0.0,
360 };
361 let profile = classifier.classify(&req);
362 assert_eq!(profile.task_type, TaskType::CodeGeneration);
363 }
364
365 #[test]
366 fn classify_complexity_increases_with_keywords() {
367 let classifier = KeywordClassifier::new();
368
369 let short = classifier.classify(&make_request(
371 "Can you implement something for me in my big project today please?",
372 ));
373
374 let dense = classifier.classify(&make_request("implement debug fix code"));
376
377 assert!(
378 dense.complexity > short.complexity,
379 "dense={} should be > short={}",
380 dense.complexity,
381 short.complexity
382 );
383 }
384
385 #[test]
386 fn classify_complexity_clamped_min() {
387 let classifier = KeywordClassifier::new();
388 let profile = classifier.classify(&make_request(
390 "This is a very long message with lots of words but only one keyword: code. \
391 I am padding this out with many many more words to make the density very low.",
392 ));
393 assert!(
394 profile.complexity >= 0.1,
395 "complexity {} should be >= 0.1",
396 profile.complexity
397 );
398 }
399
400 #[test]
401 fn classify_complexity_clamped_max() {
402 let classifier = KeywordClassifier::new();
403 let profile = classifier.classify(&make_request("code function implement debug fix"));
405 assert!(
406 profile.complexity <= 0.9,
407 "complexity {} should be <= 0.9",
408 profile.complexity
409 );
410 }
411
412 #[test]
413 fn default_trait_impl() {
414 let classifier = KeywordClassifier;
415 let profile = classifier.classify(&make_request("hello"));
416 assert_eq!(profile.task_type, TaskType::Chat);
417 }
418
419 #[test]
420 fn classify_with_system_message_uses_user_only() {
421 let classifier = KeywordClassifier::new();
422 let profile = classifier.classify(&make_request_with_system(
423 "You are a code reviewer.",
424 "Hello, how is your day?",
425 ));
426 assert_eq!(profile.task_type, TaskType::Chat);
428 }
429
430 #[test]
431 fn classify_priority_code_over_analysis() {
432 let classifier = KeywordClassifier::new();
433 let profile = classifier.classify(&make_request("Explain this code to me"));
435 assert_eq!(profile.task_type, TaskType::CodeGeneration);
436 }
437
438 #[test]
441 fn complexity_boost_increases_complexity() {
442 let classifier = KeywordClassifier::new();
443 let baseline = classifier.classify(&make_request("hello world"));
444
445 let mut boosted_req = make_request("hello world");
446 boosted_req.complexity_boost = 0.3;
447 let boosted = classifier.classify(&boosted_req);
448
449 assert!(
450 boosted.complexity > baseline.complexity,
451 "boosted={} should be > baseline={}",
452 boosted.complexity,
453 baseline.complexity
454 );
455 }
456
457 #[test]
458 fn complexity_boost_clamps_at_max() {
459 let classifier = KeywordClassifier::new();
460 let mut req = make_request("code function implement debug fix");
461 req.complexity_boost = 0.5;
462 let profile = classifier.classify(&req);
463 assert!(
464 profile.complexity <= 0.9,
465 "complexity {} should be clamped at 0.9",
466 profile.complexity
467 );
468 }
469}