Skip to main content

swarm_engine_core/learn/
profile_adapter.rs

1//! ProfileAdapter - ScenarioProfile と OfflineModel の統合
2//!
3//! ## 概要
4//!
5//! 新しい ScenarioProfile と既存の OfflineModel を橋渡しする。
6//! 既存コードを変更せずに、ScenarioProfile を使用可能にする。
7//!
8//! ## 変換方向
9//!
10//! ```text
11//! ScenarioProfile → OfflineModel (Orchestrator 適用用)
12//! OfflineModel → ScenarioProfile (移行用)
13//! ```
14
15use super::learned_component::{LearnedDepGraph, LearnedExploration, LearnedStrategy};
16use super::offline::{
17    ActionOrderSource, LearnedActionOrder, OfflineModel, OptimalParameters, StrategyConfig,
18};
19use super::scenario_profile::ScenarioProfile;
20
21// ============================================================================
22// ScenarioProfile → OfflineModel 変換
23// ============================================================================
24
25/// ScenarioProfile から OfflineModel を生成
26///
27/// Orchestrator に適用するための変換。
28/// ScenarioProfile の各コンポーネントから OfflineModel を構築する。
29pub fn profile_to_offline_model(profile: &ScenarioProfile) -> OfflineModel {
30    let mut model = OfflineModel::default();
31
32    // LearnedExploration → OptimalParameters
33    if let Some(exploration) = &profile.exploration {
34        model.parameters = OptimalParameters {
35            ucb1_c: exploration.ucb1_c,
36            learning_weight: exploration.learning_weight,
37            ngram_weight: exploration.ngram_weight,
38        };
39    }
40
41    // LearnedStrategy → StrategyConfig
42    if let Some(strategy) = &profile.strategy {
43        model.strategy_config = StrategyConfig {
44            initial_strategy: strategy.initial_strategy.clone(),
45            maturity_threshold: strategy.maturity_threshold as u32,
46            error_rate_threshold: strategy.error_rate_threshold,
47        };
48    }
49
50    // LearnedDepGraph → LearnedActionOrder + recommended_paths
51    if let Some(dep_graph) = &profile.dep_graph {
52        // Action order を設定
53        if !dep_graph.action_order.is_empty() {
54            // アクション集合のハッシュを計算
55            use std::collections::hash_map::DefaultHasher;
56            use std::hash::{Hash, Hasher};
57            let mut hasher = DefaultHasher::new();
58            dep_graph.action_order.hash(&mut hasher);
59            let action_set_hash = hasher.finish();
60
61            model.action_order = Some(LearnedActionOrder {
62                discover: dep_graph.action_order.clone(),
63                not_discover: Vec::new(), // TODO: 分離が必要な場合は対応
64                action_set_hash,
65                source: ActionOrderSource::Manual, // Profile から生成
66            });
67        }
68
69        // Recommended paths を設定
70        model.recommended_paths = dep_graph.recommended_paths.clone();
71    }
72
73    // Metadata
74    model.updated_at = profile.updated_at;
75    model.analyzed_sessions = profile
76        .dep_graph
77        .as_ref()
78        .map(|d| d.learned_from.len())
79        .unwrap_or(0);
80
81    model
82}
83
84// ============================================================================
85// OfflineModel → ScenarioProfile 変換 (移行用)
86// ============================================================================
87
88/// OfflineModel から ScenarioProfile のコンポーネントを抽出
89///
90/// 既存データの移行用。
91pub fn offline_model_to_components(
92    model: &OfflineModel,
93) -> (
94    Option<LearnedDepGraph>,
95    Option<LearnedExploration>,
96    Option<LearnedStrategy>,
97) {
98    // OptimalParameters → LearnedExploration
99    let exploration = Some(LearnedExploration {
100        ucb1_c: model.parameters.ucb1_c,
101        learning_weight: model.parameters.learning_weight,
102        ngram_weight: model.parameters.ngram_weight,
103        confidence: 0.8, // 既存データは高信頼度と仮定
104        session_count: model.analyzed_sessions,
105        updated_at: model.updated_at,
106    });
107
108    // StrategyConfig → LearnedStrategy
109    let strategy = Some(LearnedStrategy {
110        initial_strategy: model.strategy_config.initial_strategy.clone(),
111        maturity_threshold: model.strategy_config.maturity_threshold as usize,
112        error_rate_threshold: model.strategy_config.error_rate_threshold,
113        confidence: 0.8,
114        session_count: model.analyzed_sessions,
115        updated_at: model.updated_at,
116    });
117
118    // LearnedActionOrder → LearnedDepGraph
119    let dep_graph = model.action_order.as_ref().map(|order| {
120        use crate::exploration::DependencyGraph;
121
122        LearnedDepGraph {
123            graph: DependencyGraph::new(), // 空のグラフ(action_order のみ使用)
124            action_order: order.discover.clone(),
125            recommended_paths: model.recommended_paths.clone(),
126            confidence: 0.8, // 既存データは高信頼度と仮定
127            learned_from: Vec::new(),
128            updated_at: model.updated_at,
129        }
130    });
131
132    (dep_graph, exploration, strategy)
133}
134
135/// OfflineModel から ScenarioProfile を構築
136pub fn migrate_offline_model_to_profile(
137    profile_id: impl Into<String>,
138    scenario_path: impl Into<std::path::PathBuf>,
139    model: &OfflineModel,
140) -> ScenarioProfile {
141    use super::scenario_profile::{ProfileState, ScenarioSource};
142
143    let (dep_graph, exploration, strategy) = offline_model_to_components(model);
144
145    let mut profile =
146        ScenarioProfile::new(profile_id, ScenarioSource::from_path(scenario_path.into()));
147
148    profile.dep_graph = dep_graph;
149    profile.exploration = exploration;
150    profile.strategy = strategy;
151    profile.state = ProfileState::Active; // 既存データがある = Active
152    profile.updated_at = model.updated_at;
153
154    profile
155}
156
157// ============================================================================
158// LearnableSwarmBuilder 拡張用ヘルパー
159// ============================================================================
160
161/// ScenarioProfile から OfflineModel を取得する trait
162pub trait ProfileToOfflineModel {
163    /// OfflineModel に変換
164    fn to_offline_model(&self) -> OfflineModel;
165}
166
167impl ProfileToOfflineModel for ScenarioProfile {
168    fn to_offline_model(&self) -> OfflineModel {
169        profile_to_offline_model(self)
170    }
171}
172
173// ============================================================================
174// Tests
175// ============================================================================
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::exploration::DependencyGraph;
181    use crate::learn::scenario_profile::ScenarioSource;
182
183    #[test]
184    fn test_profile_to_offline_model_empty() {
185        let profile = ScenarioProfile::new("test", ScenarioSource::from_path("/test.toml"));
186        let model = profile_to_offline_model(&profile);
187
188        // デフォルト値が設定される
189        assert!(model.parameters.ucb1_c > 0.0);
190    }
191
192    #[test]
193    fn test_profile_to_offline_model_with_components() {
194        let mut profile = ScenarioProfile::new("test", ScenarioSource::from_path("/test.toml"));
195
196        profile.exploration = Some(LearnedExploration {
197            ucb1_c: 2.5,
198            learning_weight: 0.4,
199            ngram_weight: 1.2,
200            confidence: 0.9,
201            session_count: 10,
202            updated_at: 12345,
203        });
204
205        profile.strategy = Some(LearnedStrategy {
206            initial_strategy: "greedy".to_string(),
207            maturity_threshold: 10,
208            error_rate_threshold: 0.3,
209            confidence: 0.85,
210            session_count: 10,
211            updated_at: 12345,
212        });
213
214        profile.dep_graph = Some(
215            LearnedDepGraph::new(
216                DependencyGraph::new(),
217                vec!["A".to_string(), "B".to_string()],
218            )
219            .with_confidence(0.95),
220        );
221
222        let model = profile_to_offline_model(&profile);
223
224        assert_eq!(model.parameters.ucb1_c, 2.5);
225        assert_eq!(model.parameters.learning_weight, 0.4);
226        assert_eq!(model.strategy_config.initial_strategy, "greedy");
227        assert_eq!(model.strategy_config.maturity_threshold, 10);
228        assert!(model.action_order.is_some());
229        assert_eq!(model.action_order.as_ref().unwrap().discover.len(), 2);
230    }
231
232    #[test]
233    fn test_offline_model_to_components() {
234        use super::ActionOrderSource;
235
236        let mut model = OfflineModel::default();
237        model.parameters.ucb1_c = 1.8;
238        model.strategy_config.initial_strategy = "ucb1".to_string();
239        model.action_order = Some(LearnedActionOrder {
240            discover: vec!["X".to_string(), "Y".to_string()],
241            not_discover: vec![],
242            action_set_hash: 12345,
243            source: ActionOrderSource::Manual,
244        });
245        model.analyzed_sessions = 5;
246
247        let (dep_graph, exploration, strategy) = offline_model_to_components(&model);
248
249        assert!(dep_graph.is_some());
250        assert!(exploration.is_some());
251        assert!(strategy.is_some());
252
253        let exploration = exploration.unwrap();
254        assert_eq!(exploration.ucb1_c, 1.8);
255
256        let strategy = strategy.unwrap();
257        assert_eq!(strategy.initial_strategy, "ucb1");
258
259        let dep_graph = dep_graph.unwrap();
260        assert_eq!(dep_graph.action_order.len(), 2);
261    }
262
263    #[test]
264    fn test_migrate_offline_model_to_profile() {
265        let mut model = OfflineModel::default();
266        model.parameters.ucb1_c = 2.0;
267        model.analyzed_sessions = 10;
268
269        let profile =
270            migrate_offline_model_to_profile("test-profile", "/path/to/scenario.toml", &model);
271
272        assert_eq!(profile.id.0, "test-profile");
273        assert!(profile.exploration.is_some());
274        assert_eq!(profile.exploration.as_ref().unwrap().ucb1_c, 2.0);
275    }
276
277    #[test]
278    fn test_profile_to_offline_model_trait() {
279        let profile = ScenarioProfile::new("test", ScenarioSource::from_path("/test.toml"));
280        let model = profile.to_offline_model();
281
282        assert!(model.parameters.ucb1_c > 0.0);
283    }
284}