use serde::{Deserialize, Serialize};
use super::answer::Answer;
use super::question::QuestionSet;
pub const EVALUATION_RECEIPT_SCHEMA: &str = "harn.evaluation.receipt.v1";
#[derive(Clone, Default)]
pub(crate) struct EvaluationExecutionState {
pub journal: std::sync::Arc<parking_lot::Mutex<EvaluationJournal>>,
pub replay: Option<super::replay::EvaluationReplayScope>,
}
#[derive(Default)]
pub(crate) struct EvaluationJournal {
receipts: Vec<EvaluationReceipt>,
dropped: usize,
next_invocation: u64,
}
impl EvaluationJournal {
pub(crate) fn invocation_id(&mut self, execution_id: &str) -> String {
let sequence = self.next_invocation;
self.next_invocation = sequence
.checked_add(1)
.expect("evaluation sequence exhausted");
format!("{execution_id}/evaluation/{sequence}")
}
pub(crate) fn record(&mut self, receipt: EvaluationReceipt) {
if self.receipts.len() < 1024 {
self.receipts.push(receipt);
} else {
self.dropped += 1;
}
}
pub(crate) fn snapshot(&self) -> (Vec<EvaluationReceipt>, usize) {
(self.receipts.clone(), self.dropped)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeTransportReceipt {
pub data_controls: crate::llm::api::DataControlsReceipt,
pub provider_attempts_reported: Option<u64>,
pub final_provider_reported: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationSource {
Live,
Cache,
Tape,
Fixture,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccountingStatus {
Settled,
UsageUnknown,
NotDispatched,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CostAdmission {
AdaptiveProjection,
ConservativeUpperBound,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvaluationIdentity {
#[serde(default)]
pub contract_version: String,
#[serde(default)]
pub structured_output_strategy: Option<String>,
pub input_digest: String,
pub canonical_input_type: String,
pub question_set_digest: String,
pub policy_digest: String,
pub evaluator_instruction_version: String,
pub output_schema_version: String,
pub backend_kind: String,
pub protocol: String,
}
pub const EVALUATION_IDENTITY_CONTRACT: &str = "harn.evaluation_identity.v1";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EvaluationQuestionReceipt {
pub id: String,
pub kind: String,
pub confidence_kind: Option<String>,
pub raw_probabilities: std::collections::BTreeMap<String, f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EvaluationReceipt {
#[serde(default)]
pub invocation_id: Option<String>,
#[serde(default)]
pub reused_from: Option<Box<EvaluationReceipt>>,
#[serde(default)]
pub cost_admission: Option<CostAdmission>,
#[serde(default)]
pub usage: Option<Box<crate::llm::usage::LlmUsage>>,
pub native_transport: Option<NativeTransportReceipt>,
pub schema: String,
pub evaluation_id: String,
pub site_id: String,
pub identity: EvaluationIdentity,
pub requested_provider: String,
pub requested_model: String,
pub served_model: Option<String>,
pub questions: Vec<EvaluationQuestionReceipt>,
pub outcome_kind: String,
pub physical_attempts: u32,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cost_usd: Option<f64>,
#[serde(default)]
pub budget_charge_usd: Option<f64>,
pub accounting_status: AccountingStatus,
pub source: EvaluationSource,
pub elapsed_ms: u64,
pub provider_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admission_reason: Option<String>,
pub estimated_state_tokens: Option<usize>,
pub estimated_longest_question_tokens: Option<usize>,
pub estimated_request_tokens: Option<usize>,
pub limit_tokens: Option<usize>,
}
impl EvaluationReceipt {
#[allow(clippy::too_many_arguments)]
pub(crate) fn not_dispatched(
evaluation_id: String,
site_id: String,
identity: EvaluationIdentity,
requested_provider: String,
requested_model: String,
questions: &QuestionSet,
outcome_kind: &str,
elapsed_ms: u64,
) -> Self {
Self {
invocation_id: None,
reused_from: None,
cost_admission: None,
usage: None,
native_transport: None,
schema: EVALUATION_RECEIPT_SCHEMA.into(),
evaluation_id,
site_id,
identity,
requested_provider,
requested_model,
served_model: None,
questions: questions
.questions
.iter()
.map(|question| EvaluationQuestionReceipt {
id: question.id.clone(),
kind: question.body.kind().as_str().into(),
confidence_kind: None,
raw_probabilities: Default::default(),
})
.collect(),
outcome_kind: outcome_kind.into(),
physical_attempts: 0,
input_tokens: None,
output_tokens: None,
cost_usd: None,
budget_charge_usd: None,
accounting_status: AccountingStatus::NotDispatched,
source: EvaluationSource::Live,
elapsed_ms,
provider_reason: None,
admission_reason: None,
estimated_state_tokens: None,
estimated_longest_question_tokens: None,
estimated_request_tokens: None,
limit_tokens: None,
}
}
pub(crate) fn record_answers(&mut self, answers: &[Answer]) {
for question in self.questions.iter_mut() {
let Some(answer) = answers
.iter()
.find(|answer| answer.question_id == question.id)
else {
continue;
};
question.confidence_kind = Some(
match answer.confidence_kind {
super::answer::ConfidenceKind::BinaryProbability => "binary_probability",
super::answer::ConfidenceKind::DistributionShape => "distribution_shape",
super::answer::ConfidenceKind::ModelRationale => "model_rationale",
}
.into(),
);
question.raw_probabilities = answer.raw_probabilities.clone();
}
}
pub fn reference(&self) -> String {
self.invocation_id
.as_ref()
.unwrap_or(&self.evaluation_id)
.clone()
}
}