swarm_engine_core/learn/
scenario_registry.rs1use std::collections::HashMap;
26
27use super::profile_store::{ProfileStore, ProfileStoreError};
28use super::scenario_profile::{ProfileState, ScenarioProfile, ScenarioProfileId};
29
30#[derive(Debug, thiserror::Error)]
32pub enum RegistryError {
33 #[error("Store error: {0}")]
34 Store(#[from] ProfileStoreError),
35
36 #[error("Profile not found: {0}")]
37 NotFound(String),
38
39 #[error("No usable profile found")]
40 NoUsableProfile,
41
42 #[error("Profile not active: {0}")]
43 NotActive(String),
44}
45
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct TaskMatcher {
49 pub keywords: Vec<String>,
51 pub profile_id: ScenarioProfileId,
53 pub priority: i32,
55}
56
57impl TaskMatcher {
58 pub fn new(profile_id: ScenarioProfileId, keywords: Vec<String>) -> Self {
60 Self {
61 keywords,
62 profile_id,
63 priority: 0,
64 }
65 }
66
67 pub fn with_priority(mut self, priority: i32) -> Self {
69 self.priority = priority;
70 self
71 }
72
73 pub fn matches(&self, task: &str) -> bool {
75 let task_lower = task.to_lowercase();
76 self.keywords
77 .iter()
78 .any(|kw| task_lower.contains(&kw.to_lowercase()))
79 }
80}
81
82pub struct ScenarioRegistry {
84 store: ProfileStore,
86 profiles: HashMap<ScenarioProfileId, ScenarioProfile>,
88 matchers: Vec<TaskMatcher>,
90}
91
92impl ScenarioRegistry {
93 pub fn new(store: ProfileStore) -> Self {
95 Self {
96 store,
97 profiles: HashMap::new(),
98 matchers: Vec::new(),
99 }
100 }
101
102 pub fn load_all(&mut self) -> Result<(), RegistryError> {
104 let profiles = self.store.load_all()?;
105 for profile in profiles {
106 self.profiles.insert(profile.id.clone(), profile);
107 }
108 Ok(())
109 }
110
111 pub fn register(&mut self, profile: ScenarioProfile) -> Result<(), RegistryError> {
117 self.store.save(&profile)?;
118 self.profiles.insert(profile.id.clone(), profile);
119 Ok(())
120 }
121
122 pub fn get(&self, id: &ScenarioProfileId) -> Option<&ScenarioProfile> {
124 self.profiles.get(id)
125 }
126
127 pub fn get_mut(&mut self, id: &ScenarioProfileId) -> Option<&mut ScenarioProfile> {
129 self.profiles.get_mut(id)
130 }
131
132 pub fn remove(&mut self, id: &ScenarioProfileId) -> Result<(), RegistryError> {
134 self.store.delete(id)?;
135 self.profiles.remove(id);
136 self.matchers.retain(|m| &m.profile_id != id);
138 Ok(())
139 }
140
141 pub fn all_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
143 self.profiles.values()
144 }
145
146 pub fn usable_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
148 self.profiles.values().filter(|p| p.is_usable())
149 }
150
151 pub fn save(&self, id: &ScenarioProfileId) -> Result<(), RegistryError> {
153 if let Some(profile) = self.profiles.get(id) {
154 self.store.save(profile)?;
155 }
156 Ok(())
157 }
158
159 pub fn add_matcher(&mut self, matcher: TaskMatcher) {
165 self.matchers.push(matcher);
166 self.matchers.sort_by(|a, b| b.priority.cmp(&a.priority));
168 }
169
170 pub fn select_for_task(&self, task: &str) -> Result<&ScenarioProfile, RegistryError> {
172 for matcher in &self.matchers {
174 if matcher.matches(task) {
175 if let Some(profile) = self.profiles.get(&matcher.profile_id) {
176 if profile.is_usable() {
177 return Ok(profile);
178 }
179 }
180 }
181 }
182
183 self.usable_profiles()
185 .next()
186 .ok_or(RegistryError::NoUsableProfile)
187 }
188
189 pub fn candidates_for_task(&self, task: &str) -> Vec<&ScenarioProfile> {
191 let mut candidates: Vec<_> = self
192 .matchers
193 .iter()
194 .filter(|m| m.matches(task))
195 .filter_map(|m| self.profiles.get(&m.profile_id))
196 .filter(|p| p.is_usable())
197 .collect();
198
199 candidates.dedup_by_key(|p| &p.id);
201 candidates
202 }
203
204 pub fn profile_count(&self) -> usize {
210 self.profiles.len()
211 }
212
213 pub fn usable_count(&self) -> usize {
215 self.profiles.values().filter(|p| p.is_usable()).count()
216 }
217
218 pub fn matcher_count(&self) -> usize {
220 self.matchers.len()
221 }
222
223 pub fn count_by_state(&self) -> HashMap<ProfileState, usize> {
225 let mut counts = HashMap::new();
226 for profile in self.profiles.values() {
227 *counts.entry(profile.state).or_insert(0) += 1;
228 }
229 counts
230 }
231}
232
233#[cfg(test)]
238mod tests {
239 use super::*;
240 use tempfile::TempDir;
241
242 fn create_test_registry() -> (ScenarioRegistry, TempDir) {
243 let temp_dir = TempDir::new().unwrap();
244 let store = ProfileStore::new(temp_dir.path());
245 let registry = ScenarioRegistry::new(store);
246 (registry, temp_dir)
247 }
248
249 #[test]
250 fn test_register_and_get() {
251 let (mut registry, _temp) = create_test_registry();
252
253 let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
254 registry.register(profile).unwrap();
255
256 let loaded = registry.get(&ScenarioProfileId::new("test"));
257 assert!(loaded.is_some());
258 }
259
260 #[test]
261 fn test_task_matcher() {
262 let matcher = TaskMatcher::new(
263 ScenarioProfileId::new("troubleshooting"),
264 vec!["ログ".to_string(), "調査".to_string(), "エラー".to_string()],
265 );
266
267 assert!(matcher.matches("ログを調査してください"));
268 assert!(matcher.matches("エラーの原因を特定"));
269 assert!(!matcher.matches("新機能を追加"));
270 }
271
272 #[test]
273 fn test_select_for_task() {
274 let (mut registry, _temp) = create_test_registry();
275
276 let mut profile1 = ScenarioProfile::from_file("troubleshooting", "/path/to/1.toml");
278 profile1.state = ProfileState::Active;
279
280 let mut profile2 = ScenarioProfile::from_file("deep_search", "/path/to/2.toml");
281 profile2.state = ProfileState::Active;
282
283 registry.register(profile1).unwrap();
284 registry.register(profile2).unwrap();
285
286 registry.add_matcher(TaskMatcher::new(
288 ScenarioProfileId::new("troubleshooting"),
289 vec!["ログ".to_string(), "エラー".to_string()],
290 ));
291 registry.add_matcher(TaskMatcher::new(
292 ScenarioProfileId::new("deep_search"),
293 vec!["検索".to_string(), "探索".to_string()],
294 ));
295
296 let selected = registry.select_for_task("ログを調査").unwrap();
298 assert_eq!(selected.id.0, "troubleshooting");
299
300 let selected = registry.select_for_task("ファイルを検索").unwrap();
301 assert_eq!(selected.id.0, "deep_search");
302 }
303
304 #[test]
305 fn test_usable_profiles() {
306 let (mut registry, _temp) = create_test_registry();
307
308 let mut active = ScenarioProfile::from_file("active", "/path/to/1.toml");
309 active.state = ProfileState::Active;
310
311 let draft = ScenarioProfile::from_file("draft", "/path/to/2.toml");
312
313 registry.register(active).unwrap();
314 registry.register(draft).unwrap();
315
316 assert_eq!(registry.profile_count(), 2);
317 assert_eq!(registry.usable_count(), 1);
318 }
319
320 #[test]
321 fn test_count_by_state() {
322 let (mut registry, _temp) = create_test_registry();
323
324 let mut active1 = ScenarioProfile::from_file("active1", "/path/to/1.toml");
325 active1.state = ProfileState::Active;
326
327 let mut active2 = ScenarioProfile::from_file("active2", "/path/to/2.toml");
328 active2.state = ProfileState::Active;
329
330 let draft = ScenarioProfile::from_file("draft", "/path/to/3.toml");
331
332 registry.register(active1).unwrap();
333 registry.register(active2).unwrap();
334 registry.register(draft).unwrap();
335
336 let counts = registry.count_by_state();
337 assert_eq!(counts.get(&ProfileState::Active), Some(&2));
338 assert_eq!(counts.get(&ProfileState::Draft), Some(&1));
339 }
340}