Skip to main content

swarm_engine_core/analysis/
analyzer.rs

1//! Analyzer - SwarmState から TaskContext を生成
2//!
3//! # 設計
4//!
5//! ```text
6//! SwarmState → [Analyzer] → TaskContext
7//!                  │
8//!                  ├─ 自動処理(メイン)
9//!                  └─ 軽量LLM Single呼び出し(オプション)
10//! ```
11//!
12//! Analyzer は SwarmState を分析して TaskContext を生成する。
13//! デフォルト実装 (DefaultAnalyzer) は自動処理のみ。
14//! カスタム実装で軽量LLMを使った分析を追加可能。
15
16use std::collections::{HashMap, HashSet};
17
18use crate::actions::ActionsConfig;
19use crate::orchestrator::SwarmConfig;
20use crate::state::SwarmState;
21use crate::types::{SwarmTask, WorkerId};
22
23use crate::context::{TaskContext, WorkerSummary};
24
25// ============================================================================
26// Analyzer Trait
27// ============================================================================
28
29/// 状況分析器: SwarmState → TaskContext
30///
31/// # 責務
32///
33/// - SwarmState から Worker の状態を抽出
34/// - 成功率・進捗を計算
35/// - Escalation を収集
36/// - 利用可能なアクションを取得
37/// - オプションで軽量LLMを使った追加分析
38///
39/// # 実装例
40///
41/// ```ignore
42/// struct MyAnalyzer;
43///
44/// impl Analyzer for MyAnalyzer {
45///     fn analyze(&self, state: &SwarmState) -> TaskContext {
46///         let mut ctx = DefaultAnalyzer::new().analyze(state);
47///
48///         // 軽量LLMで追加分析
49///         let summary = call_lightweight_llm(&ctx);
50///         ctx.set("llm_summary", summary);
51///
52///         ctx
53///     }
54/// }
55/// ```
56pub trait Analyzer: Send + Sync {
57    /// SwarmState を分析して TaskContext を生成
58    fn analyze(&self, state: &SwarmState) -> TaskContext;
59
60    /// 名前(デバッグ/ログ用)
61    fn name(&self) -> &str {
62        "Analyzer"
63    }
64}
65
66// ============================================================================
67// DefaultAnalyzer
68// ============================================================================
69
70/// デフォルトの Analyzer 実装
71///
72/// SwarmState から自動的に TaskContext を生成する。
73/// 軽量LLM呼び出しは行わない(純粋な自動処理)。
74#[derive(Debug, Clone, Default)]
75pub struct DefaultAnalyzer {
76    /// 名前
77    name: String,
78}
79
80impl DefaultAnalyzer {
81    pub fn new() -> Self {
82        Self {
83            name: "DefaultAnalyzer".to_string(),
84        }
85    }
86
87    /// 名前を設定
88    pub fn with_name(mut self, name: impl Into<String>) -> Self {
89        self.name = name.into();
90        self
91    }
92
93    /// 成功率を計算
94    fn calculate_success_rate(state: &SwarmState) -> f64 {
95        state.shared.stats.success_rate()
96    }
97
98    /// 進捗を計算(仮実装:成功アクション数 / 最大Tick で近似)
99    fn calculate_progress(state: &SwarmState, max_ticks: u64) -> f64 {
100        if max_ticks == 0 {
101            // max_ticks が設定されていない場合は成功率で代用
102            return Self::calculate_success_rate(state);
103        }
104
105        (state.shared.tick as f64 / max_ticks as f64).min(1.0)
106    }
107}
108
109impl Analyzer for DefaultAnalyzer {
110    fn analyze(&self, state: &SwarmState) -> TaskContext {
111        let tick = state.shared.tick;
112        let mut workers = HashMap::new();
113        let mut escalations = Vec::new();
114
115        // 各 Worker の状態を収集
116        for (idx, ctx) in state.workers.iter().enumerate() {
117            let worker_id = WorkerId(idx);
118            let has_escalation = ctx.escalation.is_some();
119
120            // Escalation 収集
121            if let Some(esc) = &ctx.escalation {
122                escalations.push((worker_id, esc.clone()));
123            }
124
125            // 最新履歴を取得
126            let last_entry = ctx.history.latest();
127            let (last_action, last_success) = last_entry
128                .map(|e| (Some(e.action_name.clone()), Some(e.success)))
129                .unwrap_or((None, None));
130
131            // WorkerSummary を構築
132            let summary = WorkerSummary {
133                id: worker_id,
134                consecutive_failures: ctx.consecutive_failures,
135                last_action,
136                last_success,
137                last_output: ctx.last_output.clone(),
138                history_len: ctx.history.len(),
139                has_escalation,
140            };
141
142            workers.insert(worker_id, summary);
143        }
144
145        // 成功率・進捗を計算
146        let success_rate = Self::calculate_success_rate(state);
147        let max_ticks = state
148            .shared
149            .extensions
150            .get::<SwarmConfig>()
151            .map(|c| c.max_ticks)
152            .unwrap_or(0);
153        let progress = Self::calculate_progress(state, max_ticks);
154
155        // ActionsConfig を取得
156        let available_actions = state.shared.extensions.get::<ActionsConfig>().cloned();
157
158        // メタデータを構築
159        let mut metadata = HashMap::new();
160
161        // SwarmTask から情報を抽出
162        if let Some(task) = state.shared.extensions.get::<SwarmTask>() {
163            metadata.insert(
164                "task".to_string(),
165                serde_json::Value::String(task.goal.clone()),
166            );
167
168            // SwarmTask の context をコピー(オブジェクトの場合)
169            if let Some(obj) = task.context.as_object() {
170                for (key, value) in obj {
171                    metadata.insert(format!("task_{}", key), value.clone());
172                }
173            }
174        }
175
176        TaskContext {
177            tick,
178            workers,
179            success_rate,
180            progress,
181            escalations,
182            available_actions,
183            v2_guidances: None,                 // Orchestrator が後から設定
184            excluded_actions: Vec::new(),       // Orchestrator が後から設定
185            previous_guidances: HashMap::new(), // Orchestrator が後から設定
186            done_workers: HashSet::new(),       // RuntimeState から設定される
187            metadata,
188        }
189    }
190
191    fn name(&self) -> &str {
192        &self.name
193    }
194}
195
196// ============================================================================
197// Blanket Implementation for Box
198// ============================================================================
199
200impl Analyzer for Box<dyn Analyzer> {
201    fn analyze(&self, state: &SwarmState) -> TaskContext {
202        (**self).analyze(state)
203    }
204
205    fn name(&self) -> &str {
206        (**self).name()
207    }
208}
209
210// ============================================================================
211// Tests
212// ============================================================================
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_default_analyzer_empty_state() {
220        let analyzer = DefaultAnalyzer::new();
221        let state = SwarmState::new(3);
222
223        let ctx = analyzer.analyze(&state);
224
225        assert_eq!(ctx.tick, 0);
226        assert_eq!(ctx.workers.len(), 3);
227        // アクションがない場合、success_rate は 1.0(失敗していない)
228        assert_eq!(ctx.success_rate, 1.0);
229        assert!(!ctx.has_escalations());
230    }
231
232    #[test]
233    fn test_default_analyzer_with_name() {
234        let analyzer = DefaultAnalyzer::new().with_name("TestAnalyzer");
235        assert_eq!(analyzer.name(), "TestAnalyzer");
236    }
237}