Skip to main content

swarm_engine_core/learn/
scenario_registry.rs

1//! ScenarioRegistry - マルチシナリオ管理
2//!
3//! ## 概要
4//!
5//! 複数の ScenarioProfile を統合管理し、Task → Scenario のルーティングを行う。
6//!
7//! ## 機能
8//!
9//! - Profile の登録・取得・削除
10//! - Task パターンに基づく Scenario 選択
11//! - Ensemble 投票による Scenario 選択(将来)
12//!
13//! ## 使用例
14//!
15//! ```ignore
16//! let mut registry = ScenarioRegistry::new(store);
17//!
18//! // Profile 登録
19//! registry.register(profile)?;
20//!
21//! // Task → Scenario ルーティング
22//! let profile = registry.select_for_task("ログ調査して原因特定")?;
23//! ```
24
25use std::collections::HashMap;
26
27use super::profile_store::{ProfileStore, ProfileStoreError};
28use super::scenario_profile::{ProfileState, ScenarioProfile, ScenarioProfileId};
29
30/// ScenarioRegistry のエラー型
31#[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/// Task → Scenario マッチング設定
47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct TaskMatcher {
49    /// パターン(キーワードベース)
50    pub keywords: Vec<String>,
51    /// 対象 Profile ID
52    pub profile_id: ScenarioProfileId,
53    /// 優先度(高いほど優先)
54    pub priority: i32,
55}
56
57impl TaskMatcher {
58    /// 新規作成
59    pub fn new(profile_id: ScenarioProfileId, keywords: Vec<String>) -> Self {
60        Self {
61            keywords,
62            profile_id,
63            priority: 0,
64        }
65    }
66
67    /// 優先度を設定
68    pub fn with_priority(mut self, priority: i32) -> Self {
69        self.priority = priority;
70        self
71    }
72
73    /// Task がマッチするか判定
74    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
82/// マルチシナリオ管理
83pub struct ScenarioRegistry {
84    /// Profile ストア
85    store: ProfileStore,
86    /// キャッシュされた Profile
87    profiles: HashMap<ScenarioProfileId, ScenarioProfile>,
88    /// Task マッチャー
89    matchers: Vec<TaskMatcher>,
90}
91
92impl ScenarioRegistry {
93    /// 新規作成
94    pub fn new(store: ProfileStore) -> Self {
95        Self {
96            store,
97            profiles: HashMap::new(),
98            matchers: Vec::new(),
99        }
100    }
101
102    /// ストアから全 Profile を読み込み
103    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    // ========================================
112    // Profile 操作
113    // ========================================
114
115    /// Profile を登録
116    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    /// Profile を取得
123    pub fn get(&self, id: &ScenarioProfileId) -> Option<&ScenarioProfile> {
124        self.profiles.get(id)
125    }
126
127    /// Profile を取得(可変)
128    pub fn get_mut(&mut self, id: &ScenarioProfileId) -> Option<&mut ScenarioProfile> {
129        self.profiles.get_mut(id)
130    }
131
132    /// Profile を削除
133    pub fn remove(&mut self, id: &ScenarioProfileId) -> Result<(), RegistryError> {
134        self.store.delete(id)?;
135        self.profiles.remove(id);
136        // 関連する matcher も削除
137        self.matchers.retain(|m| &m.profile_id != id);
138        Ok(())
139    }
140
141    /// 全 Profile を取得
142    pub fn all_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
143        self.profiles.values()
144    }
145
146    /// 使用可能な Profile を取得
147    pub fn usable_profiles(&self) -> impl Iterator<Item = &ScenarioProfile> {
148        self.profiles.values().filter(|p| p.is_usable())
149    }
150
151    /// Profile を保存(更新後)
152    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    // ========================================
160    // Task Matching
161    // ========================================
162
163    /// TaskMatcher を追加
164    pub fn add_matcher(&mut self, matcher: TaskMatcher) {
165        self.matchers.push(matcher);
166        // 優先度でソート
167        self.matchers.sort_by(|a, b| b.priority.cmp(&a.priority));
168    }
169
170    /// Task に対応する Profile を選択
171    pub fn select_for_task(&self, task: &str) -> Result<&ScenarioProfile, RegistryError> {
172        // マッチするものを探す
173        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        // マッチしない場合、使用可能な最初の Profile を返す
184        self.usable_profiles()
185            .next()
186            .ok_or(RegistryError::NoUsableProfile)
187    }
188
189    /// Task に対応する全候補 Profile を取得
190    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        // 重複除去
200        candidates.dedup_by_key(|p| &p.id);
201        candidates
202    }
203
204    // ========================================
205    // Stats
206    // ========================================
207
208    /// 登録されている Profile 数
209    pub fn profile_count(&self) -> usize {
210        self.profiles.len()
211    }
212
213    /// 使用可能な Profile 数
214    pub fn usable_count(&self) -> usize {
215        self.profiles.values().filter(|p| p.is_usable()).count()
216    }
217
218    /// Matcher 数
219    pub fn matcher_count(&self) -> usize {
220        self.matchers.len()
221    }
222
223    /// 状態別 Profile 数
224    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// ============================================================================
234// Tests
235// ============================================================================
236
237#[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        // Active な Profile を作成
277        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        // Matcher を追加
287        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        // マッチング
297        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}