cupel 1.2.0

Context window management pipeline for LLM applications
use std::collections::HashMap;

use crate::CupelError;
use crate::model::{ContextItem, ContextKind};
use crate::scorer::Scorer;

/// Absolute scorer: assigns a score based on the item's kind using a weight map.
///
/// Weight lookup is case-insensitive (via ContextKind's Hash/Eq).
///
/// # Examples
///
/// ```
/// use cupel::{ContextItemBuilder, ContextKind, KindScorer, Scorer};
///
/// let scorer = KindScorer::with_default_weights();
///
/// let item = ContextItemBuilder::new("system instruction", 10)
///     .kind(ContextKind::new("SystemPrompt")?)
///     .build()?;
///
/// let score = scorer.score(&item, &[item.clone()]);
/// assert_eq!(score, 1.0); // SystemPrompt has default weight 1.0
/// # Ok::<(), cupel::CupelError>(())
/// ```
#[derive(Debug, Clone)]
pub struct KindScorer {
    weights: HashMap<ContextKind, f64>,
}

impl KindScorer {
    /// Creates a KindScorer with the default weight map.
    ///
    /// Default weights: SystemPrompt=1.0, Memory=0.8, ToolOutput=0.6, Document=0.4, Message=0.2
    pub fn with_default_weights() -> Self {
        let mut weights = HashMap::new();
        weights.insert(ContextKind::from_static(ContextKind::SYSTEM_PROMPT), 1.0);
        weights.insert(ContextKind::from_static(ContextKind::MEMORY), 0.8);
        weights.insert(ContextKind::from_static(ContextKind::TOOL_OUTPUT), 0.6);
        weights.insert(ContextKind::from_static(ContextKind::DOCUMENT), 0.4);
        weights.insert(ContextKind::from_static(ContextKind::MESSAGE), 0.2);
        Self { weights }
    }

    /// Creates a KindScorer with custom weights.
    ///
    /// Validates that all weights are non-negative and finite.
    pub fn new(weights: HashMap<ContextKind, f64>) -> Result<Self, CupelError> {
        for (kind, &weight) in &weights {
            if weight < 0.0 {
                return Err(CupelError::ScorerConfig(format!(
                    "weight for kind '{}' must be non-negative",
                    kind,
                )));
            }
            if !weight.is_finite() {
                return Err(CupelError::ScorerConfig(format!(
                    "weight for kind '{}' must be finite",
                    kind,
                )));
            }
        }
        Ok(Self { weights })
    }
}

impl Scorer for KindScorer {
    fn score(&self, item: &ContextItem, _all_items: &[ContextItem]) -> f64 {
        self.weights.get(item.kind()).copied().unwrap_or(0.0)
    }
}