use anyhow::Result;
use chrono::{DateTime, Utc};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveLearningConfig {
pub learning_rate: f64,
pub buffer_size: usize,
pub min_samples_for_adaptation: usize,
pub max_adaptation_frequency: f64,
pub quality_threshold: f64,
pub enable_meta_learning: bool,
pub adaptation_batch_size: usize,
}
impl Default for AdaptiveLearningConfig {
fn default() -> Self {
Self {
learning_rate: 0.001,
buffer_size: 10000,
min_samples_for_adaptation: 100,
max_adaptation_frequency: 1.0,
quality_threshold: 0.8,
enable_meta_learning: true,
adaptation_batch_size: 32,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityFeedback {
pub query: String,
pub embedding: Vec<f64>,
pub quality_score: f64,
#[serde(with = "chrono::serde::ts_seconds")]
pub timestamp: DateTime<Utc>,
pub relevance: Option<f64>,
pub task_context: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExperienceSample {
pub input: String,
pub target: Vec<f64>,
pub current: Vec<f64>,
pub improvement_target: f64,
pub context: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AdaptationStrategy {
GradientDescent { momentum: f64, weight_decay: f64 },
Evolutionary {
mutation_rate: f64,
population_size: usize,
},
MetaLearning {
inner_steps: usize,
outer_learning_rate: f64,
},
BayesianOptimization {
exploration_factor: f64,
kernel_bandwidth: f64,
},
}
impl Default for AdaptationStrategy {
fn default() -> Self {
Self::GradientDescent {
momentum: 0.9,
weight_decay: 0.0001,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptationMetrics {
pub adaptations_count: usize,
pub avg_quality_improvement: f64,
pub adaptation_rate: f64,
pub buffer_utilization: f64,
pub performance_drift: f64,
#[serde(with = "chrono::serde::ts_seconds_option")]
pub last_adaptation: Option<DateTime<Utc>>,
}
impl Default for AdaptationMetrics {
fn default() -> Self {
Self {
adaptations_count: 0,
avg_quality_improvement: 0.0,
adaptation_rate: 0.0,
buffer_utilization: 0.0,
performance_drift: 0.0,
last_adaptation: None,
}
}
}
pub struct AdaptiveLearningSystem {
config: AdaptiveLearningConfig,
experience_buffer: Arc<RwLock<VecDeque<ExperienceSample>>>,
feedback_receiver: Arc<RwLock<Option<mpsc::UnboundedReceiver<QualityFeedback>>>>,
feedback_sender: mpsc::UnboundedSender<QualityFeedback>,
strategy: AdaptationStrategy,
metrics: Arc<RwLock<AdaptationMetrics>>,
model_parameters: Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
learning_state: Arc<RwLock<LearningState>>,
}
#[derive(Debug, Clone)]
struct LearningState {
momentum: HashMap<String, DMatrix<f64>>,
adaptation_history: VecDeque<AdaptationRecord>,
current_learning_rate: f64,
performance_baseline: f64,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct AdaptationRecord {
timestamp: DateTime<Utc>,
quality_before: f64,
quality_after: f64,
samples_used: usize,
strategy: AdaptationStrategy,
}
impl AdaptiveLearningSystem {
pub fn new(config: AdaptiveLearningConfig) -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
let learning_rate = config.learning_rate;
Self {
config,
experience_buffer: Arc::new(RwLock::new(VecDeque::new())),
feedback_receiver: Arc::new(RwLock::new(Some(receiver))),
feedback_sender: sender,
strategy: AdaptationStrategy::default(),
metrics: Arc::new(RwLock::new(AdaptationMetrics::default())),
model_parameters: Arc::new(RwLock::new(HashMap::new())),
learning_state: Arc::new(RwLock::new(LearningState {
momentum: HashMap::new(),
adaptation_history: VecDeque::new(),
current_learning_rate: learning_rate,
performance_baseline: 0.5,
})),
}
}
pub fn with_strategy(config: AdaptiveLearningConfig, strategy: AdaptationStrategy) -> Self {
let mut system = Self::new(config);
system.strategy = strategy;
system
}
pub fn submit_feedback(&self, feedback: QualityFeedback) -> Result<()> {
self.feedback_sender.send(feedback)?;
Ok(())
}
pub async fn start_learning(&self) -> Result<()> {
let mut receiver = self
.feedback_receiver
.write()
.expect("lock poisoned")
.take()
.ok_or_else(|| anyhow::anyhow!("Learning already started"))?;
info!("Starting adaptive learning system");
let experience_buffer = Arc::clone(&self.experience_buffer);
let metrics = Arc::clone(&self.metrics);
let config = self.config.clone();
tokio::spawn(async move {
while let Some(feedback) = receiver.recv().await {
if let Err(e) =
Self::process_feedback(feedback, &experience_buffer, &metrics, &config).await
{
warn!("Error processing feedback: {}", e);
}
}
});
let buffer = Arc::clone(&self.experience_buffer);
let metrics = Arc::clone(&self.metrics);
let parameters = Arc::clone(&self.model_parameters);
let learning_state = Arc::clone(&self.learning_state);
let config = self.config.clone();
let strategy = self.strategy.clone();
tokio::spawn(async move {
let mut last_adaptation = Instant::now();
loop {
tokio::time::sleep(Duration::from_millis(100)).await;
let should_adapt = {
let buffer_guard = buffer.read().expect("lock poisoned");
let _metrics_guard = metrics.read().expect("lock poisoned");
buffer_guard.len() >= config.min_samples_for_adaptation
&& last_adaptation.elapsed().as_secs_f64()
>= 1.0 / config.max_adaptation_frequency
};
if should_adapt {
match Self::perform_adaptation(
&buffer,
&metrics,
¶meters,
&learning_state,
&config,
&strategy,
)
.await
{
Err(e) => {
warn!("Error during adaptation: {}", e);
}
_ => {
last_adaptation = Instant::now();
}
}
}
}
});
Ok(())
}
async fn process_feedback(
feedback: QualityFeedback,
buffer: &Arc<RwLock<VecDeque<ExperienceSample>>>,
metrics: &Arc<RwLock<AdaptationMetrics>>,
config: &AdaptiveLearningConfig,
) -> Result<()> {
if feedback.quality_score > config.quality_threshold {
let sample = ExperienceSample {
input: feedback.query.clone(),
target: feedback.embedding.clone(),
current: feedback.embedding.clone(), improvement_target: 1.0 - feedback.quality_score,
context: feedback
.task_context
.map(|ctx| [("task".to_string(), ctx)].into())
.unwrap_or_default(),
};
{
let mut buffer_guard = buffer.write().expect("lock poisoned");
buffer_guard.push_back(sample);
while buffer_guard.len() > config.buffer_size {
buffer_guard.pop_front();
}
}
{
let mut metrics_guard = metrics.write().expect("lock poisoned");
let buffer_guard = buffer.read().expect("lock poisoned");
metrics_guard.buffer_utilization =
buffer_guard.len() as f64 / config.buffer_size as f64;
}
debug!(
"Processed feedback with quality score: {}",
feedback.quality_score
);
}
Ok(())
}
async fn perform_adaptation(
buffer: &Arc<RwLock<VecDeque<ExperienceSample>>>,
metrics: &Arc<RwLock<AdaptationMetrics>>,
parameters: &Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
learning_state: &Arc<RwLock<LearningState>>,
config: &AdaptiveLearningConfig,
strategy: &AdaptationStrategy,
) -> Result<()> {
let samples = {
let buffer_guard = buffer.read().expect("lock poisoned");
buffer_guard
.iter()
.take(config.adaptation_batch_size)
.cloned()
.collect::<Vec<_>>()
};
if samples.is_empty() {
return Ok(());
}
info!("Performing adaptation with {} samples", samples.len());
let quality_before = Self::calculate_current_quality(&samples)?;
match strategy {
AdaptationStrategy::GradientDescent {
momentum,
weight_decay,
} => {
Self::gradient_descent_adaptation(
&samples,
parameters,
learning_state,
*momentum,
*weight_decay,
config.learning_rate,
)?;
}
AdaptationStrategy::MetaLearning {
inner_steps,
outer_learning_rate,
} => {
Self::meta_learning_adaptation(
&samples,
parameters,
learning_state,
*inner_steps,
*outer_learning_rate,
)?;
}
AdaptationStrategy::Evolutionary {
mutation_rate,
population_size,
} => {
Self::evolutionary_adaptation(
&samples,
parameters,
*mutation_rate,
*population_size,
)?;
}
AdaptationStrategy::BayesianOptimization {
exploration_factor,
kernel_bandwidth,
} => {
Self::bayesian_optimization_adaptation(
&samples,
parameters,
*exploration_factor,
*kernel_bandwidth,
)?;
}
}
let quality_after = Self::calculate_current_quality(&samples)?;
{
let mut metrics_guard = metrics.write().expect("lock poisoned");
metrics_guard.adaptations_count += 1;
let improvement = quality_after - quality_before;
metrics_guard.avg_quality_improvement = (metrics_guard.avg_quality_improvement
* (metrics_guard.adaptations_count - 1) as f64
+ improvement)
/ metrics_guard.adaptations_count as f64;
metrics_guard.last_adaptation = Some(Utc::now());
}
{
let mut state_guard = learning_state.write().expect("lock poisoned");
state_guard.adaptation_history.push_back(AdaptationRecord {
timestamp: Utc::now(),
quality_before,
quality_after,
samples_used: samples.len(),
strategy: strategy.clone(),
});
while state_guard.adaptation_history.len() > 1000 {
state_guard.adaptation_history.pop_front();
}
if quality_after > quality_before {
state_guard.current_learning_rate *= 1.01; } else {
state_guard.current_learning_rate *= 0.95; }
state_guard.current_learning_rate = state_guard
.current_learning_rate
.max(config.learning_rate * 0.1)
.min(config.learning_rate * 10.0);
}
info!(
"Adaptation completed: quality improved by {:.4}",
quality_after - quality_before
);
Ok(())
}
fn calculate_current_quality(samples: &[ExperienceSample]) -> Result<f64> {
if samples.is_empty() {
return Ok(0.0);
}
let total_quality: f64 = samples
.iter()
.map(|sample| {
let current = DVector::from_vec(sample.current.clone());
let target = DVector::from_vec(sample.target.clone());
if current.len() != target.len() {
return 0.0;
}
let dot_product = current.dot(&target);
let norm_current = current.norm();
let norm_target = target.norm();
if norm_current == 0.0 || norm_target == 0.0 {
return 0.0;
}
(dot_product / (norm_current * norm_target)).max(0.0)
})
.sum();
Ok(total_quality / samples.len() as f64)
}
fn gradient_descent_adaptation(
samples: &[ExperienceSample],
parameters: &Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
learning_state: &Arc<RwLock<LearningState>>,
momentum: f64,
weight_decay: f64,
learning_rate: f64,
) -> Result<()> {
let mut params_guard = parameters.write().expect("lock poisoned");
let mut state_guard = learning_state.write().expect("lock poisoned");
for (param_name, param_matrix) in params_guard.iter_mut() {
let gradient = Self::compute_gradient(samples, param_matrix)?;
let momentum_entry = state_guard
.momentum
.entry(param_name.clone())
.or_insert_with(|| DMatrix::zeros(param_matrix.nrows(), param_matrix.ncols()));
*momentum_entry = momentum_entry.clone() * momentum + &gradient;
let decay_term = param_matrix.clone() * weight_decay;
*param_matrix -= &(momentum_entry.clone() * learning_rate + decay_term * learning_rate);
}
Ok(())
}
fn compute_gradient(
_samples: &[ExperienceSample],
param_matrix: &DMatrix<f64>,
) -> Result<DMatrix<f64>> {
let mut gradient = DMatrix::zeros(param_matrix.nrows(), param_matrix.ncols());
for i in 0..gradient.nrows() {
for j in 0..gradient.ncols() {
gradient[(i, j)] = ({
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
} - 0.5)
* 0.001;
}
}
Ok(gradient)
}
fn meta_learning_adaptation(
samples: &[ExperienceSample],
parameters: &Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
_learning_state: &Arc<RwLock<LearningState>>,
inner_steps: usize,
outer_learning_rate: f64,
) -> Result<()> {
let mut params_guard = parameters.write().expect("lock poisoned");
for _ in 0..inner_steps {
for (_, param_matrix) in params_guard.iter_mut() {
let gradient = Self::compute_gradient(samples, param_matrix)?;
*param_matrix -= &(gradient * outer_learning_rate);
}
}
Ok(())
}
fn evolutionary_adaptation(
samples: &[ExperienceSample],
parameters: &Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
mutation_rate: f64,
population_size: usize,
) -> Result<()> {
let mut params_guard = parameters.write().expect("lock poisoned");
for (_, param_matrix) in params_guard.iter_mut() {
let mut best_fitness = Self::evaluate_fitness(samples, param_matrix)?;
let mut best_params = param_matrix.clone();
for _ in 0..population_size {
let mut mutated = param_matrix.clone();
for i in 0..mutated.nrows() {
for j in 0..mutated.ncols() {
if {
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
} < mutation_rate
{
mutated[(i, j)] += ({
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
} - 0.5)
* 0.01;
}
}
}
let fitness = Self::evaluate_fitness(samples, &mutated)?;
if fitness > best_fitness {
best_fitness = fitness;
best_params = mutated;
}
}
*param_matrix = best_params;
}
Ok(())
}
fn evaluate_fitness(
_samples: &[ExperienceSample],
_param_matrix: &DMatrix<f64>,
) -> Result<f64> {
Ok({
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
})
}
fn bayesian_optimization_adaptation(
samples: &[ExperienceSample],
parameters: &Arc<RwLock<HashMap<String, DMatrix<f64>>>>,
exploration_factor: f64,
_kernel_bandwidth: f64,
) -> Result<()> {
let mut params_guard = parameters.write().expect("lock poisoned");
for (_, param_matrix) in params_guard.iter_mut() {
let current_fitness = Self::evaluate_fitness(samples, param_matrix)?;
let mut best_candidate = param_matrix.clone();
let mut best_acquisition = 0.0;
for _ in 0..10 {
let mut candidate = param_matrix.clone();
for i in 0..candidate.nrows() {
for j in 0..candidate.ncols() {
candidate[(i, j)] += ({
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
} - 0.5)
* exploration_factor;
}
}
let fitness = Self::evaluate_fitness(samples, &candidate)?;
let acquisition = fitness
+ exploration_factor * {
use scirs2_core::random::{Random, RngExt};
let mut random = Random::default();
random.random::<f64>()
};
if acquisition > best_acquisition {
best_acquisition = acquisition;
best_candidate = candidate;
}
}
if best_acquisition > current_fitness + 0.01 {
*param_matrix = best_candidate;
}
}
Ok(())
}
pub fn get_metrics(&self) -> AdaptationMetrics {
self.metrics.read().expect("lock poisoned").clone()
}
pub fn get_feedback_sender(&self) -> mpsc::UnboundedSender<QualityFeedback> {
self.feedback_sender.clone()
}
pub fn set_strategy(&mut self, strategy: AdaptationStrategy) {
self.strategy = strategy;
}
pub fn reset_learning_state(&self) {
let mut state_guard = self.learning_state.write().expect("lock poisoned");
state_guard.momentum.clear();
state_guard.adaptation_history.clear();
state_guard.current_learning_rate = self.config.learning_rate;
state_guard.performance_baseline = 0.5;
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{sleep, Duration};
#[tokio::test]
async fn test_adaptive_learning_system_creation() {
let config = AdaptiveLearningConfig::default();
let system = AdaptiveLearningSystem::new(config);
let metrics = system.get_metrics();
assert_eq!(metrics.adaptations_count, 0);
assert_eq!(metrics.avg_quality_improvement, 0.0);
}
#[tokio::test]
async fn test_feedback_submission() {
let config = AdaptiveLearningConfig::default();
let system = AdaptiveLearningSystem::new(config);
let feedback = QualityFeedback {
query: "test query".to_string(),
embedding: vec![0.1, 0.2, 0.3],
quality_score: 0.9,
timestamp: Utc::now(),
relevance: Some(0.8),
task_context: Some("similarity".to_string()),
};
assert!(system.submit_feedback(feedback).is_ok());
}
#[tokio::test]
async fn test_adaptive_learning_config_default() {
let config = AdaptiveLearningConfig::default();
assert_eq!(config.learning_rate, 0.001);
assert_eq!(config.buffer_size, 10000);
assert_eq!(config.min_samples_for_adaptation, 100);
assert_eq!(config.quality_threshold, 0.8);
assert!(config.enable_meta_learning);
}
#[tokio::test]
async fn test_adaptation_strategies() {
let config = AdaptiveLearningConfig::default();
let strategies = vec![
AdaptationStrategy::GradientDescent {
momentum: 0.9,
weight_decay: 0.0001,
},
AdaptationStrategy::MetaLearning {
inner_steps: 3,
outer_learning_rate: 0.01,
},
AdaptationStrategy::Evolutionary {
mutation_rate: 0.1,
population_size: 20,
},
AdaptationStrategy::BayesianOptimization {
exploration_factor: 0.1,
kernel_bandwidth: 1.0,
},
];
for strategy in strategies {
let system = AdaptiveLearningSystem::with_strategy(config.clone(), strategy);
assert!(system.start_learning().await.is_ok());
sleep(Duration::from_millis(10)).await;
}
}
#[tokio::test]
async fn test_quality_calculation() {
let samples = vec![
ExperienceSample {
input: "test1".to_string(),
target: vec![1.0, 0.0, 0.0],
current: vec![0.9, 0.1, 0.0],
improvement_target: 0.1,
context: HashMap::new(),
},
ExperienceSample {
input: "test2".to_string(),
target: vec![0.0, 1.0, 0.0],
current: vec![0.0, 0.8, 0.2],
improvement_target: 0.2,
context: HashMap::new(),
},
];
let quality =
AdaptiveLearningSystem::calculate_current_quality(&samples).expect("should succeed");
assert!(quality > 0.0 && quality <= 1.0);
}
}