1use 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 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}