Skip to main content

clawft_core/
complexity.rs

1//! Task complexity analysis for model routing.
2//!
3//! Provides a [`TaskComplexityAnalyzer`] that estimates the complexity of a
4//! task description using lightweight heuristics. The resulting score drives
5//! the 3-tier routing decision in [`crate::intelligent_router`].
6//!
7//! This module is gated behind the `rvf` feature flag.
8
9// ── TaskComplexityAnalyzer ─────────────────────────────────────────────
10
11/// Estimates task complexity using surface-level text heuristics.
12///
13/// The analyser does **not** use an LLM or neural model. It relies on
14/// properties of the input text that correlate with cognitive load:
15///
16/// - Task length (word count)
17/// - Number of distinct requirements (sentence count)
18/// - Technical-keyword density
19/// - Multi-step indicators
20///
21/// All scores are returned in `0.0..=1.0`.
22pub struct TaskComplexityAnalyzer;
23
24impl TaskComplexityAnalyzer {
25    /// Create a new analyser.
26    pub fn new() -> Self {
27        Self
28    }
29
30    /// Estimate the complexity of `task`.
31    ///
32    /// Returns a value in `0.0..=1.0`:
33    /// - `0.0` -- trivial (e.g. "hello")
34    /// - `0.3` -- moderate (short technical question)
35    /// - `0.7+` -- very complex (multi-step, code-heavy, architectural)
36    pub fn analyze(&self, task: &str) -> f32 {
37        let lower = task.to_lowercase();
38        let words: Vec<&str> = lower.split_whitespace().collect();
39        let word_count = words.len();
40
41        // Component 1: Length -- 0.0-0.25 based on word count.
42        let length_score = (word_count as f32 / 200.0).min(1.0) * 0.25;
43
44        // Component 2: Sentence count (proxy for requirement count).
45        // We count sentence-ending punctuation.
46        let sentence_count = lower
47            .chars()
48            .filter(|c| *c == '.' || *c == '?' || *c == '!' || *c == ';')
49            .count()
50            .max(1); // At least 1 sentence.
51        let sentence_score = ((sentence_count as f32 - 1.0) / 5.0).clamp(0.0, 1.0) * 0.15;
52
53        // Component 3: Technical keywords.
54        let technical_keywords = [
55            "api",
56            "database",
57            "schema",
58            "authentication",
59            "authorization",
60            "deploy",
61            "kubernetes",
62            "docker",
63            "algorithm",
64            "concurrency",
65            "async",
66            "microservice",
67            "architecture",
68            "security",
69            "encryption",
70            "sql",
71            "graphql",
72            "websocket",
73            "distributed",
74            "cache",
75            "index",
76            "migration",
77        ];
78        let tech_hits = technical_keywords
79            .iter()
80            .filter(|kw| lower.contains(**kw))
81            .count();
82        let tech_score = (tech_hits as f32 / 4.0).min(1.0) * 0.25;
83
84        // Component 4: Multi-step indicators.
85        let step_indicators = [
86            "first",
87            "second",
88            "third",
89            "then",
90            "finally",
91            "next",
92            "step 1",
93            "step 2",
94            "step 3",
95            "after that",
96            "followed by",
97            "in addition",
98        ];
99        let step_hits = step_indicators
100            .iter()
101            .filter(|si| lower.contains(**si))
102            .count();
103        let step_score = (step_hits as f32 / 3.0).min(1.0) * 0.20;
104
105        // Component 5: Code fence presence.
106        let code_score = if lower.contains("```") { 0.15 } else { 0.0 };
107
108        (length_score + sentence_score + tech_score + step_score + code_score).clamp(0.0, 1.0)
109    }
110}
111
112impl Default for TaskComplexityAnalyzer {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118// ── Tests ──────────────────────────────────────────────────────────────
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn simple_question_low_complexity() {
126        let a = TaskComplexityAnalyzer::new();
127        let score = a.analyze("What is 2 + 2?");
128        assert!(
129            score < 0.3,
130            "simple question should be low complexity, got {score}"
131        );
132    }
133
134    #[test]
135    fn complex_multipart_request_higher() {
136        let a = TaskComplexityAnalyzer::new();
137        let simple = a.analyze("What is Rust?");
138        let complex = a.analyze(
139            "First, design the database schema for a distributed authentication \
140             microservice. Then implement the API endpoints with proper authorization. \
141             Next, add caching with Redis. Finally, deploy to Kubernetes with \
142             encrypted secrets.",
143        );
144        assert!(
145            complex > simple,
146            "complex ({complex}) should beat simple ({simple})"
147        );
148    }
149
150    #[test]
151    fn technical_keywords_raise_complexity() {
152        let a = TaskComplexityAnalyzer::new();
153        let no_tech = a.analyze("Tell me a story about a cat.");
154        let with_tech = a.analyze("Set up the database schema and deploy to kubernetes.");
155        assert!(
156            with_tech > no_tech,
157            "technical ({with_tech}) should beat non-technical ({no_tech})"
158        );
159    }
160
161    #[test]
162    fn empty_task_returns_zero() {
163        let a = TaskComplexityAnalyzer::new();
164        let score = a.analyze("");
165        assert!(
166            score.abs() < f32::EPSILON,
167            "empty task should be 0.0, got {score}"
168        );
169    }
170
171    #[test]
172    fn score_clamped_to_unit_range() {
173        let a = TaskComplexityAnalyzer::new();
174        // Throw everything at it.
175        let score = a.analyze(
176            "First, design the api architecture for a distributed database microservice \
177             with authentication and authorization. Then implement concurrency with async \
178             websocket connections. Next, add encryption and caching. Finally, deploy to \
179             kubernetes with docker. Step 1: schema migration. Step 2: graphql index. \
180             Step 3: security audit. ```code here``` After that, followed by more work. \
181             In addition, add algorithm for sql cache.",
182        );
183        assert!(score <= 1.0, "score should be <= 1.0, got {score}");
184        assert!(score >= 0.0, "score should be >= 0.0, got {score}");
185    }
186
187    #[test]
188    fn code_fences_raise_complexity() {
189        let a = TaskComplexityAnalyzer::new();
190        let no_code = a.analyze("Explain this function.");
191        let with_code = a.analyze("Explain this function: ```fn main() {}```");
192        assert!(
193            with_code > no_code,
194            "with code ({with_code}) should beat without ({no_code})"
195        );
196    }
197
198    #[test]
199    fn step_indicators_raise_complexity() {
200        let a = TaskComplexityAnalyzer::new();
201        let no_steps = a.analyze("Do the thing.");
202        let with_steps = a.analyze("First do A, then do B, finally do C.");
203        assert!(
204            with_steps > no_steps,
205            "with steps ({with_steps}) should beat without ({no_steps})"
206        );
207    }
208
209    #[test]
210    fn default_trait_creates_valid_analyzer() {
211        let a = TaskComplexityAnalyzer::default();
212        let score = a.analyze("hello");
213        assert!(score >= 0.0);
214        assert!(score <= 1.0);
215    }
216
217    #[test]
218    fn longer_text_scores_higher_than_shorter() {
219        let a = TaskComplexityAnalyzer::new();
220        let short = a.analyze("help");
221        let long = a.analyze(&"word ".repeat(100));
222        assert!(
223            long > short,
224            "long text ({long}) should beat short ({short})"
225        );
226    }
227
228    #[test]
229    fn multiple_sentences_increase_score() {
230        let a = TaskComplexityAnalyzer::new();
231        let one = a.analyze("Do the task.");
232        let many = a.analyze("Do task A. Then task B. Also task C. Check task D. Review task E.");
233        assert!(
234            many > one,
235            "many sentences ({many}) should beat one ({one})"
236        );
237    }
238}