use super::{NestedConfig, ValidationOutcome};
use crate::types::AiTransaction;
use std::collections::{HashMap, VecDeque};
pub struct TransactionLevel {
feature_extractor: FeatureExtractor,
classifier: TransactionClassifier,
buffer: VecDeque<ProcessedTransaction>,
buffer_size: usize,
}
impl TransactionLevel {
pub fn new(config: &NestedConfig) -> Self {
Self {
feature_extractor: FeatureExtractor::new(config.feature_dim),
classifier: TransactionClassifier::new(),
buffer: VecDeque::with_capacity(100),
buffer_size: 100,
}
}
pub fn process(&mut self, tx: AiTransaction) -> ProcessedTransaction {
let features = self.feature_extractor.extract(&tx);
let tx_type = self.classifier.classify(&features);
let confidence = self.classifier.confidence(&features);
let processed = ProcessedTransaction {
hash: tx.hash,
features,
tx_type,
confidence,
};
self.buffer.push_back(processed.clone());
if self.buffer.len() > self.buffer_size {
self.buffer.pop_front();
}
processed
}
pub fn update_features(&mut self, tx: &AiTransaction, outcome: &ValidationOutcome) {
let features = self.feature_extractor.extract(tx);
self.classifier.update(&features, outcome.valid);
}
pub fn get_recent_patterns(&self) -> Vec<&ProcessedTransaction> {
self.buffer.iter().collect()
}
pub fn buffer_stats(&self) -> BufferStats {
let type_counts: HashMap<String, usize> =
self.buffer.iter().fold(HashMap::new(), |mut acc, tx| {
*acc.entry(tx.tx_type.clone()).or_insert(0) += 1;
acc
});
let avg_confidence = if self.buffer.is_empty() {
0.0
} else {
self.buffer.iter().map(|tx| tx.confidence).sum::<f32>() / self.buffer.len() as f32
};
BufferStats {
size: self.buffer.len(),
type_distribution: type_counts,
avg_confidence,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessedTransaction {
pub hash: [u8; 32],
pub features: Vec<f32>,
pub tx_type: String,
pub confidence: f32,
}
struct FeatureExtractor {
dim: usize,
}
impl FeatureExtractor {
fn new(dim: usize) -> Self {
Self { dim }
}
fn extract(&self, tx: &AiTransaction) -> Vec<f32> {
let mut features = tx.extract_features();
features.resize(self.dim, 0.0);
let max = features.iter().cloned().fold(0.0_f32, f32::max);
if max > 0.0 {
for f in features.iter_mut() {
*f /= max;
}
}
features
}
}
struct TransactionClassifier {
centroids: HashMap<String, Vec<f32>>,
type_counts: HashMap<String, usize>,
learning_rate: f32,
}
impl TransactionClassifier {
fn new() -> Self {
let mut centroids = HashMap::new();
let mut type_counts = HashMap::new();
let types = ["data", "link", "agent", "cap", "init"];
for t in types {
centroids.insert(t.to_string(), vec![0.0; 16]);
type_counts.insert(t.to_string(), 1);
}
Self {
centroids,
type_counts,
learning_rate: 0.1,
}
}
fn classify(&self, features: &[f32]) -> String {
self.centroids
.iter()
.map(|(name, centroid)| {
let distance = Self::l2_distance(features, centroid);
(name.clone(), distance)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(name, _)| name)
.unwrap_or_else(|| "unknown".to_string())
}
fn confidence(&self, features: &[f32]) -> f32 {
let distances: Vec<_> = self
.centroids
.values()
.map(|c| Self::l2_distance(features, c))
.collect();
if distances.is_empty() {
return 0.5;
}
let mut sorted = distances.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min_dist = sorted.first().copied().unwrap_or(1.0);
let second_dist = sorted.get(1).copied().unwrap_or(min_dist);
if second_dist > 0.0 {
(1.0 - min_dist / second_dist).max(0.0).min(1.0)
} else {
1.0
}
}
fn update(&mut self, features: &[f32], was_valid: bool) {
if !was_valid {
return; }
let tx_type = self.classify(features);
if let Some(centroid) = self.centroids.get_mut(&tx_type) {
for (i, &f) in features.iter().enumerate() {
if i < centroid.len() {
centroid[i] = centroid[i] * (1.0 - self.learning_rate) + f * self.learning_rate;
}
}
}
*self.type_counts.entry(tx_type).or_insert(0) += 1;
}
fn l2_distance(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
}
#[derive(Debug, Clone)]
pub struct BufferStats {
pub size: usize,
pub type_distribution: HashMap<String, usize>,
pub avg_confidence: f32,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tx(id: u8) -> AiTransaction {
AiTransaction {
hash: [id; 32],
timestamp: 1702656000000,
agent: [1u8; 32],
entry_type: "test".to_string(),
data: vec![id; 10],
size: 10,
}
}
#[test]
fn test_transaction_processing() {
let config = NestedConfig::default();
let mut tl = TransactionLevel::new(&config);
let tx = make_tx(1);
let processed = tl.process(tx);
assert_eq!(processed.features.len(), config.feature_dim);
assert!(!processed.tx_type.is_empty());
assert!(processed.confidence >= 0.0 && processed.confidence <= 1.0);
}
#[test]
fn test_buffer() {
let config = NestedConfig::default();
let mut tl = TransactionLevel::new(&config);
for i in 0..10 {
tl.process(make_tx(i));
}
let stats = tl.buffer_stats();
assert_eq!(stats.size, 10);
}
#[test]
fn test_classifier_update() {
let config = NestedConfig::default();
let mut tl = TransactionLevel::new(&config);
let tx = make_tx(1);
let processed = tl.process(tx.clone());
let outcome = ValidationOutcome {
valid: true,
time_ms: 10,
error: None,
};
tl.update_features(&tx, &outcome);
let processed2 = tl.process(tx);
assert!(processed2.confidence > 0.0);
}
}