Skip to main content

unparser_shared/ai/
refine.rs

1//! AI-assisted markdown refine — a generative rewrite pass, distinct from the
2//! crate's rule-based [`crate::refine`] (feature `refine`).
3//!
4//! Unlike that module, this one does **not** guarantee losslessness or idempotence —
5//! a generative rewrite cannot honor either invariant by construction. It is a
6//! separate opt-in stage meant to run *after* rule-based refine in a caller's
7//! pipeline (deterministic corrections first, so there is less for the model to
8//! second-guess), never a replacement for it.
9
10use super::config::AiConfig;
11use super::error::Error;
12use super::http::{self, ChatMessage};
13
14/// Options for [`refine_markdown`].
15#[non_exhaustive]
16#[derive(Debug, Clone, Default)]
17pub struct RefineAiOptions {
18    /// Extra guidance appended to the system prompt (e.g. domain-specific
19    /// terminology to preserve). Empty by default.
20    pub instructions: Option<String>,
21}
22
23impl RefineAiOptions {
24    /// Sets [`Self::instructions`] — `#[non_exhaustive]` blocks struct-literal
25    /// construction from outside this crate, so this is how a caller sets it.
26    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
27        self.instructions = Some(instructions.into());
28        self
29    }
30}
31
32const SYSTEM_PROMPT: &str = "You clean up Markdown produced by an automated document \
33converter. Fix awkward phrasing, spacing, and structure that a human editor would \
34fix, without changing the document's meaning, adding information, or removing any \
35content. Respond with the revised Markdown only — no commentary, no code fence \
36around the whole response.";
37
38/// Rewrites `markdown` with a generative pass, per `options`.
39///
40/// Returns the model's response verbatim (trimmed of leading/trailing whitespace).
41/// Callers that need a lossless/idempotent guarantee should run
42/// [`crate::refine::refine`] (feature `refine`) instead, or in addition, before this.
43pub fn refine_markdown(
44    cfg: &AiConfig,
45    markdown: &str,
46    options: &RefineAiOptions,
47) -> Result<String, Error> {
48    let system = match &options.instructions {
49        Some(extra) => format!("{SYSTEM_PROMPT}\n\n{extra}"),
50        None => SYSTEM_PROMPT.to_string(),
51    };
52    let messages = vec![
53        ChatMessage::text("system", system),
54        ChatMessage::text("user", markdown),
55    ];
56    let content = http::call(cfg, messages, false)?;
57    Ok(content.trim().to_string())
58}