chore: additional clippy fixes after rebasing

This commit is contained in:
Per Stark
2026-05-27 07:37:18 +02:00
parent 293440b0ee
commit 414d2f5b34
39 changed files with 321 additions and 402 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ mod tests {
assert_status_code(error, StatusCode::UNAUTHORIZED);
// Test payload too large status
let error = ApiError::PayloadTooLarge("too big".to_string());
let error = ApiErr::PayloadTooLarge("too big".to_string());
assert_status_code(error, StatusCode::PAYLOAD_TOO_LARGE);
}
+2 -2
View File
@@ -6,7 +6,7 @@ use axum::{
Router,
};
use middleware_api_auth::api_auth;
use routes::{categories::get_categories, ingest::ingest_data, liveness::live, readiness::ready};
use routes::{categories::list, ingest::ingest_data, liveness::live, readiness::ready};
pub mod api_state;
pub mod error;
@@ -32,7 +32,7 @@ where
app_state.config.ingest_max_body_bytes,
)),
)
.route("/categories", get(get_categories))
.route("/categories", get(list))
.route_layer(from_fn_with_state(app_state.clone(), api_auth));
public.merge(protected)
+3 -3
View File
@@ -29,7 +29,7 @@ pub async fn ingest_data(
State(state): State<ApiState>,
Extension(user): Extension<User>,
TypedMultipart(input): TypedMultipart<IngestParams>,
) -> Result<impl IntoResponse, ApiError> {
) -> Result<impl IntoResponse, ApiErr> {
let user_id = user.id;
let content_bytes = input.content.as_ref().map_or(0, |c| c.len());
let has_content = input.content.as_ref().is_some_and(|c| !c.trim().is_empty());
@@ -46,10 +46,10 @@ pub async fn ingest_data(
) {
Ok(()) => {}
Err(IngestValidationError::PayloadTooLarge(message)) => {
return Err(ApiError::PayloadTooLarge(message));
return Err(ApiErr::PayloadTooLarge(message));
}
Err(IngestValidationError::BadRequest(message)) => {
return Err(ApiError::ValidationError(message));
return Err(ApiErr::ValidationError(message));
}
}
+1 -1
View File
@@ -367,7 +367,7 @@ pub mod testing {
storage: StorageKind::S3,
s3_bucket: Some(configured_test_s3_bucket()),
s3_endpoint: Some(configured_test_s3_endpoint()),
s3_region: Some("us-east-1".into()),
s3_region: "us-east-1".into(),
pdf_ingest_mode: PdfIngestMode::LlmFirst,
..Default::default()
}
+3 -4
View File
@@ -705,7 +705,7 @@ mod tests {
let database = &Uuid::new_v4().to_string();
let db = SurrealDbClient::memory(namespace, database)
.await
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
.expect("Failed to start in-memory surrealdb");
// Create a FileInfo instance directly
let now = Utc::now();
@@ -728,14 +728,13 @@ mod tests {
let retrieved = db
.get_item::<FileInfo>(&file_info.id)
.await
.with_context(|| "Failed to retrieve file info".to_string())?
.with_context(|| "expected file".to_string())?;
.expect("Failed to retrieve file info")
.expect("expected file");
assert_eq!(retrieved.id, file_info.id);
assert_eq!(retrieved.sha256, file_info.sha256);
assert_eq!(retrieved.file_name, file_info.file_name);
assert_eq!(retrieved.path, file_info.path);
assert_eq!(retrieved.mime_type, file_info.mime_type);
Ok(())
}
#[tokio::test]
+4 -6
View File
@@ -829,21 +829,19 @@ mod tests {
let database = &Uuid::new_v4().to_string();
let db = SurrealDbClient::memory(namespace, database)
.await
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
.expect("Failed to start in-memory surrealdb");
db.apply_migrations()
.await
.with_context(|| "Failed to apply migrations".to_string())?;
.expect("Failed to apply migrations");
KnowledgeEntityEmbedding::redefine_hnsw_index(&db, 3)
.await
.with_context(|| "Failed to redefine index length".to_string())?;
.expect("Failed to redefine index length");
let results = KnowledgeEntity::vector_search(5, vec![0.1, 0.2, 0.3], &db, "user")
.await
.with_context(|| "vector search".to_string())?;
.expect("vector search");
assert!(results.is_empty());
Ok(())
}
#[tokio::test]
+1 -1
View File
@@ -1036,7 +1036,7 @@ mod tests {
#[tokio::test]
async fn test_set_api_key_with_none_theme() {
let db = setup_test_db().await;
let db = setup_test_db().await.expect("Failed to setup test db");
let user = User::create_new(
"legacy_theme@example.com".to_string(),
+3
View File
@@ -198,6 +198,7 @@ pub struct DatabaseArgs {
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Config {
/// Convert the selected dataset and exit
#[arg(long)]
@@ -344,10 +345,12 @@ pub struct Config {
}
impl Config {
#[allow(clippy::unused_self)]
pub fn context_token_limit(&self) -> Option<usize> {
None
}
#[allow(clippy::too_many_lines)]
pub fn finalize(&mut self) -> Result<()> {
// Handle dataset paths
if let Some(raw) = &self.raw {
+3 -3
View File
@@ -1,6 +1,6 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -19,7 +19,7 @@ struct EmbeddingCacheData {
#[derive(Clone)]
pub struct EmbeddingCache {
path: Arc<PathBuf>,
path: Arc<Path>,
data: Arc<Mutex<EmbeddingCacheData>>,
dirty: Arc<AtomicBool>,
}
@@ -39,7 +39,7 @@ impl EmbeddingCache {
};
Ok(Self {
path: Arc::new(path),
path: Arc::from(path.as_path()),
data: Arc::new(Mutex::new(data)),
dirty: Arc::new(AtomicBool::new(false)),
})
+2 -1
View File
@@ -163,6 +163,7 @@ mod tests {
}
#[test]
#[allow(clippy::indexing_slicing)]
fn cases_respect_mode_filters() {
let mut manifest = sample_manifest();
manifest.metadata.include_unanswerable = false;
@@ -173,7 +174,7 @@ mod tests {
assert_eq!(strict_cases[0].question_id, "q1");
assert_eq!(strict_cases[0].paragraph_title, "Alpha");
let mut llm_manifest = manifest.clone();
let mut llm_manifest = manifest;
llm_manifest.metadata.include_unanswerable = true;
llm_manifest.metadata.require_verified_chunks = false;
+23 -13
View File
@@ -108,7 +108,15 @@ struct IngestionStats {
negative_ingested: usize,
}
#[allow(clippy::too_many_arguments)]
#[allow(
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::arithmetic_side_effects,
clippy::indexing_slicing
)]
pub async fn ensure_corpus(
dataset: &ConvertedDataset,
slice: &ResolvedSlice<'_>,
@@ -167,7 +175,7 @@ pub async fn ensure_corpus(
.paragraphs
.get(idx)
.copied()
.ok_or_else(|| anyhow!("slice missing paragraph index {}", idx))?;
.ok_or_else(|| anyhow!("slice missing paragraph index {idx}"))?;
plan.push(ParagraphPlan {
slot: plan.len(),
entry,
@@ -236,8 +244,8 @@ pub async fn ensure_corpus(
let new_shards = ingest_paragraph_batch(
dataset,
&ingest_requests,
embedding.clone(),
openai.clone(),
Arc::clone(&embedding),
Arc::clone(&openai),
user_id,
&ingestion_fingerprint,
&embedding_backend_label,
@@ -265,10 +273,10 @@ pub async fn ensure_corpus(
.context("shard record missing after ingestion run")?;
if cache.refresh_embeddings_only || shard_record.needs_reembed {
// Embeddings are now generated by the pipeline using FastEmbed - no need to re-embed
shard_record.shard.ingestion_fingerprint = ingestion_fingerprint.clone();
shard_record.shard.ingestion_fingerprint.clone_from(&ingestion_fingerprint);
shard_record.shard.ingested_at = Utc::now();
shard_record.shard.embedding_backend = embedding_backend_label.clone();
shard_record.shard.embedding_model = embedding_model_code.clone();
shard_record.shard.embedding_backend.clone_from(&embedding_backend_label);
shard_record.shard.embedding_model.clone_from(&embedding_model_code);
shard_record.shard.embedding_dimension = embedding_dimension;
shard_record.dirty = true;
shard_record.needs_reembed = false;
@@ -282,7 +290,7 @@ pub async fn ensure_corpus(
let mut corpus_paragraphs = Vec::with_capacity(plan.len());
for record in &records {
let shard = &record.as_ref().expect("record missing").shard;
let shard = &record.as_ref().context("record missing")?.shard;
corpus_paragraphs.push(shard.to_corpus_paragraph());
}
@@ -438,11 +446,11 @@ async fn ingest_paragraph_batch(
let pipeline_config = ingestion_config.clone();
let pipeline = IngestionPipeline::new_with_config(
db,
openai.clone(),
Arc::clone(&openai),
app_config,
None::<Arc<retrieval_pipeline::reranking::RerankerPool>>,
storage,
embedding.clone(),
Arc::clone(&embedding),
pipeline_config,
)?;
let pipeline = Arc::new(pipeline);
@@ -458,11 +466,11 @@ async fn ingest_paragraph_batch(
);
let model_clone = embedding_model.clone();
let backend_clone = embedding_backend.to_string();
let pipeline_clone = pipeline.clone();
let pipeline_clone = Arc::clone(&pipeline);
let category_clone = category.clone();
let tasks = batch.iter().cloned().map(move |request| {
ingest_single_paragraph(
pipeline_clone.clone(),
Arc::clone(&pipeline_clone),
request,
category_clone.clone(),
user_id,
@@ -684,6 +692,7 @@ pub fn corpus_handle_from_manifest(manifest: CorpusManifest, base_dir: PathBuf)
}
}
#[allow(clippy::indexing_slicing)]
fn compute_file_checksum(path: &Path) -> Result<String> {
let mut file = fs::File::open(path)
.with_context(|| format!("opening file {} for checksum", path.display()))?;
@@ -736,7 +745,8 @@ mod tests {
}
}
fn dummy_slice<'a>(dataset: &'a ConvertedDataset) -> ResolvedSlice<'a> {
#[allow(clippy::too_many_lines, clippy::indexing_slicing)]
fn dummy_slice(dataset: &ConvertedDataset) -> ResolvedSlice<'_> {
let paragraph = &dataset.paragraphs[0];
let question = &paragraph.questions[0];
let manifest = SliceManifest {
+17 -8
View File
@@ -197,6 +197,13 @@ pub struct CorpusHandle {
pub negative_ingested: usize,
}
#[allow(
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::indexing_slicing
)]
pub fn window_manifest(
manifest: &CorpusManifest,
offset: usize,
@@ -211,9 +218,7 @@ pub fn window_manifest(
}
if offset >= total {
return Err(anyhow!(
"window offset {} exceeds manifest questions ({})",
offset,
total
"window offset {offset} exceeds manifest questions ({total})"
));
}
let end = (offset + length).min(total);
@@ -601,6 +606,7 @@ fn normalize_answer_text(text: &str) -> String {
.join(" ")
}
#[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
fn chunk_items<T: Clone + Serialize>(
items: &[T],
max_items: usize,
@@ -644,6 +650,7 @@ fn chunk_items<T: Clone + Serialize>(
Ok(batches)
}
#[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
async fn execute_batched_inserts<T: Clone + Serialize + 'static>(
db: &SurrealDbClient,
statement: impl AsRef<str>,
@@ -781,6 +788,7 @@ mod tests {
use common::storage::types::knowledge_entity::KnowledgeEntityType;
use uuid::Uuid;
#[allow(clippy::too_many_lines)]
fn build_manifest() -> CorpusManifest {
let user_id = "user-1".to_string();
let source_id = "source-1".to_string();
@@ -822,9 +830,9 @@ mod tests {
id: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
source_id: source_id.clone(),
source_id,
chunk: "chunk text".to_string(),
user_id: user_id.clone(),
user_id,
};
let paragraph_one = CorpusParagraph {
@@ -846,9 +854,9 @@ mod tests {
let paragraph_two = CorpusParagraph {
paragraph_id: "p2".to_string(),
title: "Paragraph 2".to_string(),
text_content: text_content.clone(),
text_content,
entities: vec![EmbeddedKnowledgeEntity {
entity: entity.clone(),
entity,
embedding: vec![0.1, 0.2, 0.3],
}],
relationships: Vec::new(),
@@ -865,7 +873,7 @@ mod tests {
question_text: "What is this?".to_string(),
answers: vec!["Hello".to_string()],
is_impossible: false,
matching_chunk_ids: vec![chunk.id.clone()],
matching_chunk_ids: vec![chunk.id],
};
CorpusManifest {
@@ -893,6 +901,7 @@ mod tests {
}
}
#[allow(clippy::indexing_slicing, clippy::expect_used)]
#[test]
fn window_manifest_trims_questions_and_negatives() {
let manifest = build_manifest();
+11 -7
View File
@@ -47,6 +47,7 @@ struct QrelEntry {
score: i32,
}
#[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
pub fn convert_beir(raw_dir: &Path, dataset: DatasetKind) -> Result<Vec<ConvertedParagraph>> {
let corpus_path = raw_dir.join("corpus.jsonl");
let queries_path = raw_dir.join("queries.jsonl");
@@ -76,18 +77,17 @@ pub fn convert_beir(raw_dir: &Path, dataset: DatasetKind) -> Result<Vec<Converte
let mut skipped_answers = 0usize;
for (query_id, entries) in qrels {
let query = if let Some(query) = queries.get(&query_id) { query } else {
let Some(query) = queries.get(&query_id) else {
missing_queries += 1;
warn!(query_id = %query_id, "Skipping qrels entry for missing query");
continue;
};
let best = match select_best_doc(&entries) {
Some(entry) => entry,
None => continue,
let Some(best) = select_best_doc(&entries) else {
continue;
};
let paragraph_slot = if let Some(slot) = paragraph_index.get(&best.doc_id) { *slot } else {
let Some(&paragraph_slot) = paragraph_index.get(&best.doc_id) else {
missing_docs += 1;
warn!(
query_id = %query_id,
@@ -97,8 +97,7 @@ pub fn convert_beir(raw_dir: &Path, dataset: DatasetKind) -> Result<Vec<Converte
continue;
};
let answer = answer_snippet(&paragraphs[paragraph_slot].context);
let answers = if let Some(snippet) = answer { vec![snippet] } else {
let Some(snippet) = answer_snippet(&paragraphs[paragraph_slot].context) else {
skipped_answers += 1;
warn!(
query_id = %query_id,
@@ -107,6 +106,7 @@ pub fn convert_beir(raw_dir: &Path, dataset: DatasetKind) -> Result<Vec<Converte
);
continue;
};
let answers = vec![snippet];
let question_id = format!("{}-{query_id}", dataset.source_prefix());
paragraphs[paragraph_slot]
@@ -147,6 +147,7 @@ fn resolve_qrels_path(raw_dir: &Path) -> Result<PathBuf> {
))
}
#[allow(clippy::arithmetic_side_effects)]
fn load_corpus(path: &Path) -> Result<BTreeMap<String, BeirParagraph>> {
let file =
File::open(path).with_context(|| format!("opening BEIR corpus at {}", path.display()))?;
@@ -181,6 +182,7 @@ fn load_corpus(path: &Path) -> Result<BTreeMap<String, BeirParagraph>> {
Ok(corpus)
}
#[allow(clippy::arithmetic_side_effects)]
fn load_queries(path: &Path) -> Result<BTreeMap<String, BeirQuery>> {
let file = File::open(path)
.with_context(|| format!("opening BEIR queries file at {}", path.display()))?;
@@ -211,6 +213,7 @@ fn load_queries(path: &Path) -> Result<BTreeMap<String, BeirQuery>> {
Ok(queries)
}
#[allow(clippy::arithmetic_side_effects)]
fn load_qrels(path: &Path) -> Result<BTreeMap<String, Vec<QrelEntry>>> {
let file =
File::open(path).with_context(|| format!("opening BEIR qrels at {}", path.display()))?;
@@ -294,6 +297,7 @@ mod tests {
use tempfile::tempdir;
#[test]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
fn converts_basic_beir_layout() {
let dir = tempdir().unwrap();
let corpus = r#"
+12 -6
View File
@@ -305,7 +305,7 @@ impl DatasetKind {
pub fn category(self) -> &'static str {
match self {
Self::SquadV2 => "SQuAD v2.0",
Self::NaturalQuestions => "Natural Questions",
Self::NaturalQuestions | Self::NqBeir => "Natural Questions",
Self::Beir => "BEIR",
Self::Fever => "FEVER",
Self::Fiqa => "FiQA-2018",
@@ -314,14 +314,13 @@ impl DatasetKind {
Self::Quora => "Quora",
Self::TrecCovid => "TREC-COVID",
Self::Scifact => "SciFact",
Self::NqBeir => "Natural Questions",
}
}
pub fn entity_suffix(self) -> &'static str {
match self {
Self::SquadV2 => "SQuAD",
Self::NaturalQuestions => "Natural Questions",
Self::NaturalQuestions | Self::NqBeir => "Natural Questions",
Self::Beir => "BEIR",
Self::Fever => "FEVER",
Self::Fiqa => "FiQA",
@@ -330,7 +329,6 @@ impl DatasetKind {
Self::Quora => "Quora",
Self::TrecCovid => "TREC-COVID",
Self::Scifact => "SciFact",
Self::NqBeir => "Natural Questions",
}
}
@@ -351,11 +349,19 @@ impl DatasetKind {
}
pub fn default_raw_path(self) -> PathBuf {
dataset_entry_for_kind(self).map_or_else(|err| panic!("dataset manifest missing entry for {self:?}: {err}"), |entry| entry.raw_path.clone())
#[allow(clippy::panic)]
match dataset_entry_for_kind(self) {
Ok(entry) => entry.raw_path.clone(),
Err(err) => panic!("dataset manifest missing entry for {self:?}: {err}"),
}
}
pub fn default_converted_path(self) -> PathBuf {
dataset_entry_for_kind(self).map_or_else(|err| panic!("dataset manifest missing entry for {self:?}: {err}"), |entry| entry.converted_path.clone())
#[allow(clippy::panic)]
match dataset_entry_for_kind(self) {
Ok(entry) => entry.converted_path.clone(),
Err(err) => panic!("dataset manifest missing entry for {self:?}: {err}"),
}
}
}
+1
View File
@@ -11,6 +11,7 @@ use tracing::warn;
use super::{ConvertedParagraph, ConvertedQuestion};
#[allow(clippy::too_many_lines, clippy::arithmetic_side_effects, clippy::cast_sign_loss)]
pub fn convert_nq(
raw_path: &Path,
include_unanswerable: bool,
+2 -2
View File
@@ -55,6 +55,7 @@ mod tests {
}
#[tokio::test]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)]
async fn reset_namespace_drops_existing_rows() {
let namespace = format!("reset_ns_{}", Uuid::new_v4().simple());
let database = format!("reset_db_{}", Uuid::new_v4().simple());
@@ -90,8 +91,7 @@ mod tests {
let rows: Vec<FooRow> = response.take(0).unwrap_or_default();
assert!(
rows.is_empty(),
"reset namespace should drop rows, found {:?}",
rows
"reset namespace should drop rows, found {rows:?}",
);
}
Err(error) => {
+2 -2
View File
@@ -25,7 +25,7 @@ use crate::{
};
/// Grow the slice ledger to contain the target number of cases.
pub async fn grow_slice(dataset: &ConvertedDataset, config: &Config) -> Result<()> {
pub fn grow_slice(dataset: &ConvertedDataset, config: &Config) -> Result<()> {
let ledger_limit = ledger_target(config);
let slice_settings = slice::slice_config_with_limit(config, ledger_limit);
let slice =
@@ -70,8 +70,8 @@ pub(crate) async fn write_chunk_diagnostics(path: &Path, cases: &[CaseDiagnostic
Ok(())
}
#[allow(clippy::cast_precision_loss)]
pub(crate) async fn warm_hnsw_cache(db: &SurrealDbClient, dimension: usize) -> Result<()> {
// Create a dummy embedding for cache warming
let dummy_embedding: Vec<f32> = (0..dimension).map(|i| (i as f32).sin()).collect();
info!("Warming HNSW caches with sample queries");
+2 -1
View File
@@ -22,6 +22,7 @@ use tracing::info;
use tracing_subscriber::{fmt, EnvFilter};
/// Configure `SurrealDB` environment variables for optimal performance
#[allow(clippy::arithmetic_side_effects, clippy::unwrap_used)]
fn configure_surrealdb_performance(cpu_count: usize) {
// Set environment variables only if they're not already set
let indexing_batch_size = std::env::var("SURREAL_INDEXING_BATCH_SIZE")
@@ -74,6 +75,7 @@ fn main() -> anyhow::Result<()> {
runtime.block_on(async_main())
}
#[allow(clippy::too_many_lines)]
async fn async_main() -> anyhow::Result<()> {
// Log runtime configuration
let cpu_count = std::thread::available_parallelism()?.get();
@@ -165,7 +167,6 @@ async fn async_main() -> anyhow::Result<()> {
if parsed.config.slice_grow.is_some() {
eval::grow_slice(&dataset, &parsed.config)
.await
.context("growing slice ledger")?;
return Ok(());
}
+1 -1
View File
@@ -101,7 +101,7 @@ pub(crate) async fn can_reuse_namespace(
ingestion_fingerprint: &str,
slice_case_count: usize,
) -> Result<bool> {
let state = if let Some(state) = descriptor.load_db_state().await? { state } else {
let Some(state) = descriptor.load_db_state().await? else {
info!("No namespace state recorded; reseeding corpus from cached shards");
return Ok(false);
};
+1
View File
@@ -213,6 +213,7 @@ mod tests {
}
#[test]
#[allow(clippy::unwrap_used, clippy::expect_used)]
fn writes_perf_mirrors_from_record() {
let tmp = tempdir().unwrap();
let report_root = tmp.path().join("reports");
+20 -20
View File
@@ -4,6 +4,7 @@ use std::{
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use async_openai::Client;
use common::{
storage::{
@@ -26,6 +27,7 @@ use crate::{
slice, snapshot,
};
#[allow(clippy::struct_excessive_bools)]
pub(super) struct EvaluationContext<'a> {
dataset: &'a ConvertedDataset,
config: &'a Config,
@@ -119,41 +121,39 @@ impl<'a> EvaluationContext<'a> {
self.config
}
pub fn slice(&self) -> &slice::ResolvedSlice<'a> {
self.slice.as_ref().expect("slice has not been prepared")
pub fn slice(&self) -> Result<&slice::ResolvedSlice<'a>> {
self.slice.as_ref().ok_or_else(|| anyhow!("slice has not been prepared"))
}
pub fn db(&self) -> &SurrealDbClient {
self.db.as_ref().expect("database connection missing")
pub fn db(&self) -> Result<&SurrealDbClient> {
self.db.as_ref().ok_or_else(|| anyhow!("database connection missing"))
}
pub fn descriptor(&self) -> &snapshot::Descriptor {
pub fn descriptor(&self) -> Result<&snapshot::Descriptor> {
self.descriptor
.as_ref()
.expect("snapshot descriptor unavailable")
.ok_or_else(|| anyhow!("snapshot descriptor unavailable"))
}
pub fn embedding_provider(&self) -> &EmbeddingProvider {
pub fn embedding_provider(&self) -> Result<&EmbeddingProvider> {
self.embedding_provider
.as_ref()
.expect("embedding provider not initialised")
.ok_or_else(|| anyhow!("embedding provider not initialised"))
}
pub fn openai_client(&self) -> Arc<Client<async_openai::config::OpenAIConfig>> {
self.openai_client
.as_ref()
.expect("openai client missing")
.clone()
pub fn openai_client(&self) -> Result<Arc<Client<async_openai::config::OpenAIConfig>>> {
Ok(Arc::clone(self.openai_client.as_ref().ok_or_else(|| anyhow!("openai client missing"))?))
}
pub fn corpus_handle(&self) -> &corpus::CorpusHandle {
self.corpus_handle.as_ref().expect("corpus handle missing")
pub fn corpus_handle(&self) -> Result<&corpus::CorpusHandle> {
self.corpus_handle.as_ref().ok_or_else(|| anyhow!("corpus handle missing"))
}
pub fn evaluation_user(&self) -> &User {
self.eval_user.as_ref().expect("evaluation user missing")
pub fn evaluation_user(&self) -> Result<&User> {
self.eval_user.as_ref().ok_or_else(|| anyhow!("evaluation user missing"))
}
#[allow(clippy::arithmetic_side_effects)]
pub fn record_stage_duration(&mut self, stage: EvalStage, duration: Duration) {
let elapsed = duration.as_millis();
match stage {
@@ -167,8 +167,8 @@ impl<'a> EvaluationContext<'a> {
}
}
pub fn into_summary(self) -> EvaluationSummary {
self.summary.expect("evaluation summary missing")
pub fn into_summary(self) -> Result<EvaluationSummary> {
self.summary.ok_or_else(|| anyhow!("evaluation summary missing"))
}
}
@@ -184,7 +184,7 @@ pub(super) enum EvalStage {
}
impl EvalStage {
pub fn label(&self) -> &'static str {
pub fn label(self) -> &'static str {
match self {
EvalStage::PrepareSlice => "prepare-slice",
EvalStage::PrepareDb => "prepare-db",
+1 -1
View File
@@ -23,5 +23,5 @@ pub async fn run_evaluation(
let machine = stages::summarize(machine, &mut ctx).await?;
let _ = stages::finalize(machine, &mut ctx).await?;
Ok(ctx.into_summary())
ctx.into_summary()
}
+1 -1
View File
@@ -55,5 +55,5 @@ pub(crate) async fn finalize(
machine
.finalize()
.map_err(|(_, guard)| map_guard_error("finalize", guard))
.map_err(|(_, guard)| map_guard_error("finalize", &guard))
}
+1 -1
View File
@@ -19,7 +19,7 @@ use state_machines::core::GuardError;
use super::state::EvaluationMachine;
fn map_guard_error(event: &str, guard: GuardError) -> anyhow::Error {
fn map_guard_error(event: &str, guard: &GuardError) -> anyhow::Error {
anyhow::anyhow!("invalid evaluation pipeline transition during {event}: {guard:?}")
}
@@ -11,6 +11,7 @@ use super::super::{
};
use super::{map_guard_error, StageResult};
#[allow(clippy::too_many_lines)]
pub(crate) async fn prepare_corpus(
machine: EvaluationMachine<(), DbReady>,
ctx: &mut EvaluationContext<'_>,
@@ -24,13 +25,13 @@ pub(crate) async fn prepare_corpus(
let config = ctx.config();
let cache_settings = corpus::CorpusCacheConfig::from(config);
let embedding_provider = ctx.embedding_provider().clone();
let openai_client = ctx.openai_client();
let slice = ctx.slice();
let embedding_provider = ctx.embedding_provider()?.clone();
let openai_client = ctx.openai_client()?;
let slice = ctx.slice()?;
let window = slice::select_window(slice, ctx.config().slice_offset, ctx.config().limit)
.context("selecting slice window for corpus preparation")?;
let descriptor = snapshot::Descriptor::new(config, slice, ctx.embedding_provider());
let descriptor = snapshot::Descriptor::new(config, slice, ctx.embedding_provider()?);
let ingestion_config = corpus::make_ingestion_config(config);
let expected_fingerprint = corpus::compute_ingestion_fingerprint(
ctx.dataset(),
@@ -47,7 +48,7 @@ pub(crate) async fn prepare_corpus(
if !config.reseed_slice {
let requested_cases = window.cases.len();
if can_reuse_namespace(
ctx.db(),
ctx.db()?,
&descriptor,
&ctx.namespace,
&ctx.database,
@@ -81,7 +82,7 @@ pub(crate) async fn prepare_corpus(
return machine
.prepare_corpus()
.map_err(|(_, guard)| map_guard_error("prepare_corpus", guard));
.map_err(|(_, guard)| map_guard_error("prepare_corpus", &guard));
}
info!(
cache = %base_dir.display(),
@@ -137,5 +138,5 @@ pub(crate) async fn prepare_corpus(
machine
.prepare_corpus()
.map_err(|(_, guard)| map_guard_error("prepare_corpus", guard))
.map_err(|(_, guard)| map_guard_error("prepare_corpus", &guard))
}
@@ -117,5 +117,5 @@ pub(crate) async fn prepare_db(
machine
.prepare_db()
.map_err(|(_, guard)| map_guard_error("prepare_db", guard))
.map_err(|(_, guard)| map_guard_error("prepare_db", &guard))
}
@@ -19,6 +19,7 @@ use super::super::{
};
use super::{map_guard_error, StageResult};
#[allow(clippy::too_many_lines)]
pub(crate) async fn prepare_namespace(
machine: EvaluationMachine<(), CorpusReady>,
ctx: &mut EvaluationContext<'_>,
@@ -39,9 +40,9 @@ pub(crate) async fn prepare_namespace(
.to_string();
let namespace = ctx.namespace.clone();
let database = ctx.database.clone();
let embedding_provider = ctx.embedding_provider().clone();
let embedding_provider = ctx.embedding_provider()?.clone();
let corpus_handle = ctx.corpus_handle();
let corpus_handle = ctx.corpus_handle()?;
let base_manifest = &corpus_handle.manifest;
let manifest_for_seed =
if ctx.window_offset == 0 && ctx.window_length >= base_manifest.questions.len() {
@@ -60,10 +61,10 @@ pub(crate) async fn prepare_namespace(
let mut namespace_reused = false;
if !config.reseed_slice {
namespace_reused = {
let slice = ctx.slice();
let slice = ctx.slice()?;
can_reuse_namespace(
ctx.db(),
ctx.descriptor(),
ctx.db()?,
ctx.descriptor()?,
&namespace,
&database,
dataset.metadata.id.as_str(),
@@ -78,19 +79,19 @@ pub(crate) async fn prepare_namespace(
let mut namespace_seed_ms = None;
if !namespace_reused {
ctx.must_reapply_settings = true;
if let Err(err) = reset_namespace(ctx.db(), &namespace, &database).await {
if let Err(err) = reset_namespace(ctx.db()?, &namespace, &database).await {
warn!(
error = %err,
namespace,
database = %database,
"Failed to reset namespace before reseeding; continuing with existing data"
);
} else if let Err(err) = ctx.db().apply_migrations().await {
} else if let Err(err) = ctx.db()?.apply_migrations().await {
warn!(error = %err, "Failed to reapply migrations after namespace reset");
}
{
let slice = ctx.slice();
let slice = ctx.slice()?;
info!(
slice = slice.manifest.slice_id.as_str(),
window_offset = ctx.window_offset,
@@ -113,10 +114,10 @@ pub(crate) async fn prepare_namespace(
"Seeding ingestion corpus into SurrealDB"
);
}
let indexes_disabled = remove_all_indexes(ctx.db()).await.is_ok();
let indexes_disabled = remove_all_indexes(ctx.db()?).await.is_ok();
let seed_start = Instant::now();
corpus::seed_manifest_into_db(ctx.db(), &manifest_for_seed)
corpus::seed_manifest_into_db(ctx.db()?, &manifest_for_seed)
.await
.context("seeding ingestion corpus from manifest")?;
namespace_seed_ms = Some(seed_start.elapsed().as_millis());
@@ -124,15 +125,15 @@ pub(crate) async fn prepare_namespace(
// Recreate indexes AFTER data is loaded (correct bulk loading pattern)
if indexes_disabled {
info!("Recreating indexes after seeding data");
recreate_indexes(ctx.db(), embedding_provider.dimension())
recreate_indexes(ctx.db()?, embedding_provider.dimension())
.await
.context("recreating indexes with correct dimension")?;
warm_hnsw_cache(ctx.db(), embedding_provider.dimension()).await?;
warm_hnsw_cache(ctx.db()?, embedding_provider.dimension()).await?;
}
{
let slice = ctx.slice();
let slice = ctx.slice()?;
record_namespace_state(
ctx.descriptor(),
ctx.descriptor()?,
dataset.metadata.id.as_str(),
slice.manifest.slice_id.as_str(),
expected_fingerprint.as_str(),
@@ -145,17 +146,17 @@ pub(crate) async fn prepare_namespace(
}
if ctx.must_reapply_settings {
let mut settings = SystemSettings::get_current(ctx.db())
let mut settings = SystemSettings::get_current(ctx.db()?)
.await
.context("reloading system settings after namespace reset")?;
settings =
enforce_system_settings(ctx.db(), settings, embedding_provider.dimension(), config)
enforce_system_settings(ctx.db()?, settings, embedding_provider.dimension(), config)
.await?;
ctx.settings = Some(settings);
ctx.must_reapply_settings = false;
}
let user = ensure_eval_user(ctx.db()).await?;
let user = ensure_eval_user(ctx.db()?).await?;
ctx.eval_user = Some(user);
let total_manifest_questions = manifest_for_seed.questions.len();
@@ -199,5 +200,5 @@ pub(crate) async fn prepare_namespace(
machine
.prepare_namespace()
.map_err(|(_, guard)| map_guard_error("prepare_namespace", guard))
.map_err(|(_, guard)| map_guard_error("prepare_namespace", &guard))
}
@@ -68,5 +68,5 @@ pub(crate) async fn prepare_slice(
machine
.prepare_slice()
.map_err(|(_, guard)| map_guard_error("prepare_slice", guard))
.map_err(|(_, guard)| map_guard_error("prepare_slice", &guard))
}
+17 -14
View File
@@ -1,6 +1,6 @@
use std::{collections::HashSet, sync::Arc, time::Instant};
use anyhow::Context;
use anyhow::{anyhow, Context};
use common::storage::types::StoredObject;
use futures::stream::{self, StreamExt};
use tracing::{debug, info};
@@ -21,6 +21,7 @@ use super::super::{
};
use super::{map_guard_error, StageResult};
#[allow(clippy::too_many_lines, clippy::arithmetic_side_effects)]
pub(crate) async fn run_queries(
machine: EvaluationMachine<(), NamespaceReady>,
ctx: &mut EvaluationContext<'_>,
@@ -37,7 +38,7 @@ pub(crate) async fn run_queries(
let slice_settings = ctx
.slice_settings
.as_ref()
.expect("slice settings missing during query stage");
.ok_or_else(|| anyhow!("slice settings missing during query stage"))?;
let total_cases = ctx.cases.len();
let cases_iter = std::mem::take(&mut ctx.cases).into_iter().enumerate();
@@ -115,9 +116,9 @@ pub(crate) async fn run_queries(
chunk_rrf_fts_weight = active_tuning.chunk_rrf_fts_weight,
chunk_rrf_use_vector = active_tuning.flags.chunk_rrf_use_vector.as_bool(),
chunk_rrf_use_fts = active_tuning.flags.chunk_rrf_use_fts.as_bool(),
embedding_backend = ctx.embedding_provider().backend_label(),
embedding_backend = ctx.embedding_provider()?.backend_label(),
embedding_model = ctx
.embedding_provider()
.embedding_provider()?
.model_code()
.as_deref()
.unwrap_or("<default>"),
@@ -125,11 +126,11 @@ pub(crate) async fn run_queries(
);
let retrieval_config = Arc::new(retrieval_config);
ctx.rerank_pool = rerank_pool.clone();
ctx.retrieval_config = Some(retrieval_config.clone());
ctx.rerank_pool.clone_from(&rerank_pool);
ctx.retrieval_config = Some(Arc::clone(&retrieval_config));
ctx.evaluation_start = Some(Instant::now());
let user_id = ctx.evaluation_user().id.clone();
let user_id = ctx.evaluation_user()?.id.clone();
let concurrency = config.concurrency.max(1);
let diagnostics_enabled = ctx.diagnostics_enabled;
@@ -141,20 +142,20 @@ pub(crate) async fn run_queries(
"Starting evaluation with staged query execution"
);
let embedding_provider_for_queries = ctx.embedding_provider().clone();
let embedding_provider_for_queries = ctx.embedding_provider()?.clone();
let rerank_pool_for_queries = rerank_pool.clone();
let db = ctx.db().clone();
let openai_client = ctx.openai_client();
let db = ctx.db()?.clone();
let openai_client = ctx.openai_client()?;
let raw_results = stream::iter(cases_iter)
.map(move |(idx, case)| {
let db = db.clone();
let openai_client = openai_client.clone();
let openai_client = Arc::clone(&openai_client);
let user_id = user_id.clone();
let retrieval_config = retrieval_config.clone();
let retrieval_config = Arc::clone(&retrieval_config);
let embedding_provider = embedding_provider_for_queries.clone();
let rerank_pool = rerank_pool_for_queries.clone();
let semaphore = query_semaphore.clone();
let semaphore = Arc::clone(&query_semaphore);
let diagnostics_enabled = diagnostics_enabled;
async move {
@@ -374,9 +375,10 @@ pub(crate) async fn run_queries(
machine
.run_queries()
.map_err(|(_, guard)| map_guard_error("run_queries", guard))
.map_err(|(_, guard)| map_guard_error("run_queries", &guard))
}
#[allow(clippy::arithmetic_side_effects, clippy::cast_precision_loss)]
fn calculate_reciprocal_rank(rank: Option<usize>) -> f64 {
match rank {
Some(r) if r > 0 => 1.0 / (r as f64),
@@ -384,6 +386,7 @@ fn calculate_reciprocal_rank(rank: Option<usize>) -> f64 {
}
}
#[allow(clippy::arithmetic_side_effects, clippy::cast_precision_loss)]
fn calculate_ndcg(retrieved: &[RetrievedSummary], k: usize) -> f64 {
let mut dcg = 0.0;
let mut relevant_count = 0;
+8 -7
View File
@@ -13,6 +13,7 @@ use super::super::{
};
use super::{map_guard_error, StageResult};
#[allow(clippy::too_many_lines, clippy::arithmetic_side_effects, clippy::cast_precision_loss)]
pub(crate) async fn summarize(
machine: EvaluationMachine<(), QueriesFinished>,
ctx: &mut EvaluationContext<'_>,
@@ -34,8 +35,8 @@ pub(crate) async fn summarize(
.unwrap_or_default();
let config = ctx.config();
let dataset = ctx.dataset();
let slice = ctx.slice();
let corpus_handle = ctx.corpus_handle();
let slice = ctx.slice()?;
let corpus_handle = ctx.corpus_handle()?;
let total_cases = summaries.len();
let mut correct = 0usize;
@@ -176,7 +177,7 @@ pub(crate) async fn summarize(
slice_total_paragraphs: slice.manifest.total_paragraphs,
slice_negative_multiplier: slice.manifest.negative_multiplier,
namespace_reused: ctx.namespace_reused,
corpus_paragraphs: ctx.corpus_handle().manifest.metadata.paragraph_count,
corpus_paragraphs: ctx.corpus_handle()?.manifest.metadata.paragraph_count,
ingestion_cache_path: corpus_handle.path.display().to_string(),
ingestion_reused: corpus_handle.reused_ingestion,
ingestion_embeddings_reused: corpus_handle.reused_embeddings,
@@ -189,9 +190,9 @@ pub(crate) async fn summarize(
negative_paragraphs_reused: corpus_handle.negative_reused,
latency_ms: latency_stats,
perf: perf_timings,
embedding_backend: ctx.embedding_provider().backend_label().to_string(),
embedding_model: ctx.embedding_provider().model_code(),
embedding_dimension: ctx.embedding_provider().dimension(),
embedding_backend: ctx.embedding_provider()?.backend_label().to_string(),
embedding_model: ctx.embedding_provider()?.model_code(),
embedding_dimension: ctx.embedding_provider()?.dimension(),
rerank_enabled: config.retrieval.rerank,
rerank_pool_size: ctx
.rerank_pool
@@ -228,5 +229,5 @@ pub(crate) async fn summarize(
machine
.summarize()
.map_err(|(_, guard)| map_guard_error("summarize", guard))
.map_err(|(_, guard)| map_guard_error("summarize", &guard))
}
+76 -141
View File
@@ -1,4 +1,5 @@
use std::{
fmt::Write,
fs,
path::{Path, PathBuf},
};
@@ -71,6 +72,7 @@ pub struct SliceSection {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct RetrievalSection {
pub k: usize,
pub cases: usize,
@@ -179,6 +181,7 @@ pub struct ReportOutcome {
}
impl EvaluationReport {
#[allow(clippy::too_many_lines)]
pub fn from_summary(summary: &EvaluationSummary, sample: usize) -> Self {
let overview = OverviewSection {
generated_at: format_timestamp(&summary.generated_at),
@@ -400,21 +403,17 @@ pub fn write_reports(
})
}
#[allow(clippy::too_many_lines, clippy::write_with_newline, clippy::unwrap_used)]
fn render_markdown(report: &EvaluationReport) -> String {
let mut md = String::new();
md.push_str(&format!(
"# Retrieval Evaluation (k={})\\n\\n",
report.retrieval.k
));
write!(md, "# Retrieval Evaluation (k={})\\n\\n", report.retrieval.k).unwrap();
md.push_str("## Overview\\n\\n");
md.push_str("| Metric | Value |\\n| --- | --- |\\n");
md.push_str(&format!(
"| Generated | {} |\\n",
report.overview.generated_at
));
md.push_str(&format!(
write!(md, "| Generated | {} |\\n", report.overview.generated_at).unwrap();
write!(
md,
"| Run Label | {} |\\n",
report
.overview
@@ -422,109 +421,55 @@ fn render_markdown(report: &EvaluationReport) -> String {
.as_deref()
.filter(|label| !label.is_empty())
.unwrap_or("-")
));
md.push_str(&format!(
"| Total Cases | {} |\\n",
report.overview.total_cases
));
md.push_str(&format!(
"| Filtered Questions | {} |\\n",
report.overview.filtered_questions
));
)
.unwrap();
write!(md, "| Total Cases | {} |\\n", report.overview.total_cases).unwrap();
write!(md, "| Filtered Questions | {} |\\n", report.overview.filtered_questions).unwrap();
md.push_str("\\n## Dataset & Slice\\n\\n");
md.push_str("| Metric | Value |\\n| --- | --- |\\n");
md.push_str(&format!(
"| Dataset | {} (`{}`) |\\n",
report.dataset.label, report.dataset.id
));
md.push_str(&format!(
"| Dataset Source | {} |\\n",
report.dataset.source
));
md.push_str(&format!(
"| Includes Unanswerable | {} |\\n",
bool_badge(report.dataset.includes_unanswerable)
));
md.push_str(&format!(
"| Require Verified Chunks | {} |\\n",
bool_badge(report.dataset.require_verified_chunks)
));
write!(md, "| Dataset | {} (`{}`) |\\n", report.dataset.label, report.dataset.id).unwrap();
write!(md, "| Dataset Source | {} |\\n", report.dataset.source).unwrap();
write!(md, "| Includes Unanswerable | {} |\\n", bool_badge(report.dataset.includes_unanswerable)).unwrap();
write!(md, "| Require Verified Chunks | {} |\\n", bool_badge(report.dataset.require_verified_chunks)).unwrap();
let embedding_label = if let Some(model) = report.dataset.embedding_model.as_ref() {
format!("{} ({model})", report.dataset.embedding_backend)
} else {
report.dataset.embedding_backend.clone()
};
md.push_str(&format!("| Embedding | {embedding_label} |\\n"));
md.push_str(&format!(
"| Embedding Dim | {} |\\n",
report.dataset.embedding_dimension
));
md.push_str(&format!("| Slice ID | `{}` |\\n", report.slice.id));
md.push_str(&format!("| Slice Seed | {} |\\n", report.slice.seed));
md.push_str(&format!(
"| Slice Window (offset/length) | {}/{} |\\n",
report.slice.window_offset, report.slice.window_length
));
md.push_str(&format!(
"| Slice Questions (window/ledger) | {}/{} |\\n",
report.slice.slice_cases, report.slice.ledger_total_cases
));
md.push_str(&format!(
"| Slice Positives / Negatives | {}/{} |\\n",
report.slice.positives, report.slice.negatives
));
md.push_str(&format!(
"| Slice Paragraphs | {} |\\n",
report.slice.total_paragraphs
));
md.push_str(&format!(
"| Negative Multiplier | {:.2} |\\n",
report.slice.negative_multiplier
));
write!(md, "| Embedding | {embedding_label} |\\n").unwrap();
write!(md, "| Embedding Dim | {} |\\n", report.dataset.embedding_dimension).unwrap();
write!(md, "| Slice ID | `{}` |\\n", report.slice.id).unwrap();
write!(md, "| Slice Seed | {} |\\n", report.slice.seed).unwrap();
write!(md, "| Slice Window (offset/length) | {}/{} |\\n", report.slice.window_offset, report.slice.window_length).unwrap();
write!(md, "| Slice Questions (window/ledger) | {}/{} |\\n", report.slice.slice_cases, report.slice.ledger_total_cases).unwrap();
write!(md, "| Slice Positives / Negatives | {}/{} |\\n", report.slice.positives, report.slice.negatives).unwrap();
write!(md, "| Slice Paragraphs | {} |\\n", report.slice.total_paragraphs).unwrap();
write!(md, "| Negative Multiplier | {:.2} |\\n", report.slice.negative_multiplier).unwrap();
md.push_str("\\n## Retrieval Metrics\\n\\n");
md.push_str("| Metric | Value |\\n| --- | --- |\\n");
md.push_str(&format!("| Cases | {} |\\n", report.retrieval.cases));
md.push_str(&format!(
"| Correct@{} | {}/{} |\\n",
report.retrieval.k, report.retrieval.correct, report.retrieval.cases
));
md.push_str(&format!(
"| Precision@{} | {:.3} |\\n",
report.retrieval.k, report.retrieval.precision
));
md.push_str(&format!(
write!(md, "| Cases | {} |\\n", report.retrieval.cases).unwrap();
write!(md, "| Correct@{} | {}/{} |\\n", report.retrieval.k, report.retrieval.correct, report.retrieval.cases).unwrap();
write!(md, "| Precision@{} | {:.3} |\\n", report.retrieval.k, report.retrieval.precision).unwrap();
write!(
md,
"| Precision@1/2/3 | {:.3} / {:.3} / {:.3} |\\n",
report.retrieval.precision_at_1,
report.retrieval.precision_at_2,
report.retrieval.precision_at_3
));
md.push_str(&format!("| MRR | {:.3} |\\n", report.retrieval.mrr));
md.push_str(&format!(
"| NDCG | {:.3} |\\n",
report.retrieval.average_ndcg
));
md.push_str(&format!(
"| Latency Avg / P50 / P95 (ms) | {:.1} / {} / {} |\\n",
report.retrieval.latency.avg, report.retrieval.latency.p50, report.retrieval.latency.p95
));
md.push_str(&format!(
"| Strategy | `{}` |\\n",
report.retrieval.strategy
));
md.push_str(&format!(
"| Concurrency | {} |\\n",
report.retrieval.concurrency
));
)
.unwrap();
write!(md, "| MRR | {:.3} |\\n", report.retrieval.mrr).unwrap();
write!(md, "| NDCG | {:.3} |\\n", report.retrieval.average_ndcg).unwrap();
write!(md, "| Latency Avg / P50 / P95 (ms) | {:.1} / {} / {} |\\n", report.retrieval.latency.avg, report.retrieval.latency.p50, report.retrieval.latency.p95).unwrap();
write!(md, "| Strategy | `{}` |\\n", report.retrieval.strategy).unwrap();
write!(md, "| Concurrency | {} |\\n", report.retrieval.concurrency).unwrap();
if report.retrieval.rerank_enabled {
let pool = report
.retrieval
.rerank_pool_size.map_or_else(|| "?".into(), |size| size.to_string());
md.push_str(&format!(
"| Rerank | enabled (pool {pool}, keep top {}) |\\n",
report.retrieval.rerank_keep_top
));
write!(md, "| Rerank | enabled (pool {pool}, keep top {}) |\\n", report.retrieval.rerank_keep_top).unwrap();
} else {
md.push_str("| Rerank | disabled |\\n");
}
@@ -532,58 +477,36 @@ fn render_markdown(report: &EvaluationReport) -> String {
if let Some(llm) = &report.llm {
md.push_str("\\n## LLM Mode Metrics\\n\\n");
md.push_str("| Metric | Value |\\n| --- | --- |\\n");
md.push_str(&format!("| Cases | {} |\\n", llm.cases));
md.push_str(&format!("| Answered | {} |\\n", llm.answered));
md.push_str(&format!("| Precision | {:.3} |\\n", llm.precision));
write!(md, "| Cases | {} |\\n", llm.cases).unwrap();
write!(md, "| Answered | {} |\\n", llm.answered).unwrap();
write!(md, "| Precision | {:.3} |\\n", llm.precision).unwrap();
}
md.push_str("\\n## Performance\\n\\n");
md.push_str("| Metric | Value |\\n| --- | --- |\\n");
md.push_str(&format!(
"| OpenAI Base URL | {} |\\n",
report.performance.openai_base_url
));
md.push_str(&format!(
"| Ingestion Duration | {} ms |\\n",
report.performance.ingestion_ms
));
write!(md, "| OpenAI Base URL | {} |\\n", report.performance.openai_base_url).unwrap();
write!(md, "| Ingestion Duration | {} ms |\\n", report.performance.ingestion_ms).unwrap();
if let Some(seed) = report.performance.namespace_seed_ms {
md.push_str(&format!("| Namespace Seed | {seed} ms |\\n"));
write!(md, "| Namespace Seed | {seed} ms |\\n").unwrap();
}
md.push_str(&format!(
write!(
md,
"| Namespace State | {} |\\n",
if report.performance.namespace_reused {
"reused"
} else {
"seeded"
}
));
md.push_str(&format!(
"| Corpus Paragraphs | {} |\\n",
report.performance.corpus_paragraphs
));
)
.unwrap();
write!(md, "| Corpus Paragraphs | {} |\\n", report.performance.corpus_paragraphs).unwrap();
if report.detailed_report {
md.push_str(&format!(
"| Ingestion Cache | `{}` |\\n",
report.performance.ingestion_cache_path
));
md.push_str(&format!(
"| Ingestion Reused | {} |\\n",
bool_badge(report.performance.ingestion_reused)
));
md.push_str(&format!(
"| Embeddings Reused | {} |\\n",
bool_badge(report.performance.embeddings_reused)
));
write!(md, "| Ingestion Cache | `{}` |\\n", report.performance.ingestion_cache_path).unwrap();
write!(md, "| Ingestion Reused | {} |\\n", bool_badge(report.performance.ingestion_reused)).unwrap();
write!(md, "| Embeddings Reused | {} |\\n", bool_badge(report.performance.embeddings_reused)).unwrap();
}
md.push_str(&format!(
"| Positives Cached | {} |\\n",
report.performance.positive_paragraphs_reused
));
md.push_str(&format!(
"| Negatives Cached | {} |\\n",
report.performance.negative_paragraphs_reused
));
write!(md, "| Positives Cached | {} |\\n", report.performance.positive_paragraphs_reused).unwrap();
write!(md, "| Negatives Cached | {} |\\n", report.performance.negative_paragraphs_reused).unwrap();
md.push_str("\\n## Retrieval Stage Timings\\n\\n");
md.push_str("| Stage | Avg (ms) | P50 (ms) | P95 (ms) |\\n| --- | --- | --- | --- |\\n");
@@ -635,7 +558,8 @@ fn render_markdown(report: &EvaluationReport) -> String {
for case in &report.misses {
let retrieved = render_retrieved(&case.retrieved);
if report.detailed_report {
md.push_str(&format!(
write!(
md,
"| `{}` | {} | `{}` | {} | {} | {} | {} |\\n",
case.question_id,
case.paragraph_title,
@@ -644,12 +568,15 @@ fn render_markdown(report: &EvaluationReport) -> String {
bool_badge(case.chunk_text_match),
bool_badge(case.chunk_id_match),
retrieved
));
)
.unwrap();
} else {
md.push_str(&format!(
write!(
md,
"| `{}` | {} | `{}` | {} |\\n",
case.question_id, case.paragraph_title, case.expected_source, retrieved
));
)
.unwrap();
}
}
}
@@ -671,24 +598,29 @@ fn render_markdown(report: &EvaluationReport) -> String {
let retrieved = render_retrieved(&case.retrieved);
let rank = case
.match_rank.map_or_else(|| "-".into(), |rank| rank.to_string());
md.push_str(&format!(
write!(
md,
"| `{}` | {} | {} | {} |\\n",
case.question_id,
bool_badge(case.answered),
rank,
retrieved
));
)
.unwrap();
}
}
}
md
}
#[allow(clippy::write_with_newline, clippy::unwrap_used)]
fn write_stage_row(buf: &mut String, label: &str, stats: &LatencyStats) {
buf.push_str(&format!(
"| {} | {:.1} | {} | {} |\n",
writeln!(
buf,
"| {} | {:.1} | {} | {} |",
label, stats.avg, stats.p50, stats.p95
));
)
.unwrap();
}
fn bool_badge(value: bool) -> &'static str {
@@ -819,6 +751,7 @@ fn default_stage_latency() -> StageLatencyBreakdown {
}
}
#[allow(clippy::too_many_lines)]
fn convert_legacy_entry(entry: LegacyHistoryEntry) -> EvaluationReport {
let overview = OverviewSection {
generated_at: entry.generated_at,
@@ -987,6 +920,7 @@ mod tests {
use chrono::Utc;
use tempfile::tempdir;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn latency(ms: f64) -> LatencyStats {
LatencyStats {
avg: ms,
@@ -1087,7 +1021,7 @@ mod tests {
retrieval_precision: 1.0,
average_ndcg: 0.0,
mrr: 0.0,
llm_cases: if include_llm { 1 } else { 0 },
llm_cases: usize::from(include_llm),
llm_answered: 0,
llm_precision: 0.0,
slice_id: "slice".into(),
@@ -1161,6 +1095,7 @@ mod tests {
assert!(!md.contains("LLM-Only Cases"));
}
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
#[test]
fn evaluations_history_captures_strategy_and_concurrency() {
let tmp = tempdir().unwrap();
+1
View File
@@ -10,6 +10,7 @@ use tracing::info;
use crate::args::Config;
/// Enforce evaluation-specific system settings overrides.
#[allow(clippy::cast_possible_truncation)]
pub(crate) async fn enforce_system_settings(
db: &SurrealDbClient,
mut settings: SystemSettings,
+31 -22
View File
@@ -1,5 +1,6 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Write,
fs,
path::{Path, PathBuf},
};
@@ -98,10 +99,10 @@ fn sanitize_identifier(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let digest = hasher.finalize();
digest[..6]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
digest.iter().take(6).fold(String::with_capacity(12), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
} else {
trimmed
}
@@ -168,7 +169,7 @@ impl DatasetIndex {
.paragraph_by_id
.get(id)
.ok_or_else(|| anyhow!("slice references unknown paragraph '{id}'"))?;
Ok(&dataset.paragraphs[*idx])
dataset.paragraphs.get(*idx).ok_or_else(|| anyhow!("paragraph index out of bounds"))
}
fn question<'a>(
@@ -180,7 +181,8 @@ impl DatasetIndex {
.question_by_id
.get(question_id)
.ok_or_else(|| anyhow!("slice references unknown question '{question_id}'"))?;
let paragraph = &dataset.paragraphs[*p_idx];
let paragraph = dataset.paragraphs.get(*p_idx)
.ok_or_else(|| anyhow!("paragraph index out of bounds for question '{question_id}'"))?;
let question = paragraph
.questions
.get(*q_idx)
@@ -205,6 +207,7 @@ struct BuildParams {
rng_seed: u64,
}
#[allow(clippy::too_many_lines)]
pub fn resolve_slice<'a>(
dataset: &'a ConvertedDataset,
config: &SliceConfig<'_>,
@@ -237,7 +240,7 @@ pub fn resolve_slice<'a>(
requested_corpus,
seed: config.slice_seed,
};
let slice_id = compute_slice_id(&key);
let slice_id = compute_slice_id(&key)?;
let base = config
.cache_dir
.join("slices")
@@ -360,7 +363,7 @@ pub fn resolve_slice<'a>(
&params,
desired_negatives,
requested_corpus,
)?;
);
refresh_manifest_stats(&mut manifest);
if changed {
@@ -390,6 +393,7 @@ pub fn resolve_slice<'a>(
Ok(resolved)
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
pub fn select_window<'a>(
resolved: &'a ResolvedSlice<'a>,
offset: usize,
@@ -404,9 +408,7 @@ pub fn select_window<'a>(
}
if offset >= total {
return Err(anyhow!(
"slice offset {} exceeds available cases ({})",
offset,
total
"slice offset {offset} exceeds available cases ({total})",
));
}
let available = total - offset;
@@ -516,6 +518,7 @@ fn empty_manifest(
}
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
fn ensure_case_capacity(
dataset: &ConvertedDataset,
manifest: &mut SliceManifest,
@@ -631,6 +634,7 @@ fn ordered_question_refs(
Ok(question_refs)
}
#[allow(clippy::too_many_lines, clippy::arithmetic_side_effects)]
fn ordered_question_refs_beir(
dataset: &ConvertedDataset,
params: &BuildParams,
@@ -795,20 +799,21 @@ fn question_prefix(question_id: &str) -> Option<&'static str> {
None
}
#[allow(clippy::indexing_slicing)]
fn ensure_negative_pool(
dataset: &ConvertedDataset,
manifest: &mut SliceManifest,
params: &BuildParams,
target_negatives: usize,
requested_corpus: usize,
) -> Result<bool> {
) -> bool {
let current_negatives = manifest
.paragraphs
.iter()
.filter(|entry| matches!(entry.kind, SliceParagraphKind::Negative))
.count();
if current_negatives >= target_negatives {
return Ok(false);
return false;
}
let positive_ids: HashSet<String> = manifest
@@ -816,7 +821,7 @@ fn ensure_negative_pool(
.iter()
.filter_map(|entry| match entry.kind {
SliceParagraphKind::Positive { .. } => Some(entry.id.clone()),
_ => None,
SliceParagraphKind::Negative => None,
})
.collect();
let mut negative_ids: HashSet<String> = manifest
@@ -824,7 +829,7 @@ fn ensure_negative_pool(
.iter()
.filter_map(|entry| match entry.kind {
SliceParagraphKind::Negative => Some(entry.id.clone()),
_ => None,
SliceParagraphKind::Positive { .. } => None,
})
.collect();
@@ -863,7 +868,7 @@ fn ensure_negative_pool(
);
}
Ok(added)
added
}
fn ordered_negative_indices(
@@ -914,6 +919,7 @@ fn ensure_shard_paths(manifest: &mut SliceManifest) -> bool {
changed
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn desired_negative_target(
positive_count: usize,
requested_corpus: usize,
@@ -996,17 +1002,18 @@ fn manifest_to_resolved<'a>(
})
}
fn compute_slice_id(key: &SliceKey<'_>) -> String {
let payload = serde_json::to_vec(key).expect("SliceKey serialisation should not fail");
fn compute_slice_id(key: &SliceKey<'_>) -> Result<String> {
let payload = serde_json::to_vec(key).context("SliceKey serialisation failed")?;
let mut hasher = Sha256::new();
hasher.update(payload);
let digest = hasher.finalize();
digest[..16]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
Ok(digest.iter().take(16).fold(String::with_capacity(32), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
}))
}
#[allow(clippy::indexing_slicing)]
fn mix_seed(dataset_id: &str, seed: u64) -> u64 {
let mut hasher = Sha256::new();
hasher.update(dataset_id.as_bytes());
@@ -1146,6 +1153,7 @@ mod tests {
}
#[test]
#[allow(clippy::indexing_slicing)]
fn select_window_yields_expected_cases() -> Result<()> {
let dataset = sample_dataset();
let temp = tempdir().context("creating temp directory")?;
@@ -1177,6 +1185,7 @@ mod tests {
}
#[test]
#[allow(clippy::indexing_slicing)]
fn beir_mix_balances_and_rebalances() -> Result<()> {
let mut paragraphs = Vec::new();
let counts = [
+2
View File
@@ -121,6 +121,7 @@ impl Descriptor {
}
}
#[allow(clippy::expect_used)]
fn compute_hash(metadata: &SnapshotMetadata) -> String {
let mut hasher = Sha256::new();
hasher.update(
@@ -134,6 +135,7 @@ mod tests {
use super::*;
#[tokio::test]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn state_round_trip() {
let temp_dir = tempfile::tempdir().unwrap();
let metadata = SnapshotMetadata {
+12
View File
@@ -8,6 +8,7 @@ use retrieval_pipeline::{
use serde::{Deserialize, Serialize};
use unicode_normalization::UnicodeNormalization;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
pub struct EvaluationSummary {
pub generated_at: DateTime<Utc>,
@@ -86,6 +87,7 @@ pub struct EvaluationSummary {
pub cases: Vec<CaseSummary>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
pub struct CaseSummary {
pub question_id: String,
@@ -137,6 +139,7 @@ pub struct StageLatencyBreakdown {
pub assemble: LatencyStats,
}
#[allow(clippy::struct_field_names)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct EvaluationStageTimings {
pub prepare_slice_ms: u128,
@@ -345,6 +348,7 @@ fn chunk_snippet(text: &str) -> String {
acc
}
#[allow(clippy::cast_precision_loss)]
pub fn compute_latency_stats(latencies: &[u128]) -> LatencyStats {
if latencies.is_empty() {
return LatencyStats {
@@ -386,6 +390,13 @@ pub fn build_stage_latency_breakdown(samples: &[PipelineStageTimings]) -> StageL
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
fn percentile(sorted: &[u128], fraction: f64) -> u128 {
if sorted.is_empty() {
return 0;
@@ -395,6 +406,7 @@ fn percentile(sorted: &[u128], fraction: f64) -> u128 {
sorted[idx.min(sorted.len() - 1)]
}
#[allow(clippy::arithmetic_side_effects)]
pub fn build_case_diagnostics(
summary: &CaseSummary,
expected_chunk_ids: &[String],
-91
View File
@@ -44,7 +44,6 @@
--leading-snug: 1.375;
--leading-relaxed: 1.625;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -285,37 +284,6 @@
}
}
}
.drawer-open {
> .drawer-side {
overflow-y: auto;
}
> .drawer-toggle {
display: none;
& ~ .drawer-side {
pointer-events: auto;
visibility: visible;
position: sticky;
display: block;
width: auto;
overscroll-behavior: auto;
opacity: 100%;
& > .drawer-overlay {
cursor: default;
background-color: transparent;
}
& > *:not(.drawer-overlay) {
translate: 0%;
[dir="rtl"] & {
translate: 0%;
}
}
}
&:checked ~ .drawer-side {
pointer-events: auto;
visibility: visible;
}
}
}
.drawer-toggle {
position: fixed;
height: calc(0.25rem * 0);
@@ -1074,22 +1042,6 @@
grid-row-start: 1;
min-width: calc(0.25rem * 0);
}
.chat-image {
grid-row: span 2 / span 2;
align-self: flex-end;
}
.chat-footer {
grid-row-start: 3;
display: flex;
gap: calc(0.25rem * 1);
font-size: 0.6875rem;
}
.chat-header {
grid-row-start: 1;
display: flex;
gap: calc(0.25rem * 1);
font-size: 0.6875rem;
}
.container {
width: 100%;
@media (width >= 40rem) {
@@ -1796,9 +1748,6 @@
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-11 {
width: calc(var(--spacing) * 11);
}
.w-11\/12 {
width: calc(11/12 * 100%);
}
@@ -1862,9 +1811,6 @@
.flex-none {
flex: none;
}
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
@@ -1877,13 +1823,6 @@
.grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1956,9 +1895,6 @@
.justify-start {
justify-content: flex-start;
}
.gap-0 {
gap: calc(var(--spacing) * 0);
}
.gap-0\.5 {
gap: calc(var(--spacing) * 0.5);
}
@@ -2115,9 +2051,6 @@
.bg-transparent {
background-color: transparent;
}
.bg-warning {
background-color: var(--color-warning);
}
.bg-warning\/10 {
background-color: var(--color-warning);
@supports (color: color-mix(in lab, red, red)) {
@@ -2136,9 +2069,6 @@
.loading-spinner {
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
}
.mask-repeat {
mask-repeat: repeat;
}
.fill-current {
fill: currentcolor;
}
@@ -2169,9 +2099,6 @@
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-1 {
padding-inline: calc(var(--spacing) * 1);
}
.px-1\.5 {
padding-inline: calc(var(--spacing) * 1.5);
}
@@ -2326,9 +2253,6 @@
--tw-tracking: var(--tracking-widest);
letter-spacing: var(--tracking-widest);
}
.text-wrap {
text-wrap: wrap;
}
.break-words {
overflow-wrap: break-word;
}
@@ -2395,17 +2319,6 @@
.italic {
font-style: italic;
}
.underline {
text-decoration-line: underline;
}
.swap-active {
.swap-off {
opacity: 0%;
}
.swap-on {
opacity: 100%;
}
}
.opacity-0 {
opacity: 0%;
}
@@ -2496,10 +2409,6 @@
--tw-duration: 300ms;
transition-duration: 300ms;
}
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
@@ -10,7 +10,7 @@ use axum::{
use axum_htmx::{HxRequest, HX_TRIGGER};
use common::{
error::AppError,
utils::template_engine::{ProvidesTemplateEngine, TemplateEngine, Value},
utils::template_engine::{ProvidesTemplateEngine, Value},
};
use minijinja::context;
use serde::Serialize;
@@ -333,7 +333,7 @@ async fn prepare_chat_request(
history: &[Message],
) -> Result<
(async_openai::types::CreateChatCompletionRequest, Vec<String>),
Sse<Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>>>,
SseResponse,
> {
let rerank_lease = match state.reranker_pool.as_ref() {
Some(pool) => pool.checkout().await,
@@ -356,7 +356,7 @@ async fn prepare_chat_request(
{
Ok(result) => result,
Err(_e) => {
return Err(Sse::new(create_error_stream("Failed to retrieve knowledge")));
return Err(sse_with_keep_alive(create_error_stream("Failed to retrieve knowledge")));
}
};
@@ -365,7 +365,7 @@ async fn prepare_chat_request(
let context_json = match retrieval_result {
retrieval_pipeline::StrategyOutput::Chunks(chunks) => chunks_to_chat_context(&chunks),
retrieval_pipeline::StrategyOutput::Entities(entities) => {
retrieved_entities_to_json(entities)
retrieved_entities_to_json(&entities)
}
retrieval_pipeline::StrategyOutput::Search(search_result) => {
chunks_to_chat_context(&search_result.chunks)
@@ -374,10 +374,10 @@ async fn prepare_chat_request(
let formatted_user_message =
create_user_message_with_history(&context_json, history, &user_message.content);
let Ok(settings) = SystemSettings::get_current(&state.db).await else {
return Err(Sse::new(create_error_stream("Failed to retrieve system settings")));
return Err(sse_with_keep_alive(create_error_stream("Failed to retrieve system settings")));
};
let Ok(request) = create_chat_request(formatted_user_message, &settings) else {
return Err(Sse::new(create_error_stream("Failed to create chat request")));
return Err(sse_with_keep_alive(create_error_stream("Failed to create chat request")));
};
Ok((request, allowed_reference_ids))
@@ -415,7 +415,7 @@ fn spawn_storage_task(
Err(err) => {
error!(error = %err, "Reference validation failed, storing answer without references");
let ai_message = Message::new(
user_message.conversation_id,
conversation_id.clone(),
MessageRole::AI,
answer,
Some(Vec::new()),
+20 -8
View File
@@ -161,6 +161,7 @@ mod tests {
Router,
};
use common::storage::{
db::SurrealDbClient,
store::StorageManager,
types::{system_settings::SystemSettings, user::User},
};
@@ -195,9 +196,17 @@ mod tests {
.expect("failed to create temp data directory");
let config = smoke_test_config(namespace, &database, &data_dir);
let services = crate::bootstrap::init_with_config(config.clone()).await?;
let services = crate::bootstrap::init_with_config(config.clone())
.await
.expect("failed to init services");
let session_store = Arc::new(services.db.create_session_store().await?);
let session_store = Arc::new(
services
.db
.create_session_store()
.await
.expect("failed to create session store"),
);
let html_state = HtmlState::new_with_resources(StateResources {
db: Arc::clone(&services.db),
@@ -224,7 +233,7 @@ mod tests {
html_state,
});
(app, db, data_dir)
(app, services.db, data_dir)
}
fn assert_redirect_to(response: &Response, expected_location: &str) {
@@ -289,22 +298,25 @@ mod tests {
.oneshot(
Request::builder()
.uri("/api/v1/live")
.body(Body::empty())?,
.body(Body::empty())
.expect("building live request"),
)
.await?;
.await
.expect("sending live request");
assert_eq!(response.status(), StatusCode::OK);
let ready_response = app
.oneshot(
Request::builder()
.uri("/api/v1/ready")
.body(Body::empty())?,
.body(Body::empty())
.expect("building ready request"),
)
.await?;
.await
.expect("sending ready request");
assert_eq!(ready_response.status(), StatusCode::OK);
tokio::fs::remove_dir_all(&data_dir).await.ok();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]