Skip to main content

aios_agent/backends/
rule_based.rs

1//! RuleBasedBackend — 规则驱动的意图生成后端。
2//!
3//! 扫描 `StructuredContext` 中的事件信号(文件通知、Activity 启动、
4//! 前台切换、屏幕状态、电量等),生成对应的 `Intent` 列表。
5
6use std::time::Instant;
7
8use aios_spec::{
9    ActionType, ActionUrgency, AppTransition, DecisionBackendResult, DecisionRoute, Intent,
10    IntentBatch, IntentType, RiskLevel, SanitizedEventType, SemanticHint, StructuredContext,
11    SuggestedAction,
12};
13
14use crate::{new_id, DecisionBackend};
15
16pub struct RuleBasedBackend;
17
18impl RuleBasedBackend {
19    /// Generate intents by scanning context events for known signal patterns.
20    fn generate_intents(&self, context: &StructuredContext) -> Vec<Intent> {
21        let mut intents = Vec::new();
22        let summary = &context.summary;
23
24        let mut has_file_mention = false;
25        let mut has_activity_launch = false;
26        let mut launched_apps: Vec<String> = Vec::new();
27        let mut observed_foreground_apps: Vec<String> = Vec::new();
28        let mut has_screen_on = false;
29        let mut is_low_battery = false;
30        let notified_apps: Vec<String> = summary.notified_apps.clone();
31
32        for event in &context.events {
33            match &event.event_type {
34                SanitizedEventType::Notification { semantic_hints, .. }
35                    if semantic_hints.contains(&SemanticHint::FileMention) =>
36                {
37                    has_file_mention = true;
38                },
39                SanitizedEventType::InterAppInteraction {
40                    interaction_type,
41                    source_package,
42                    ..
43                } => {
44                    if matches!(interaction_type, aios_spec::InteractionType::ActivityLaunch) {
45                        has_activity_launch = true;
46                        if let Some(pkg) = source_package {
47                            if !launched_apps.contains(pkg) {
48                                launched_apps.push(pkg.clone());
49                            }
50                        }
51                    }
52                },
53                SanitizedEventType::AppTransition {
54                    package_name,
55                    transition: AppTransition::Foreground,
56                    ..
57                } if !observed_foreground_apps.contains(package_name) => {
58                    observed_foreground_apps.push(package_name.clone());
59                },
60                SanitizedEventType::FileActivity {
61                    extension_category, ..
62                } => {
63                    intents.push(Intent {
64                        intent_id: new_id(),
65                        intent_type: IntentType::HandleFile(extension_category.clone()),
66                        confidence: 0.75,
67                        risk_level: RiskLevel::Low,
68                        suggested_actions: vec![SuggestedAction {
69                            action_type: ActionType::PrefetchFile,
70                            target: None,
71                            urgency: ActionUrgency::IdleTime,
72                        }],
73                        rationale_tags: vec![format!("{:?}", extension_category)],
74                    });
75                },
76                SanitizedEventType::Screen { state } => {
77                    if matches!(state, aios_spec::ScreenState::Interactive) {
78                        has_screen_on = true;
79                    }
80                },
81                SanitizedEventType::SystemStatus {
82                    battery_pct: Some(pct),
83                    ..
84                } if *pct < 20 => {
85                    is_low_battery = true;
86                },
87                _ => {},
88            }
89        }
90
91        if has_file_mention {
92            let from_app = notified_apps.first().cloned().unwrap_or_default();
93            intents.push(Intent {
94                intent_id: new_id(),
95                intent_type: IntentType::OpenApp(from_app.clone()),
96                confidence: 0.70,
97                risk_level: RiskLevel::Low,
98                suggested_actions: vec![SuggestedAction {
99                    action_type: ActionType::PreWarmProcess,
100                    target: Some(from_app),
101                    urgency: ActionUrgency::Immediate,
102                }],
103                rationale_tags: vec!["file_received".into()],
104            });
105        }
106
107        if has_activity_launch && !launched_apps.is_empty() {
108            let target = launched_apps[0].clone();
109            intents.push(Intent {
110                intent_id: new_id(),
111                intent_type: IntentType::SwitchToApp(target.clone()),
112                confidence: 0.85,
113                risk_level: RiskLevel::Low,
114                suggested_actions: vec![
115                    SuggestedAction {
116                        action_type: ActionType::PreWarmProcess,
117                        target: Some(target.clone()),
118                        urgency: ActionUrgency::Immediate,
119                    },
120                    SuggestedAction {
121                        action_type: ActionType::KeepAlive,
122                        target: Some(target),
123                        urgency: ActionUrgency::Immediate,
124                    },
125                ],
126                rationale_tags: vec!["app_launch_detected".into()],
127            });
128        }
129
130        if let Some(target) = observed_foreground_apps.first().cloned() {
131            intents.push(Intent {
132                intent_id: new_id(),
133                intent_type: IntentType::SwitchToApp(target.clone()),
134                confidence: 0.80,
135                risk_level: RiskLevel::Low,
136                suggested_actions: vec![
137                    SuggestedAction {
138                        action_type: ActionType::PreWarmProcess,
139                        target: Some(target.clone()),
140                        urgency: ActionUrgency::Immediate,
141                    },
142                    SuggestedAction {
143                        action_type: ActionType::KeepAlive,
144                        target: Some(target),
145                        urgency: ActionUrgency::Immediate,
146                    },
147                ],
148                rationale_tags: vec!["app_foreground_observed".into()],
149            });
150        }
151
152        if has_screen_on {
153            intents.push(Intent {
154                intent_id: new_id(),
155                intent_type: IntentType::Idle,
156                confidence: 0.60,
157                risk_level: RiskLevel::Low,
158                suggested_actions: vec![SuggestedAction {
159                    action_type: ActionType::KeepAlive,
160                    target: summary.foreground_apps.first().cloned(),
161                    urgency: ActionUrgency::IdleTime,
162                }],
163                rationale_tags: vec!["screen_on".into()],
164            });
165        }
166
167        if is_low_battery {
168            intents.push(Intent {
169                intent_id: new_id(),
170                intent_type: IntentType::Idle,
171                confidence: 0.80,
172                risk_level: RiskLevel::Low,
173                suggested_actions: vec![SuggestedAction {
174                    action_type: ActionType::ReleaseMemory,
175                    target: None,
176                    urgency: ActionUrgency::Immediate,
177                }],
178                rationale_tags: vec!["low_battery".into()],
179            });
180        }
181
182        if intents.is_empty() {
183            intents.push(Intent {
184                intent_id: new_id(),
185                intent_type: IntentType::Idle,
186                confidence: 0.50,
187                risk_level: RiskLevel::Low,
188                suggested_actions: vec![SuggestedAction {
189                    action_type: ActionType::NoOp,
190                    target: None,
191                    urgency: ActionUrgency::IdleTime,
192                }],
193                rationale_tags: vec!["idle_window".into()],
194            });
195        }
196
197        tracing::debug!(
198            window_id = %context.window_id,
199            event_count = context.events.len(),
200            intent_count = intents.len(),
201            "RuleBasedBackend generated intents"
202        );
203
204        intents
205    }
206}
207
208impl DecisionBackend for RuleBasedBackend {
209    fn evaluate(&self, context: &StructuredContext) -> DecisionBackendResult {
210        let start = Instant::now();
211        let intents = self.generate_intents(context);
212        let intent_batch = IntentBatch {
213            window_id: context.window_id.clone(),
214            intents,
215            generated_at_ms: context.window_end_ms,
216            model: "rule-based-v0.2".to_string(),
217        };
218        let rationale_tags = intent_batch
219            .intents
220            .iter()
221            .flat_map(|intent| intent.rationale_tags.iter().cloned())
222            .collect();
223
224        DecisionBackendResult {
225            route: DecisionRoute::RuleBased,
226            intent_batch,
227            rationale_tags,
228            latency_us: start.elapsed().as_micros() as u64,
229            error: None,
230        }
231    }
232}