Files
clawmates/crates/cm-orchestrator/src/judge.rs
T
Omar SobhandClaude Opus 4.8 34f744734b Large World graph, agent platform, brain stack & dashboard rebuild
Frontend
- Large World: collapse org/company/team tiers into one expandable React Flow
  hierarchy (WorldFlow) with per-click expand, persisted node positions, a
  compact tree sidebar, wrench multi-select delete across levels, and a sized
  right slide-out (phone/tablet/full) showing an agent summary + drill button.
- Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible
  System Prompt + Personality cards, restructured anatomy cards, bigger avatar
  with name/title header row, Markdown/JSON-aware rendering, brain registry +
  history, avatar generate/upload.
- User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel;
  Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered);
  Team Runs view; reap-progress modal; dashboard is the single live interface.

Backend
- cm-brain crate (.brain as the agent definition) + brain apply/history.
- Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete.
- Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks
  (migration 0013), org/company/team delete endpoints, scheduler sweeps.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 23:21:54 -07:00

123 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! An LLM-judge [`Scorer`]: ask a model to rate how well a run's output
//! accomplishes the task, on a 0100 scale, normalized to `[0,1]`.
//!
//! Gives the comparison harness real quality numbers (vs the deterministic
//! scorers used in tests). Behind the `provider` feature.
use std::sync::Arc;
use cm_llm::{ChatMessage, ChatRequest, ChatRole, ContentPart, LlmEvent, LlmProvider};
use futures::StreamExt;
use crate::{RunRecord, Scorer};
/// Scores run quality with an LLM via any `cm-llm` provider.
pub struct JudgeScorer {
provider: Arc<dyn LlmProvider>,
model: String,
max_tokens: u32,
}
impl JudgeScorer {
/// Build a judge over a shared provider, model id, and token budget.
pub fn new(provider: Arc<dyn LlmProvider>, model: impl Into<String>, max_tokens: u32) -> Self {
JudgeScorer {
provider,
model: model.into(),
max_tokens,
}
}
}
/// Extract the first integer in the text and map 0..100 → 0.0..1.0.
/// Returns 0.0 if no integer is present.
fn parse_score(s: &str) -> f64 {
let mut digits = String::new();
for c in s.chars() {
if c.is_ascii_digit() {
digits.push(c);
} else if !digits.is_empty() {
break;
}
}
digits
.parse::<f64>()
.ok()
.map(|n| (n / 100.0).clamp(0.0, 1.0))
.unwrap_or(0.0)
}
impl Scorer for JudgeScorer {
async fn score(&self, task: &str, record: &RunRecord) -> f64 {
let request = ChatRequest {
system: "You are a strict evaluator. Rate how well RESULT accomplishes TASK on a \
scale from 0 to 100. Reply with ONLY the integer."
.to_string(),
messages: vec![ChatMessage {
role: ChatRole::User,
parts: vec![ContentPart::text(format!(
"TASK:\n{task}\n\nRESULT:\n{}",
record.final_output
))],
}],
tools: vec![],
model: self.model.clone(),
max_tokens: self.max_tokens,
web_search: false,
};
let mut stream = match self.provider.stream(request).await {
Ok(s) => s,
Err(_) => return 0.0,
};
let mut text = String::new();
while let Some(event) = stream.next().await {
if let Ok(LlmEvent::TextDelta(t)) = event {
text.push_str(&t);
}
}
parse_score(&text)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{RunMetrics, RunRecord};
use cm_topology::TopologyKind;
fn record(output: &str) -> RunRecord {
RunRecord {
kind: TopologyKind::Flat,
steps: vec![],
final_output: output.to_string(),
totals: RunMetrics::default(),
}
}
#[test]
fn parse_score_handles_text_and_garbage() {
assert!((parse_score("85") - 0.85).abs() < 1e-9);
assert!((parse_score("Score: 92/100") - 0.92).abs() < 1e-9);
assert!((parse_score("over") - 0.0).abs() < 1e-9);
assert!((parse_score("150") - 1.0).abs() < 1e-9); // clamped
}
#[tokio::test]
async fn judge_returns_normalized_score() {
// Scenario keyed on "RESULT" (always in the judge prompt) returns "85".
let toml = r#"
[[scenario]]
marker = "RESULT"
[[scenario.turns]]
[[scenario.turns.events]]
type = "text"
text = "85"
"#;
let provider = Arc::new(cm_llm::ScriptedProvider::from_toml(toml).unwrap());
let judge = JudgeScorer::new(provider, "test-model", 16);
let score = judge.score("write a poem", &record("a fine poem")).await;
assert!((score - 0.85).abs() < 1e-9, "got {score}");
}
}