use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use brainwires_agents::contract_net::TaskBid;
use brainwires_agents::eval::{EvaluationCase, TrialResult, ndcg_at_k};
use brainwires_agents::market_allocation::ResourceBid;
pub struct TaskBidScoringCase;
fn make_task_bid(capability_score: f32, current_load: f32, duration_secs: u64) -> TaskBid {
TaskBid {
agent_id: "eval-agent".to_string(),
task_id: "eval-task".to_string(),
capability_score,
current_load,
estimated_duration: Duration::from_secs(duration_secs),
conditions: vec![],
submitted_at: Instant::now(),
}
}
#[async_trait]
impl EvaluationCase for TaskBidScoringCase {
fn name(&self) -> &str {
"task_bid_scoring"
}
fn category(&self) -> &str {
"agent_allocation"
}
async fn run(&self, trial_id: usize) -> anyhow::Result<TrialResult> {
let start = std::time::Instant::now();
let caps = [1.0f32, 0.7, 0.4, 0.1];
let scores_a: Vec<f64> = caps
.iter()
.map(|&c| make_task_bid(c, 0.5, 60).score() as f64)
.collect();
let ndcg_a = ndcg_at_k(&scores_a, &[3, 2, 1, 0], 4);
let loads = [0.0f32, 0.3, 0.6, 0.9];
let scores_b: Vec<f64> = loads
.iter()
.map(|&l| make_task_bid(0.5, l, 60).score() as f64)
.collect();
let ndcg_b = ndcg_at_k(&scores_b, &[3, 2, 1, 0], 4);
let durations = [0u64, 30, 120, 300];
let scores_c: Vec<f64> = durations
.iter()
.map(|&d| make_task_bid(0.5, 0.5, d).score() as f64)
.collect();
let ndcg_c = ndcg_at_k(&scores_c, &[3, 2, 1, 0], 4);
let mean_ndcg = (ndcg_a + ndcg_b + ndcg_c) / 3.0;
let ms = start.elapsed().as_millis() as u64;
let threshold = 0.99;
if ndcg_a >= threshold && ndcg_b >= threshold && ndcg_c >= threshold {
Ok(TrialResult::success(trial_id, ms)
.with_meta("ndcg_capability", serde_json::json!(ndcg_a))
.with_meta("ndcg_availability", serde_json::json!(ndcg_b))
.with_meta("ndcg_speed", serde_json::json!(ndcg_c))
.with_meta("mean_ndcg", serde_json::json!(mean_ndcg)))
} else {
Ok(TrialResult::failure(
trial_id,
ms,
format!(
"TaskBid ranking incorrect — \
NDCG(capability)={ndcg_a:.4}, \
NDCG(availability)={ndcg_b:.4}, \
NDCG(speed)={ndcg_c:.4}; \
all must be >= {threshold}"
),
)
.with_meta("mean_ndcg", serde_json::json!(mean_ndcg)))
}
}
}
pub struct ResourceBidScoringCase;
fn make_resource_bid(base_priority: u8, urgency_multiplier: f32, max_bid: u32) -> ResourceBid {
ResourceBid {
agent_id: "eval-agent".to_string(),
resource_id: "eval-resource".to_string(),
base_priority,
urgency_multiplier,
max_bid,
urgency_reason: "eval".to_string(),
estimated_duration: Duration::from_secs(60),
submitted_at: Instant::now(),
}
}
#[async_trait]
impl EvaluationCase for ResourceBidScoringCase {
fn name(&self) -> &str {
"resource_bid_scoring"
}
fn category(&self) -> &str {
"agent_allocation"
}
async fn run(&self, trial_id: usize) -> anyhow::Result<TrialResult> {
let start = std::time::Instant::now();
let bases = [9u8, 6, 3, 1];
let scores_a: Vec<f64> = bases
.iter()
.map(|&b| make_resource_bid(b, 1.0, 50).score() as f64)
.collect();
let ndcg_a = ndcg_at_k(&scores_a, &[3, 2, 1, 0], 4);
let urgencies = [2.0f32, 1.5, 1.0, 0.5];
let scores_b: Vec<f64> = urgencies
.iter()
.map(|&u| make_resource_bid(5, u, 50).score() as f64)
.collect();
let ndcg_b = ndcg_at_k(&scores_b, &[3, 2, 1, 0], 4);
let mean_ndcg = (ndcg_a + ndcg_b) / 2.0;
let ms = start.elapsed().as_millis() as u64;
let threshold = 0.99;
if ndcg_a >= threshold && ndcg_b >= threshold {
Ok(TrialResult::success(trial_id, ms)
.with_meta("ndcg_priority", serde_json::json!(ndcg_a))
.with_meta("ndcg_urgency", serde_json::json!(ndcg_b))
.with_meta("mean_ndcg", serde_json::json!(mean_ndcg)))
} else {
Ok(TrialResult::failure(
trial_id,
ms,
format!(
"ResourceBid ranking incorrect — \
NDCG(priority)={ndcg_a:.4}, \
NDCG(urgency)={ndcg_b:.4}; \
both must be >= {threshold}"
),
)
.with_meta("mean_ndcg", serde_json::json!(mean_ndcg)))
}
}
}
pub fn agent_scoring_suite() -> Vec<Arc<dyn EvaluationCase>> {
vec![
Arc::new(TaskBidScoringCase),
Arc::new(ResourceBidScoringCase),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_task_bid_scoring_passes() {
let case = TaskBidScoringCase;
let result = case.run(0).await.unwrap();
assert!(
result.success,
"TaskBidScoringCase failed: {:?}",
result.error
);
}
#[tokio::test]
async fn test_resource_bid_scoring_passes() {
let case = ResourceBidScoringCase;
let result = case.run(0).await.unwrap();
assert!(
result.success,
"ResourceBidScoringCase failed: {:?}",
result.error
);
}
}