Skip to main content

thulp_workspace/
filter.rs

1//! Session filtering for queries.
2//!
3//! This module provides the `SessionFilter` enum for filtering sessions
4//! when listing or querying.
5
6use crate::session::{Session, SessionStatus, SessionType, Timestamp};
7
8/// Filter for querying sessions.
9///
10/// Filters can be combined using `SessionFilter::And` for complex queries.
11///
12/// # Example
13///
14/// ```ignore
15/// use thulp_workspace::{SessionFilter, SessionStatus};
16///
17/// // Find active conversation sessions
18/// let filter = SessionFilter::And(vec![
19///     SessionFilter::ByStatus(SessionStatus::Active),
20///     SessionFilter::ByTypeName("conversation".to_string()),
21/// ]);
22/// ```
23#[derive(Debug, Clone)]
24pub enum SessionFilter {
25    /// Match sessions with the given status.
26    ByStatus(SessionStatus),
27
28    /// Match sessions by type name (e.g., "conversation", "teacher_demo").
29    ByTypeName(String),
30
31    /// Match sessions that have a specific tag.
32    HasTag(String),
33
34    /// Match sessions created after the given timestamp.
35    CreatedAfter(Timestamp),
36
37    /// Match sessions created before the given timestamp.
38    CreatedBefore(Timestamp),
39
40    /// Match sessions updated after the given timestamp.
41    UpdatedAfter(Timestamp),
42
43    /// Match sessions updated before the given timestamp.
44    UpdatedBefore(Timestamp),
45
46    /// Match sessions where the name contains the given text.
47    NameContains(String),
48
49    /// Match sessions that have a parent session.
50    HasParent,
51
52    /// Match sessions with a specific parent session ID.
53    WithParent(crate::session::SessionId),
54
55    /// Match sessions that are root sessions (no parent).
56    IsRoot,
57
58    /// Combine multiple filters with AND logic.
59    And(Vec<SessionFilter>),
60
61    /// Combine multiple filters with OR logic.
62    Or(Vec<SessionFilter>),
63
64    /// Negate a filter.
65    Not(Box<SessionFilter>),
66
67    /// Match all sessions.
68    All,
69}
70
71impl SessionFilter {
72    /// Check if a session matches this filter.
73    pub fn matches(&self, session: &Session) -> bool {
74        match self {
75            SessionFilter::ByStatus(status) => session.status() == *status,
76
77            SessionFilter::ByTypeName(type_name) => {
78                let session_type_name = session_type_name(&session.metadata.session_type);
79                session_type_name.eq_ignore_ascii_case(type_name)
80            }
81
82            SessionFilter::HasTag(tag) => session.metadata.tags.iter().any(|t| t == tag),
83
84            SessionFilter::CreatedAfter(timestamp) => {
85                session.metadata.created_at.as_millis() > timestamp.as_millis()
86            }
87
88            SessionFilter::CreatedBefore(timestamp) => {
89                session.metadata.created_at.as_millis() < timestamp.as_millis()
90            }
91
92            SessionFilter::UpdatedAfter(timestamp) => {
93                session.metadata.updated_at.as_millis() > timestamp.as_millis()
94            }
95
96            SessionFilter::UpdatedBefore(timestamp) => {
97                session.metadata.updated_at.as_millis() < timestamp.as_millis()
98            }
99
100            SessionFilter::NameContains(text) => session
101                .metadata
102                .name
103                .to_lowercase()
104                .contains(&text.to_lowercase()),
105
106            SessionFilter::HasParent => session.metadata.parent_session.is_some(),
107
108            SessionFilter::WithParent(parent_id) => {
109                session.metadata.parent_session.as_ref() == Some(parent_id)
110            }
111
112            SessionFilter::IsRoot => session.metadata.parent_session.is_none(),
113
114            SessionFilter::And(filters) => filters.iter().all(|f| f.matches(session)),
115
116            SessionFilter::Or(filters) => filters.iter().any(|f| f.matches(session)),
117
118            SessionFilter::Not(filter) => !filter.matches(session),
119
120            SessionFilter::All => true,
121        }
122    }
123
124    /// Create an AND filter from two filters.
125    pub fn and(self, other: SessionFilter) -> SessionFilter {
126        match self {
127            SessionFilter::And(mut filters) => {
128                filters.push(other);
129                SessionFilter::And(filters)
130            }
131            _ => SessionFilter::And(vec![self, other]),
132        }
133    }
134
135    /// Create an OR filter from two filters.
136    pub fn or(self, other: SessionFilter) -> SessionFilter {
137        match self {
138            SessionFilter::Or(mut filters) => {
139                filters.push(other);
140                SessionFilter::Or(filters)
141            }
142            _ => SessionFilter::Or(vec![self, other]),
143        }
144    }
145
146    /// Negate this filter.
147    pub fn negate(self) -> SessionFilter {
148        SessionFilter::Not(Box::new(self))
149    }
150
151    /// Create a filter for active sessions.
152    pub fn active() -> Self {
153        SessionFilter::ByStatus(SessionStatus::Active)
154    }
155
156    /// Create a filter for completed sessions.
157    pub fn completed() -> Self {
158        SessionFilter::ByStatus(SessionStatus::Completed)
159    }
160
161    /// Create a filter for failed sessions.
162    pub fn failed() -> Self {
163        SessionFilter::ByStatus(SessionStatus::Failed)
164    }
165
166    /// Create a filter for conversation sessions.
167    pub fn conversations() -> Self {
168        SessionFilter::ByTypeName("conversation".to_string())
169    }
170
171    /// Create a filter for teacher demo sessions.
172    pub fn teacher_demos() -> Self {
173        SessionFilter::ByTypeName("teacher_demo".to_string())
174    }
175
176    /// Create a filter for evaluation sessions.
177    pub fn evaluations() -> Self {
178        SessionFilter::ByTypeName("evaluation".to_string())
179    }
180
181    /// Create a filter for refinement sessions.
182    pub fn refinements() -> Self {
183        SessionFilter::ByTypeName("refinement".to_string())
184    }
185
186    /// Create a filter for agent sessions.
187    pub fn agent_sessions() -> Self {
188        SessionFilter::ByTypeName("agent".to_string())
189    }
190}
191
192/// Get the type name for a session type.
193fn session_type_name(session_type: &SessionType) -> &'static str {
194    match session_type {
195        SessionType::TeacherDemo { .. } => "teacher_demo",
196        SessionType::Evaluation { .. } => "evaluation",
197        SessionType::Refinement { .. } => "refinement",
198        SessionType::Conversation { .. } => "conversation",
199        SessionType::Agent { .. } => "agent",
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::session::{Session, SessionId, SessionType};
207
208    fn create_test_session(name: &str) -> Session {
209        Session::new(
210            name,
211            SessionType::Conversation {
212                purpose: "Test".to_string(),
213            },
214        )
215    }
216
217    #[test]
218    fn test_filter_by_status() {
219        let mut session = create_test_session("Test");
220
221        let filter = SessionFilter::ByStatus(SessionStatus::Active);
222        assert!(filter.matches(&session));
223
224        session.complete();
225        assert!(!filter.matches(&session));
226
227        let completed_filter = SessionFilter::ByStatus(SessionStatus::Completed);
228        assert!(completed_filter.matches(&session));
229    }
230
231    #[test]
232    fn test_filter_by_type_name() {
233        let conversation = Session::new(
234            "Test",
235            SessionType::Conversation {
236                purpose: "Test".to_string(),
237            },
238        );
239
240        let evaluation = Session::new(
241            "Test",
242            SessionType::Evaluation {
243                skill_name: "test".to_string(),
244                test_cases: 10,
245            },
246        );
247
248        let conv_filter = SessionFilter::ByTypeName("conversation".to_string());
249        assert!(conv_filter.matches(&conversation));
250        assert!(!conv_filter.matches(&evaluation));
251
252        let eval_filter = SessionFilter::ByTypeName("evaluation".to_string());
253        assert!(eval_filter.matches(&evaluation));
254        assert!(!eval_filter.matches(&conversation));
255    }
256
257    #[test]
258    fn test_filter_has_tag() {
259        let mut session = create_test_session("Test");
260        session.metadata.tags.push("important".to_string());
261
262        let filter = SessionFilter::HasTag("important".to_string());
263        assert!(filter.matches(&session));
264
265        let no_tag_filter = SessionFilter::HasTag("other".to_string());
266        assert!(!no_tag_filter.matches(&session));
267    }
268
269    #[test]
270    fn test_filter_name_contains() {
271        let session = create_test_session("My Test Session");
272
273        let filter = SessionFilter::NameContains("test".to_string());
274        assert!(filter.matches(&session));
275
276        let no_match = SessionFilter::NameContains("other".to_string());
277        assert!(!no_match.matches(&session));
278    }
279
280    #[test]
281    fn test_filter_and() {
282        let mut session = create_test_session("Test");
283        session.metadata.tags.push("important".to_string());
284
285        let filter = SessionFilter::active().and(SessionFilter::HasTag("important".to_string()));
286
287        assert!(filter.matches(&session));
288
289        session.complete();
290        assert!(!filter.matches(&session));
291    }
292
293    #[test]
294    fn test_filter_or() {
295        let mut session = create_test_session("Test");
296
297        let filter = SessionFilter::completed().or(SessionFilter::active());
298
299        assert!(filter.matches(&session)); // Active
300
301        session.complete();
302        assert!(filter.matches(&session)); // Completed
303
304        session.fail();
305        assert!(!filter.matches(&session)); // Failed - neither active nor completed
306    }
307
308    #[test]
309    fn test_filter_not() {
310        let session = create_test_session("Test");
311
312        let filter = SessionFilter::completed().negate();
313        assert!(filter.matches(&session)); // Not completed (active)
314
315        let mut completed = create_test_session("Test");
316        completed.complete();
317        assert!(!filter.matches(&completed)); // Is completed
318    }
319
320    #[test]
321    fn test_filter_parent() {
322        let mut session = create_test_session("Test");
323
324        let has_parent = SessionFilter::HasParent;
325        let is_root = SessionFilter::IsRoot;
326
327        assert!(!has_parent.matches(&session));
328        assert!(is_root.matches(&session));
329
330        session.metadata.parent_session = Some(SessionId::new());
331        assert!(has_parent.matches(&session));
332        assert!(!is_root.matches(&session));
333    }
334
335    #[test]
336    fn test_filter_all() {
337        let session = create_test_session("Test");
338        assert!(SessionFilter::All.matches(&session));
339    }
340
341    #[test]
342    fn test_convenience_constructors() {
343        let session = Session::new(
344            "Test",
345            SessionType::Conversation {
346                purpose: "Test".to_string(),
347            },
348        );
349
350        assert!(SessionFilter::conversations().matches(&session));
351        assert!(!SessionFilter::evaluations().matches(&session));
352        assert!(SessionFilter::active().matches(&session));
353    }
354}