use super::content_hash::{compute_asset_id, AssetIdError};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::sync::Mutex;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MemoryEventKind {
Signal,
Hypothesis,
Attempt,
Outcome,
ConfidenceEdge,
GeneSelected,
CapsuleCreated,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MemoryGraphEvent {
#[serde(rename = "type")]
pub event_type: String,
pub kind: MemoryEventKind,
pub id: String,
pub ts: String,
#[serde(default)]
pub signal: Option<serde_json::Value>,
#[serde(default)]
pub gene: Option<GeneRef>,
#[serde(default)]
pub outcome: Option<OutcomeRef>,
#[serde(default)]
pub hypothesis: Option<HypothesisRef>,
#[serde(default)]
pub parent: Option<String>,
}
impl MemoryGraphEvent {
pub fn signal(id: String, signal_data: serde_json::Value) -> Self {
Self {
event_type: "MemoryGraphEvent".to_string(),
kind: MemoryEventKind::Signal,
id,
ts: Utc::now().to_rfc3339(),
signal: Some(signal_data),
gene: None,
outcome: None,
hypothesis: None,
parent: None,
}
}
pub fn hypothesis(id: String, hypothesis: HypothesisRef, parent: Option<String>) -> Self {
Self {
event_type: "MemoryGraphEvent".to_string(),
kind: MemoryEventKind::Hypothesis,
id,
ts: Utc::now().to_rfc3339(),
signal: None,
gene: None,
outcome: None,
hypothesis: Some(hypothesis),
parent,
}
}
pub fn outcome(id: String, outcome: OutcomeRef, parent: Option<String>) -> Self {
Self {
event_type: "MemoryGraphEvent".to_string(),
kind: MemoryEventKind::Outcome,
id,
ts: Utc::now().to_rfc3339(),
signal: None,
gene: None,
outcome: Some(outcome),
hypothesis: None,
parent,
}
}
pub fn gene_selected(id: String, gene: GeneRef, parent: Option<String>) -> Self {
Self {
event_type: "MemoryGraphEvent".to_string(),
kind: MemoryEventKind::GeneSelected,
id,
ts: Utc::now().to_rfc3339(),
signal: None,
gene: Some(gene),
outcome: None,
hypothesis: None,
parent,
}
}
pub fn capsule_created(id: String, capsule_id: String, parent: Option<String>) -> Self {
Self {
event_type: "MemoryGraphEvent".to_string(),
kind: MemoryEventKind::CapsuleCreated,
id,
ts: Utc::now().to_rfc3339(),
signal: None,
gene: None,
outcome: None,
hypothesis: None,
parent,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GeneRef {
pub id: String,
pub category: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OutcomeRef {
pub status: String,
pub score: f32,
pub note: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HypothesisRef {
pub id: String,
pub text: String,
pub predicted_outcome: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignalGeneOutcome {
pub signal_pattern: String,
pub gene_id: String,
pub attempts: u32,
pub successes: u32,
pub success_rate: f32,
pub last_attempt: Option<String>,
pub smoothed_probability: f32,
pub weight: f32,
pub value: f32,
}
impl SignalGeneOutcome {
pub fn compute(successes: u32, total: u32, age_days: f32, half_life_days: f32) -> Self {
let attempts = total.max(1);
let successes = successes.min(attempts);
let p = (successes as f32 + 1.0) / (attempts as f32 + 2.0);
let weight = 0.5_f32.powf(age_days / half_life_days);
let value = p * weight;
Self {
signal_pattern: String::new(),
gene_id: String::new(),
attempts,
successes,
success_rate: successes as f32 / attempts as f32,
last_attempt: None,
smoothed_probability: p,
weight,
value,
}
}
}
pub struct MemoryGraph {
events: Vec<MemoryGraphEvent>,
statistics: HashMap<String, HashMap<String, SignalGeneOutcome>>,
banned: Vec<(String, String)>,
half_life_days: f32,
ban_threshold: f32,
similarity_threshold: f32,
}
impl Default for MemoryGraph {
fn default() -> Self {
Self::new()
}
}
impl MemoryGraph {
pub fn new() -> Self {
Self {
events: Vec::new(),
statistics: HashMap::new(),
banned: Vec::new(),
half_life_days: 30.0,
ban_threshold: 0.18,
similarity_threshold: 0.34,
}
}
pub fn with_config(half_life_days: f32, ban_threshold: f32, similarity_threshold: f32) -> Self {
Self {
events: Vec::new(),
statistics: HashMap::new(),
banned: Vec::new(),
half_life_days,
ban_threshold,
similarity_threshold,
}
}
pub fn append(&mut self, event: MemoryGraphEvent) {
if let Some(outcome) = &event.outcome {
if let Some(gene) = &event.gene {
if let Some(signal_data) = &event.signal {
if let Some(signal_str) = signal_data.as_str() {
self.update_statistics(signal_str, &gene.id, outcome);
}
}
}
}
self.events.push(event);
}
fn update_statistics(&mut self, signal: &str, gene_id: &str, outcome: &OutcomeRef) {
let stats_by_gene = self.statistics.entry(signal.to_string()).or_default();
let entry = stats_by_gene
.entry(gene_id.to_string())
.or_insert_with(|| SignalGeneOutcome::compute(0, 0, 0.0, self.half_life_days));
entry.attempts += 1;
if outcome.status == "success" {
entry.successes += 1;
}
entry.success_rate = entry.successes as f32 / entry.attempts as f32;
entry.last_attempt = Some(Utc::now().to_rfc3339());
let updated =
SignalGeneOutcome::compute(entry.successes, entry.attempts, 0.0, self.half_life_days);
entry.smoothed_probability = updated.smoothed_probability;
entry.weight = updated.weight;
entry.value = updated.value;
if entry.attempts >= 2 && entry.value < self.ban_threshold {
self.banned.push((signal.to_string(), gene_id.to_string()));
}
}
pub fn get_advice(&self, signals: &[String]) -> GeneAdvice {
let mut gene_scores: HashMap<String, f32> = HashMap::new();
let mut preferred: Vec<String> = Vec::new();
let mut banned: Vec<String> = Vec::new();
for signal in signals {
if let Some(stats_by_gene) = self.statistics.get(signal) {
for (gene_id, stat) in stats_by_gene {
let score = gene_scores.entry(gene_id.clone()).or_insert(0.0);
*score += stat.value;
if self.banned.iter().any(|(s, g)| s == signal && g == gene_id) {
banned.push(gene_id.clone());
} else if stat.value > 0.5 {
preferred.push(gene_id.clone());
}
}
}
}
GeneAdvice {
scores: gene_scores,
preferred,
banned,
}
}
pub fn is_banned(&self, signal: &str, gene_id: &str) -> bool {
self.banned.iter().any(|(s, g)| s == signal && g == gene_id)
}
pub fn find_similar(&self, signals: &[String]) -> Vec<(String, f32)> {
let mut similarities = Vec::new();
for (pattern, _) in &self.statistics {
let pattern_tokens: std::collections::HashSet<&str> = pattern.split('_').collect();
let signal_tokens: std::collections::HashSet<&str> =
signals.iter().flat_map(|s| s.split('_')).collect();
let intersection: usize = pattern_tokens.intersection(&signal_tokens).count();
let union = pattern_tokens.union(&signal_tokens).count();
let similarity = if union > 0 {
intersection as f32 / union as f32
} else {
0.0
};
if similarity >= self.similarity_threshold {
similarities.push((pattern.clone(), similarity));
}
}
similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
similarities
}
pub fn get_session_events(&self, session_id: &str) -> Vec<&MemoryGraphEvent> {
self.events
.iter()
.filter(|e| {
e.hypothesis
.as_ref()
.and_then(|h| h.id.split('_').nth(1))
.map(|s| s == session_id)
.unwrap_or(false)
})
.collect()
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GeneAdvice {
pub scores: HashMap<String, f32>,
pub preferred: Vec<String>,
pub banned: Vec<String>,
}
pub struct FileMemoryGraph {
path: PathBuf,
graph: Mutex<MemoryGraph>,
}
impl FileMemoryGraph {
pub fn open<P: Into<PathBuf>>(path: P) -> std::io::Result<Self> {
let path = path.into();
let graph = if path.exists() {
let file = File::open(&path)?;
let reader = BufReader::new(file);
let mut events = Vec::new();
for line in reader.lines() {
let line = line?;
if !line.trim().is_empty() {
let event: MemoryGraphEvent = serde_json::from_str(&line)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
events.push(event);
}
}
let mut g = MemoryGraph::new();
for event in events {
g.append(event);
}
g
} else {
MemoryGraph::new()
};
Ok(Self {
path,
graph: Mutex::new(graph),
})
}
pub fn append(&self, event: MemoryGraphEvent) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let line = serde_json::to_string(&event)?;
file.write_all(line.as_bytes())?;
file.write_all(b"\n")?;
let mut graph = self.graph.lock().unwrap();
graph.append(event);
Ok(())
}
pub fn get_advice(&self, signals: &[String]) -> GeneAdvice {
let graph = self.graph.lock().unwrap();
graph.get_advice(signals)
}
pub fn is_banned(&self, signal: &str, gene_id: &str) -> bool {
let graph = self.graph.lock().unwrap();
graph.is_banned(signal, gene_id)
}
pub fn graph(&self) -> std::sync::MutexGuard<'_, MemoryGraph> {
self.graph.lock().unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_graph_append() {
let mut graph = MemoryGraph::new();
let event =
MemoryGraphEvent::signal("sig_001".to_string(), serde_json::json!("timeout_error"));
graph.append(event);
assert_eq!(graph.len(), 1);
}
#[test]
fn test_signal_gene_outcome() {
let stat = SignalGeneOutcome::compute(8, 10, 0.0, 30.0);
assert_eq!(stat.attempts, 10);
assert_eq!(stat.successes, 8);
assert!((stat.smoothed_probability - 0.75).abs() < 0.01);
}
#[test]
fn test_ban_threshold() {
let mut graph = MemoryGraph::with_config(30.0, 0.18, 0.34);
for i in 0..3 {
let event =
MemoryGraphEvent::signal(format!("sig_{}", i), serde_json::json!("test_signal"));
graph.append(event);
}
assert!(graph.is_empty() || graph.len() >= 0);
}
#[test]
fn test_gene_advice() {
let graph = MemoryGraph::new();
let advice = graph.get_advice(&["timeout".to_string()]);
assert!(advice.scores.is_empty());
}
#[test]
fn test_find_similar() {
let mut graph = MemoryGraph::new();
let event = MemoryGraphEvent::signal(
"sig_001".to_string(),
serde_json::json!("connection_timeout"),
);
graph.append(event);
let similar = graph.find_similar(&["timeout_error".to_string()]);
assert!(true);
}
#[test]
fn test_memory_graph_default() {
let graph = MemoryGraph::default();
assert!(graph.is_empty());
assert_eq!(graph.half_life_days, 30.0);
assert_eq!(graph.ban_threshold, 0.18);
}
}