Skip to main content

entrenar/monitor/llm/
prompt.rs

1//! Prompt versioning with content-addressable IDs.
2
3use crate::monitor::llm::error::{LLMError, Result};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::collections::HashMap;
8
9/// Prompt identifier (content-addressable)
10pub type PromptId = String;
11
12/// Prompt version with content-addressable ID
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PromptVersion {
15    /// Content-addressable ID (SHA-256 of template)
16    pub id: PromptId,
17    /// Prompt template (with {variable} placeholders)
18    pub template: String,
19    /// Variable names in the template
20    pub variables: Vec<String>,
21    /// Version number (monotonically increasing per template family)
22    pub version: u32,
23    /// Creation timestamp
24    pub created_at: DateTime<Utc>,
25    /// SHA-256 hash of the template
26    pub sha256: String,
27    /// Optional description
28    pub description: Option<String>,
29    /// Optional tags
30    pub tags: HashMap<String, String>,
31}
32
33impl PromptVersion {
34    /// Create a new prompt version
35    pub fn new(template: &str, variables: Vec<String>) -> Self {
36        let sha256 = Self::compute_hash(template);
37        let id = sha256[..16].to_string(); // Short ID from hash
38
39        Self {
40            id,
41            template: template.to_string(),
42            variables,
43            version: 1,
44            created_at: Utc::now(),
45            sha256,
46            description: None,
47            tags: HashMap::new(),
48        }
49    }
50
51    /// Create with specific version number
52    pub fn with_version(mut self, version: u32) -> Self {
53        self.version = version;
54        self
55    }
56
57    /// Add description
58    pub fn with_description(mut self, desc: &str) -> Self {
59        self.description = Some(desc.to_string());
60        self
61    }
62
63    /// Add tag
64    pub fn with_tag(mut self, key: &str, value: &str) -> Self {
65        self.tags.insert(key.to_string(), value.to_string());
66        self
67    }
68
69    /// Compute SHA-256 hash of template
70    fn compute_hash(template: &str) -> String {
71        let mut hasher = Sha256::new();
72        hasher.update(template.as_bytes());
73        let result = hasher.finalize();
74        hex::encode(result)
75    }
76
77    /// Render template with variables
78    pub fn render(&self, vars: &HashMap<String, String>) -> Result<String> {
79        let mut result = self.template.clone();
80        for var in &self.variables {
81            let placeholder = format!("{{{var}}}");
82            if let Some(value) = vars.get(var) {
83                result = result.replace(&placeholder, value);
84            } else {
85                return Err(LLMError::EvaluationFailed(format!("Missing variable: {var}")));
86            }
87        }
88        Ok(result)
89    }
90
91    /// Extract variables from template
92    pub fn extract_variables(template: &str) -> Vec<String> {
93        let mut vars = Vec::new();
94        let mut in_var = false;
95        let mut current = String::new();
96
97        for c in template.chars() {
98            match c {
99                '{' => {
100                    in_var = true;
101                    current.clear();
102                }
103                '}' if in_var => {
104                    if !current.is_empty() && !vars.contains(&current) {
105                        vars.push(current.clone());
106                    }
107                    in_var = false;
108                }
109                _ if in_var => {
110                    current.push(c);
111                }
112                _non_brace => {}
113            }
114        }
115        vars
116    }
117}