Skip to main content

rskit_ai/prompt/
template.rs

1//! Prompt template types and validation.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use semver::Version;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use super::render::{placeholders, render};
10
11/// Errors returned by prompt rendering, construction, and registry lookup.
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum PromptError {
15    /// A named variable required by the template was absent from the render context.
16    #[error("missing prompt variable {0:?}")]
17    MissingVariable(String),
18    /// A builder was missing a required field.
19    #[error("missing required prompt field {0}")]
20    MissingField(&'static str),
21    /// A prompt version string was not valid semver.
22    #[error("invalid prompt version {version:?}: {source}")]
23    InvalidVersion {
24        /// Supplied version.
25        version: String,
26        /// Parse failure.
27        #[source]
28        source: semver::Error,
29    },
30    /// A registry entry already exists.
31    #[error("prompt already registered: {name}@{version}")]
32    AlreadyRegistered {
33        /// Prompt name.
34        name: String,
35        /// Prompt version.
36        version: Version,
37    },
38    /// A registry entry was not found.
39    #[error("prompt not found: {name}@{version}")]
40    NotFound {
41        /// Prompt name.
42        name: String,
43        /// Prompt version.
44        version: Version,
45    },
46    /// No versions exist for a prompt name.
47    #[error("prompt not found: {0}")]
48    NameNotFound(String),
49}
50
51/// Render context for a prompt template.
52pub type RenderContext = BTreeMap<String, serde_json::Value>;
53
54/// Declared prompt variable type.
55#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57#[non_exhaustive]
58pub enum VariableType {
59    /// String value.
60    String,
61    /// Number value.
62    Number,
63    /// Boolean value.
64    Boolean,
65    /// Object value.
66    Object,
67    /// Array value.
68    Array,
69    /// Any JSON value.
70    #[default]
71    Any,
72}
73
74/// Typed declaration for a prompt variable.
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct VariableDecl {
77    /// Variable name used in `{{name}}` placeholders.
78    pub name: String,
79    /// Expected variable type.
80    #[serde(default, rename = "type")]
81    pub kind: VariableType,
82    /// Whether callers must supply the variable.
83    #[serde(default = "default_required")]
84    pub required: bool,
85    /// Optional default value used when rendering.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub default: Option<serde_json::Value>,
88}
89
90const fn default_required() -> bool {
91    true
92}
93
94/// Prompt template with semver identity and optional output schema.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct PromptTemplate {
97    /// Stable prompt name.
98    pub name: String,
99    /// Semver prompt version.
100    pub version: Version,
101    /// Template body. Variables use `{{name}}` placeholders.
102    pub template: String,
103    /// Declared input variables.
104    #[serde(default, skip_serializing_if = "Vec::is_empty")]
105    pub variables: Vec<VariableDecl>,
106    /// Optional output contract.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub output_schema: Option<serde_json::Value>,
109    /// Human-readable description.
110    #[serde(default, skip_serializing_if = "String::is_empty")]
111    pub description: String,
112}
113
114impl PromptTemplate {
115    /// Render the template using the supplied context.
116    pub fn render(&self, context: &RenderContext) -> Result<String, PromptError> {
117        let mut values = context.clone();
118        for variable in &self.variables {
119            if !values.contains_key(&variable.name) {
120                if let Some(default) = &variable.default {
121                    values.insert(variable.name.clone(), default.clone());
122                } else if variable.required {
123                    return Err(PromptError::MissingVariable(variable.name.clone()));
124                }
125            }
126        }
127        render(&self.template, &values)
128    }
129}
130
131/// Backwards-compatible template name for the canonical prompt template shape.
132pub type Template = PromptTemplate;
133
134/// Validation finding kind.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum ValidationFindingKind {
138    /// Placeholder has no declaration.
139    MissingVariable,
140    /// Declaration is not used by the template.
141    UnusedVariable,
142}
143
144/// Prompt template validation finding.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct ValidationFinding {
147    /// Finding kind.
148    pub kind: ValidationFindingKind,
149    /// Variable name.
150    pub variable: String,
151}
152
153/// Validate template placeholders against declarations.
154#[must_use]
155pub fn validate(template: &PromptTemplate) -> Vec<ValidationFinding> {
156    let used = placeholders(&template.template);
157    let declared = template
158        .variables
159        .iter()
160        .map(|variable| variable.name.clone())
161        .collect::<BTreeSet<_>>();
162    let mut findings = Vec::new();
163    for variable in used.difference(&declared) {
164        findings.push(ValidationFinding {
165            kind: ValidationFindingKind::MissingVariable,
166            variable: variable.clone(),
167        });
168    }
169    for variable in declared.difference(&used) {
170        findings.push(ValidationFinding {
171            kind: ValidationFindingKind::UnusedVariable,
172            variable: variable.clone(),
173        });
174    }
175    findings
176}