Skip to main content

rover/summarizer/
backend.rs

1//! Backend trait and compaction options.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::summarizer::error::BackendError;
7
8/// Compaction modes (PRD §7.5).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CompactMode {
12    Extractive,
13    Abstractive,
14    Headlines,
15}
16
17impl CompactMode {
18    /// Stable string for params_hash and config parsing.
19    pub fn as_str(self) -> &'static str {
20        match self {
21            CompactMode::Extractive => "extractive",
22            CompactMode::Abstractive => "abstractive",
23            CompactMode::Headlines => "headlines",
24        }
25    }
26}
27
28/// Compaction style.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum Style {
32    Bullet,
33    Prose,
34    Executive,
35}
36
37impl Style {
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Style::Bullet => "bullet",
41            Style::Prose => "prose",
42            Style::Executive => "executive",
43        }
44    }
45}
46
47/// Section kinds the summarizer is asked to preserve verbatim.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum PreserveSection {
51    Code,
52    Tables,
53    Quotes,
54    Lists,
55}
56
57impl PreserveSection {
58    pub fn as_str(self) -> &'static str {
59        match self {
60            PreserveSection::Code => "code",
61            PreserveSection::Tables => "tables",
62            PreserveSection::Quotes => "quotes",
63            PreserveSection::Lists => "lists",
64        }
65    }
66}
67
68/// One summarization request after defaults have been merged.
69///
70/// `target_tokens` counts via the configured tokenizer family on the
71/// service side; backends treat it as advisory text in the prompt
72/// (Abstractive) or as a hard greedy cap (Extractive/Headlines).
73///
74/// Note: the derived `PartialEq`/`Eq` is order-sensitive on `preserve`
75/// (it's a `Vec`), but the `params_hash` cache key is order-invariant —
76/// it sorts and dedups `preserve` before hashing. Two `CompactOpts` that
77/// compare non-equal under `==` may still produce the same cache key.
78/// Keep this divergence in mind if you ever switch the cache key to
79/// derive from the struct directly.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct CompactOpts {
82    pub mode: CompactMode,
83    pub style: Style,
84    pub target_tokens: Option<usize>,
85    pub focus: Option<String>,
86    pub preserve: Vec<PreserveSection>,
87    /// The resolved backend's config-key name (e.g. "default", "fast").
88    /// Filled in by `SummarizerService::compact` from the registry; backends
89    /// see it for logging only.
90    pub backend_name: String,
91}
92
93#[async_trait]
94pub trait SummarizerBackend: Send + Sync {
95    async fn compact(&self, content: &str, opts: &CompactOpts) -> Result<String, BackendError>;
96
97    /// Config-key name (e.g. "default", "fast").
98    fn name(&self) -> &str;
99
100    /// Resolved model identifier for `params_hash`. The default `""` is
101    /// intended only for the extractive backend (which has no model). All
102    /// cloud / network-backed backends MUST override this with their
103    /// resolved model id so the cache key partitions across model versions.
104    fn model_id(&self) -> &str {
105        ""
106    }
107
108    /// Whether this backend feeds `content` into a model prompt (and is thus an
109    /// injection target needing HIGH internal hardening). Prompt-free backends
110    /// (e.g. extractive TextRank) return `false`.
111    fn uses_model_prompt(&self) -> bool {
112        false
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn enums_round_trip_through_as_str() {
122        assert_eq!(CompactMode::Extractive.as_str(), "extractive");
123        assert_eq!(CompactMode::Abstractive.as_str(), "abstractive");
124        assert_eq!(CompactMode::Headlines.as_str(), "headlines");
125        assert_eq!(Style::Bullet.as_str(), "bullet");
126        assert_eq!(Style::Prose.as_str(), "prose");
127        assert_eq!(Style::Executive.as_str(), "executive");
128        assert_eq!(PreserveSection::Code.as_str(), "code");
129    }
130
131    #[test]
132    fn compact_opts_is_clonable() {
133        let o = CompactOpts {
134            mode: CompactMode::Abstractive,
135            style: Style::Prose,
136            target_tokens: Some(500),
137            focus: Some("api shape".to_string()),
138            preserve: vec![PreserveSection::Code],
139            backend_name: "fast".to_string(),
140        };
141        let cloned = o.clone();
142        assert_eq!(o, cloned);
143    }
144}