oxify_model/
template.rs

1//! Workflow templates for parameterized workflow creation
2//!
3//! Templates allow creating reusable workflow patterns with configurable parameters.
4
5use crate::Workflow;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14/// Template ID type
15pub type TemplateId = Uuid;
16
17/// A workflow template with parameterized values
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(ToSchema))]
20pub struct WorkflowTemplate {
21    /// Unique template identifier
22    #[cfg_attr(feature = "openapi", schema(value_type = String))]
23    pub id: TemplateId,
24
25    /// Template name
26    pub name: String,
27
28    /// Template description
29    pub description: Option<String>,
30
31    /// Template category (e.g., "RAG", "Agent", "Data Processing")
32    pub category: Option<String>,
33
34    /// Tags for discovery
35    #[serde(default)]
36    pub tags: Vec<String>,
37
38    /// Template version
39    pub version: String,
40
41    /// Template parameters (configurable values)
42    pub parameters: Vec<TemplateParameter>,
43
44    /// Base workflow JSON (with parameter placeholders)
45    /// Placeholders use format: {{param_name}}
46    pub workflow_json: String,
47
48    /// Template author
49    pub author: Option<String>,
50
51    /// Creation timestamp
52    #[cfg_attr(feature = "openapi", schema(value_type = String))]
53    pub created_at: DateTime<Utc>,
54
55    /// Last updated timestamp
56    #[cfg_attr(feature = "openapi", schema(value_type = String))]
57    pub updated_at: DateTime<Utc>,
58
59    /// Number of times this template has been instantiated
60    #[serde(default)]
61    pub usage_count: u64,
62
63    /// Is this template public
64    #[serde(default)]
65    pub is_public: bool,
66
67    /// Owner user ID
68    #[cfg_attr(feature = "openapi", schema(value_type = String))]
69    pub owner_id: Option<Uuid>,
70}
71
72/// A parameter in a workflow template
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(ToSchema))]
75pub struct TemplateParameter {
76    /// Parameter name (used in placeholders)
77    pub name: String,
78
79    /// Display label for UI
80    pub label: String,
81
82    /// Parameter description
83    pub description: Option<String>,
84
85    /// Parameter type
86    pub param_type: ParameterType,
87
88    /// Default value (JSON)
89    pub default_value: Option<serde_json::Value>,
90
91    /// Whether this parameter is required
92    #[serde(default)]
93    pub required: bool,
94
95    /// Validation rules
96    #[serde(default)]
97    pub validation: Option<ParameterValidation>,
98
99    /// Allowed values (for enum types)
100    #[serde(default)]
101    pub allowed_values: Vec<serde_json::Value>,
102
103    /// Group name for UI organization
104    pub group: Option<String>,
105
106    /// Display order within group
107    #[serde(default)]
108    pub order: u32,
109}
110
111/// Parameter types
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113#[cfg_attr(feature = "openapi", derive(ToSchema))]
114pub enum ParameterType {
115    /// String value
116    String,
117
118    /// Integer number
119    Integer,
120
121    /// Floating point number
122    Float,
123
124    /// Boolean value
125    Boolean,
126
127    /// JSON object
128    Object,
129
130    /// JSON array
131    Array,
132
133    /// Selection from allowed values
134    Enum,
135
136    /// Secret reference (won't be stored in plain text)
137    Secret,
138
139    /// LLM model selection
140    Model,
141
142    /// Vector database collection
143    Collection,
144}
145
146/// Parameter validation rules
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148#[cfg_attr(feature = "openapi", derive(ToSchema))]
149pub struct ParameterValidation {
150    /// Minimum value (for numbers)
151    pub min: Option<f64>,
152
153    /// Maximum value (for numbers)
154    pub max: Option<f64>,
155
156    /// Minimum length (for strings)
157    pub min_length: Option<usize>,
158
159    /// Maximum length (for strings)
160    pub max_length: Option<usize>,
161
162    /// Regex pattern (for strings)
163    pub pattern: Option<String>,
164
165    /// Custom validation message
166    pub message: Option<String>,
167}
168
169impl WorkflowTemplate {
170    /// Create a new workflow template
171    pub fn new(name: String, workflow_json: String) -> Self {
172        let now = Utc::now();
173        Self {
174            id: Uuid::new_v4(),
175            name,
176            description: None,
177            category: None,
178            tags: Vec::new(),
179            version: "1.0.0".to_string(),
180            parameters: Vec::new(),
181            workflow_json,
182            author: None,
183            created_at: now,
184            updated_at: now,
185            usage_count: 0,
186            is_public: false,
187            owner_id: None,
188        }
189    }
190
191    /// Add a parameter to the template
192    pub fn add_parameter(&mut self, param: TemplateParameter) {
193        self.parameters.push(param);
194    }
195
196    /// Validate parameter values against the template
197    pub fn validate_parameters(
198        &self,
199        values: &HashMap<String, serde_json::Value>,
200    ) -> Result<(), Vec<String>> {
201        let mut errors = Vec::new();
202
203        for param in &self.parameters {
204            if param.required && !values.contains_key(&param.name) {
205                errors.push(format!("Required parameter '{}' is missing", param.name));
206                continue;
207            }
208
209            if let Some(value) = values.get(&param.name) {
210                // Type validation
211                if !self.validate_type(&param.param_type, value) {
212                    errors.push(format!(
213                        "Parameter '{}' has invalid type, expected {:?}",
214                        param.name, param.param_type
215                    ));
216                }
217
218                // Range/length validation
219                if let Some(ref validation) = param.validation {
220                    if let Some(err) = self.validate_value(value, validation, &param.name) {
221                        errors.push(err);
222                    }
223                }
224
225                // Enum validation
226                if param.param_type == ParameterType::Enum
227                    && !param.allowed_values.is_empty()
228                    && !param.allowed_values.contains(value)
229                {
230                    errors.push(format!(
231                        "Parameter '{}' must be one of: {:?}",
232                        param.name, param.allowed_values
233                    ));
234                }
235            }
236        }
237
238        if errors.is_empty() {
239            Ok(())
240        } else {
241            Err(errors)
242        }
243    }
244
245    fn validate_type(&self, param_type: &ParameterType, value: &serde_json::Value) -> bool {
246        match param_type {
247            ParameterType::String
248            | ParameterType::Secret
249            | ParameterType::Model
250            | ParameterType::Collection => value.is_string(),
251            ParameterType::Integer => value.is_i64() || value.is_u64(),
252            ParameterType::Float => value.is_f64() || value.is_i64() || value.is_u64(),
253            ParameterType::Boolean => value.is_boolean(),
254            ParameterType::Object => value.is_object(),
255            ParameterType::Array => value.is_array(),
256            ParameterType::Enum => true, // Validated separately
257        }
258    }
259
260    fn validate_value(
261        &self,
262        value: &serde_json::Value,
263        validation: &ParameterValidation,
264        name: &str,
265    ) -> Option<String> {
266        // Number validation
267        if let Some(num) = value.as_f64() {
268            if let Some(min) = validation.min {
269                if num < min {
270                    return Some(format!("Parameter '{}' must be >= {}", name, min));
271                }
272            }
273            if let Some(max) = validation.max {
274                if num > max {
275                    return Some(format!("Parameter '{}' must be <= {}", name, max));
276                }
277            }
278        }
279
280        // String validation
281        if let Some(s) = value.as_str() {
282            if let Some(min_len) = validation.min_length {
283                if s.len() < min_len {
284                    return Some(format!(
285                        "Parameter '{}' must be at least {} characters",
286                        name, min_len
287                    ));
288                }
289            }
290            if let Some(max_len) = validation.max_length {
291                if s.len() > max_len {
292                    return Some(format!(
293                        "Parameter '{}' must be at most {} characters",
294                        name, max_len
295                    ));
296                }
297            }
298            if let Some(ref pattern) = validation.pattern {
299                // Note: Full regex validation would require regex crate
300                // For now, just check if pattern is provided
301                if !pattern.is_empty() {
302                    // Pattern validation would go here
303                }
304            }
305        }
306
307        None
308    }
309
310    /// Instantiate the template with parameter values
311    pub fn instantiate(
312        &self,
313        values: &HashMap<String, serde_json::Value>,
314    ) -> Result<Workflow, String> {
315        // Validate parameters first
316        if let Err(errors) = self.validate_parameters(values) {
317            return Err(format!(
318                "Parameter validation failed: {}",
319                errors.join(", ")
320            ));
321        }
322
323        // Apply parameter substitution
324        let mut workflow_str = self.workflow_json.clone();
325
326        for param in &self.parameters {
327            let placeholder = format!("{{{{{}}}}}", param.name);
328            let value = values
329                .get(&param.name)
330                .or(param.default_value.as_ref())
331                .map(|v| {
332                    if v.is_string() {
333                        v.as_str().unwrap_or("").to_string()
334                    } else {
335                        v.to_string()
336                    }
337                })
338                .unwrap_or_default();
339
340            workflow_str = workflow_str.replace(&placeholder, &value);
341        }
342
343        // Parse the workflow
344        let workflow: Workflow = serde_json::from_str(&workflow_str)
345            .map_err(|e| format!("Failed to parse instantiated workflow: {}", e))?;
346
347        Ok(workflow)
348    }
349
350    /// Create a template from an existing workflow
351    pub fn from_workflow(workflow: &Workflow, name: String) -> Result<Self, String> {
352        let workflow_json = serde_json::to_string_pretty(workflow)
353            .map_err(|e| format!("Failed to serialize workflow: {}", e))?;
354
355        Ok(Self::new(name, workflow_json))
356    }
357
358    /// Extract parameter placeholders from the workflow JSON
359    pub fn extract_placeholders(&self) -> Vec<String> {
360        let mut placeholders = Vec::new();
361
362        // Simple pattern matching without regex crate
363        let chars: Vec<char> = self.workflow_json.chars().collect();
364        let mut i = 0;
365        while i < chars.len().saturating_sub(3) {
366            if chars[i] == '{' && chars[i + 1] == '{' {
367                let start = i + 2;
368                let mut end = start;
369                while end < chars.len().saturating_sub(1)
370                    && !(chars[end] == '}' && chars[end + 1] == '}')
371                {
372                    end += 1;
373                }
374                if end < chars.len().saturating_sub(1) {
375                    let name: String = chars[start..end].iter().collect();
376                    let trimmed = name.trim().to_string();
377                    if !trimmed.is_empty() && !placeholders.contains(&trimmed) {
378                        placeholders.push(trimmed);
379                    }
380                }
381                i = end + 2;
382            } else {
383                i += 1;
384            }
385        }
386
387        placeholders
388    }
389}
390
391impl TemplateParameter {
392    /// Create a new string parameter
393    pub fn string(name: &str, label: &str) -> Self {
394        Self {
395            name: name.to_string(),
396            label: label.to_string(),
397            description: None,
398            param_type: ParameterType::String,
399            default_value: None,
400            required: false,
401            validation: None,
402            allowed_values: Vec::new(),
403            group: None,
404            order: 0,
405        }
406    }
407
408    /// Create a new integer parameter
409    pub fn integer(name: &str, label: &str) -> Self {
410        Self {
411            name: name.to_string(),
412            label: label.to_string(),
413            description: None,
414            param_type: ParameterType::Integer,
415            default_value: None,
416            required: false,
417            validation: None,
418            allowed_values: Vec::new(),
419            group: None,
420            order: 0,
421        }
422    }
423
424    /// Create a new boolean parameter
425    pub fn boolean(name: &str, label: &str) -> Self {
426        Self {
427            name: name.to_string(),
428            label: label.to_string(),
429            description: None,
430            param_type: ParameterType::Boolean,
431            default_value: None,
432            required: false,
433            validation: None,
434            allowed_values: Vec::new(),
435            group: None,
436            order: 0,
437        }
438    }
439
440    /// Create an enum parameter with allowed values
441    pub fn enumeration(name: &str, label: &str, allowed: Vec<&str>) -> Self {
442        Self {
443            name: name.to_string(),
444            label: label.to_string(),
445            description: None,
446            param_type: ParameterType::Enum,
447            default_value: None,
448            required: false,
449            validation: None,
450            allowed_values: allowed
451                .into_iter()
452                .map(|s| serde_json::Value::String(s.to_string()))
453                .collect(),
454            group: None,
455            order: 0,
456        }
457    }
458
459    /// Create a model selection parameter
460    pub fn model(name: &str, label: &str) -> Self {
461        Self {
462            name: name.to_string(),
463            label: label.to_string(),
464            description: None,
465            param_type: ParameterType::Model,
466            default_value: None,
467            required: false,
468            validation: None,
469            allowed_values: Vec::new(),
470            group: None,
471            order: 0,
472        }
473    }
474
475    /// Set as required
476    pub fn required(mut self) -> Self {
477        self.required = true;
478        self
479    }
480
481    /// Set default value
482    pub fn with_default(mut self, value: serde_json::Value) -> Self {
483        self.default_value = Some(value);
484        self
485    }
486
487    /// Set description
488    pub fn with_description(mut self, desc: &str) -> Self {
489        self.description = Some(desc.to_string());
490        self
491    }
492
493    /// Set validation
494    pub fn with_validation(mut self, validation: ParameterValidation) -> Self {
495        self.validation = Some(validation);
496        self
497    }
498
499    /// Set group
500    pub fn in_group(mut self, group: &str) -> Self {
501        self.group = Some(group.to_string());
502        self
503    }
504
505    /// Set order
506    pub fn with_order(mut self, order: u32) -> Self {
507        self.order = order;
508        self
509    }
510}
511
512/// Request to instantiate a template
513#[derive(Debug, Serialize, Deserialize)]
514#[cfg_attr(feature = "openapi", derive(ToSchema))]
515pub struct InstantiateTemplateRequest {
516    /// Workflow name for the new instance
517    pub workflow_name: String,
518
519    /// Parameter values
520    pub parameters: HashMap<String, serde_json::Value>,
521}
522
523/// Template gallery item (for listing)
524#[derive(Debug, Clone, Serialize, Deserialize)]
525#[cfg_attr(feature = "openapi", derive(ToSchema))]
526pub struct TemplateListItem {
527    #[cfg_attr(feature = "openapi", schema(value_type = String))]
528    pub id: TemplateId,
529    pub name: String,
530    pub description: Option<String>,
531    pub category: Option<String>,
532    pub tags: Vec<String>,
533    pub version: String,
534    pub author: Option<String>,
535    pub usage_count: u64,
536    pub is_public: bool,
537    #[cfg_attr(feature = "openapi", schema(value_type = String))]
538    pub created_at: DateTime<Utc>,
539}
540
541impl From<&WorkflowTemplate> for TemplateListItem {
542    fn from(template: &WorkflowTemplate) -> Self {
543        Self {
544            id: template.id,
545            name: template.name.clone(),
546            description: template.description.clone(),
547            category: template.category.clone(),
548            tags: template.tags.clone(),
549            version: template.version.clone(),
550            author: template.author.clone(),
551            usage_count: template.usage_count,
552            is_public: template.is_public,
553            created_at: template.created_at,
554        }
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_template_creation() {
564        let workflow_json = r#"{"metadata": {"name": "{{workflow_name}}"}}"#;
565        let mut template =
566            WorkflowTemplate::new("Test Template".to_string(), workflow_json.to_string());
567
568        template.add_parameter(
569            TemplateParameter::string("workflow_name", "Workflow Name")
570                .required()
571                .with_description("Name of the workflow"),
572        );
573
574        assert_eq!(template.name, "Test Template");
575        assert_eq!(template.parameters.len(), 1);
576        assert!(template.parameters[0].required);
577    }
578
579    #[test]
580    fn test_placeholder_extraction() {
581        let workflow_json =
582            r#"{"name": "{{name}}", "model": "{{model}}", "temp": {{temperature}}}"#;
583        let template = WorkflowTemplate::new("Test".to_string(), workflow_json.to_string());
584
585        let placeholders = template.extract_placeholders();
586
587        assert_eq!(placeholders.len(), 3);
588        assert!(placeholders.contains(&"name".to_string()));
589        assert!(placeholders.contains(&"model".to_string()));
590        assert!(placeholders.contains(&"temperature".to_string()));
591    }
592
593    #[test]
594    fn test_parameter_validation() {
595        let mut template = WorkflowTemplate::new("Test".to_string(), "{}".to_string());
596        template.add_parameter(
597            TemplateParameter::integer("count", "Count")
598                .required()
599                .with_validation(ParameterValidation {
600                    min: Some(1.0),
601                    max: Some(100.0),
602                    ..Default::default()
603                }),
604        );
605
606        // Missing required parameter
607        let values = HashMap::new();
608        assert!(template.validate_parameters(&values).is_err());
609
610        // Invalid value (too low)
611        let mut values = HashMap::new();
612        values.insert("count".to_string(), serde_json::json!(0));
613        assert!(template.validate_parameters(&values).is_err());
614
615        // Valid value
616        let mut values = HashMap::new();
617        values.insert("count".to_string(), serde_json::json!(50));
618        assert!(template.validate_parameters(&values).is_ok());
619    }
620
621    #[test]
622    fn test_enum_parameter() {
623        let param = TemplateParameter::enumeration(
624            "provider",
625            "LLM Provider",
626            vec!["openai", "anthropic", "ollama"],
627        )
628        .required();
629
630        assert_eq!(param.param_type, ParameterType::Enum);
631        assert_eq!(param.allowed_values.len(), 3);
632        assert!(param.required);
633    }
634}