llm_toolkit_expertise/
context.rs

1//! Context-related types for expertise fragments.
2//!
3//! This module defines types for controlling when and how knowledge fragments
4//! should be activated based on task context, health, and priority.
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9/// Priority: Instruction strength/enforcement level
10///
11/// Controls how strongly a knowledge fragment should be enforced during
12/// prompt generation. Higher priority fragments appear first and are
13/// treated as more critical constraints.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum Priority {
17    /// Critical: Absolute must-follow (violation = error / strong negative constraint)
18    Critical,
19    /// High: Recommended/emphasized (explicit instruction)
20    High,
21    /// Normal: Standard context (general guidance)
22    #[default]
23    Normal,
24    /// Low: Reference information (background info)
25    Low,
26}
27
28// Custom ordering: Critical > High > Normal > Low
29impl PartialOrd for Priority {
30    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
31        Some(self.cmp(other))
32    }
33}
34
35impl Ord for Priority {
36    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
37        self.weight().cmp(&other.weight())
38    }
39}
40
41impl Priority {
42    /// Returns a numeric value for ordering (higher = more important)
43    pub fn weight(&self) -> u8 {
44        match self {
45            Priority::Critical => 4,
46            Priority::High => 3,
47            Priority::Normal => 2,
48            Priority::Low => 1,
49        }
50    }
51
52    /// Returns a display label for visualization
53    pub fn label(&self) -> &'static str {
54        match self {
55            Priority::Critical => "CRITICAL",
56            Priority::High => "HIGH",
57            Priority::Normal => "NORMAL",
58            Priority::Low => "LOW",
59        }
60    }
61}
62
63/// ContextProfile: Activation conditions for knowledge fragments
64///
65/// Defines when a fragment should be included in the generated prompt
66/// based on various contextual factors.
67#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum ContextProfile {
70    /// Always active (no conditions)
71    #[default]
72    Always,
73
74    /// Conditionally active based on context matching
75    Conditional {
76        /// Task types (e.g., "Debug", "Ideation", "Review")
77        #[serde(default, skip_serializing_if = "Vec::is_empty")]
78        task_types: Vec<String>,
79
80        /// User states (e.g., "Beginner", "Confused", "Expert")
81        #[serde(default, skip_serializing_if = "Vec::is_empty")]
82        user_states: Vec<String>,
83
84        /// Task health status for behavior adjustment
85        #[serde(default, skip_serializing_if = "Option::is_none")]
86        task_health: Option<TaskHealth>,
87    },
88}
89
90impl ContextProfile {
91    /// Check if this context profile matches the given context
92    pub fn matches(&self, context: &ContextMatcher) -> bool {
93        match self {
94            ContextProfile::Always => true,
95            ContextProfile::Conditional {
96                task_types,
97                user_states,
98                task_health,
99            } => {
100                let task_match = task_types.is_empty()
101                    || context
102                        .task_type
103                        .as_ref()
104                        .map(|t| task_types.contains(t))
105                        .unwrap_or(false);
106
107                let user_match = user_states.is_empty()
108                    || context
109                        .user_state
110                        .as_ref()
111                        .map(|s| user_states.contains(s))
112                        .unwrap_or(false);
113
114                let health_match = task_health
115                    .as_ref()
116                    .map(|h| context.task_health.as_ref() == Some(h))
117                    .unwrap_or(true);
118
119                task_match && user_match && health_match
120            }
121        }
122    }
123}
124
125/// TaskHealth: Task health/quality status indicator
126///
127/// Represents the current state of a task for triggering adaptive behavior.
128/// Enables "gear shifting" based on progress and quality.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
130#[serde(rename_all = "snake_case")]
131pub enum TaskHealth {
132    /// On track: Proceed confidently (Action: Go/SpeedUp)
133    OnTrack,
134
135    /// At risk: Proceed cautiously with verification (Action: Review/Clarify)
136    AtRisk,
137
138    /// Off track: Stop and reassess (Action: Stop/Reject)
139    OffTrack,
140}
141
142impl TaskHealth {
143    /// Returns a display label for visualization
144    pub fn label(&self) -> &'static str {
145        match self {
146            TaskHealth::OnTrack => "On Track",
147            TaskHealth::AtRisk => "At Risk",
148            TaskHealth::OffTrack => "Off Track",
149        }
150    }
151
152    /// Returns an emoji representation
153    pub fn emoji(&self) -> &'static str {
154        match self {
155            TaskHealth::OnTrack => "✅",
156            TaskHealth::AtRisk => "⚠️",
157            TaskHealth::OffTrack => "🚫",
158        }
159    }
160}
161
162/// ContextMatcher: Runtime context for matching conditional fragments
163///
164/// Used to evaluate whether conditional fragments should be activated.
165#[derive(Debug, Clone, Default)]
166pub struct ContextMatcher {
167    pub task_type: Option<String>,
168    pub user_state: Option<String>,
169    pub task_health: Option<TaskHealth>,
170}
171
172impl ContextMatcher {
173    /// Create a new context matcher
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Set task type
179    pub fn with_task_type(mut self, task_type: impl Into<String>) -> Self {
180        self.task_type = Some(task_type.into());
181        self
182    }
183
184    /// Set user state
185    pub fn with_user_state(mut self, user_state: impl Into<String>) -> Self {
186        self.user_state = Some(user_state.into());
187        self
188    }
189
190    /// Set task health
191    pub fn with_task_health(mut self, task_health: TaskHealth) -> Self {
192        self.task_health = Some(task_health);
193        self
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_priority_ordering() {
203        assert!(Priority::Critical > Priority::High);
204        assert!(Priority::High > Priority::Normal);
205        assert!(Priority::Normal > Priority::Low);
206    }
207
208    #[test]
209    fn test_priority_weight() {
210        assert_eq!(Priority::Critical.weight(), 4);
211        assert_eq!(Priority::High.weight(), 3);
212        assert_eq!(Priority::Normal.weight(), 2);
213        assert_eq!(Priority::Low.weight(), 1);
214    }
215
216    #[test]
217    fn test_context_always_matches() {
218        let profile = ContextProfile::Always;
219        let context = ContextMatcher::new();
220        assert!(profile.matches(&context));
221    }
222
223    #[test]
224    fn test_context_conditional_matching() {
225        let profile = ContextProfile::Conditional {
226            task_types: vec!["Debug".to_string()],
227            user_states: vec![],
228            task_health: Some(TaskHealth::AtRisk),
229        };
230
231        let matching_context = ContextMatcher::new()
232            .with_task_type("Debug")
233            .with_task_health(TaskHealth::AtRisk);
234
235        let non_matching_context = ContextMatcher::new()
236            .with_task_type("Review")
237            .with_task_health(TaskHealth::OnTrack);
238
239        assert!(profile.matches(&matching_context));
240        assert!(!profile.matches(&non_matching_context));
241    }
242}