gioser
⎇ d270c5e67457195cd845f783c0b3b7bd66b9add0 crates/ente-brain/src/autopromote.rs Rust · 102 líneas · 3.4 KB Historia Quién tocó qué crudo

audit-verify, autopromote, histogramas y hot-reload caps badf4257e · Sergio ·

1//! Autopromote loop. Background task que cada N segundos detecta cristales
2//! con thresholds altos y los promueve sin intervención humana.
3//!
4//! Anti-doble-promote: tras promover, registramos en un set la pareja
5//! (antecedent_kind, consequent_kind). Antes de promover, verificamos que
6//! no exista ya una regla con el mismo trigger_kind (heurística simple —
7//! evita ráfagas de duplicados de la misma estadística).
8
9use crate::audit::AuditAction;
10use crate::crystallize::{crystal_to_kcl, crystal_to_rule, detect_crystals, Crystal, CrystallizationParams};
11use crate::introspect::{append_kcl_snippet, BrainState};
12use crate::rules::EventKind;
13use std::collections::HashSet;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::sync::Mutex;
17use tracing::{info, warn};
18
19#[derive(Debug, Clone, Copy)]
20pub struct AutopromoteParams {
21    pub interval_secs: u64,
22    pub threshold: CrystallizationParams,
23}
24
25impl Default for AutopromoteParams {
26    fn default() -> Self {
27        Self {
28            interval_secs: 60,
29            // Más estrictos que el threshold default — evitar ruido.
30            threshold: CrystallizationParams {
31                min_support: 10,
32                min_conditional_prob: 0.85,
33                min_pmi: 2.0,
34            },
35        }
36    }
37}
38
39/// Spawn del bucle. El handle Mutex evita que dos pasadas concurrentes
40/// promuevan el mismo cristal (el lock garantiza serialización por brain).
41pub fn spawn_autopromote_loop(state: BrainState, params: AutopromoteParams) {
42    let promoted_keys: Arc<Mutex<HashSet<(EventKind, EventKind)>>> =
43        Arc::new(Mutex::new(HashSet::new()));
44
45    tokio::spawn(async move {
46        let mut tick = tokio::time::interval(Duration::from_secs(params.interval_secs));
47        tick.tick().await; // descartar primer tick inmediato
48        info!(?params, "autopromote loop activo");
49        loop {
50            tick.tick().await;
51            run_one_pass(&state, &params, &promoted_keys).await;
52        }
53    });
54}
55
56async fn run_one_pass(
57    state: &BrainState,
58    params: &AutopromoteParams,
59    promoted_keys: &Arc<Mutex<HashSet<(EventKind, EventKind)>>>,
60) {
61    let crystals: Vec<Crystal> = {
62        let obs = state.observer.read().await;
63        detect_crystals(&obs, &params.threshold)
64    };
65    if crystals.is_empty() { return; }
66
67    let mut pk = promoted_keys.lock().await;
68    for c in crystals {
69        let key = (c.antecedent.clone(), c.consequent.clone());
70        if pk.contains(&key) {
71            // Ya promovido — el observer puede seguir reportando este
72            // cristal pero no necesitamos otra regla.
73            continue;
74        }
75        promote_one(state, &c).await;
76        pk.insert(key);
77    }
78}
79
80async fn promote_one(state: &BrainState, c: &Crystal) {
81    let rule = crystal_to_rule(c);
82    let snippet = crystal_to_kcl(c);
83    let rule_id = rule.id;
84    state.engine.write().await.insert(rule);
85    if let Some(path) = state.rules_out.as_ref() {
86        if let Err(e) = append_kcl_snippet(path, &snippet) {
87            warn!(?e, "autopromote: rules_out append falló");
88        }
89    }
90    state.audit.write().await.append(AuditAction::PromoteCrystal {
91        rule_id,
92        crystal: c.clone(),
93    });
94    info!(
95        %rule_id,
96        antecedent = ?c.antecedent,
97        consequent = ?c.consequent,
98        cp = c.conditional_prob,
99        pmi = c.pmi,
100        "autopromote: cristal → regla"
101    );
102}