systemprompt-models 0.2.1

Foundation data models for systemprompt.io AI governance infrastructure. Shared DTOs, config, and domain types consumed by every layer of the MCP governance pipeline.
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use systemprompt_identifiers::SkillId;

use super::IncludableString;
use super::ai::ToolModelConfig;

const fn default_true() -> bool {
    true
}

pub const SKILL_CONFIG_FILENAME: &str = "config.yaml";
pub const DEFAULT_SKILL_CONTENT_FILE: &str = "index.md";

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillsConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,

    #[serde(default)]
    pub auto_discover: bool,

    #[serde(default)]
    pub skills_path: Option<String>,

    #[serde(default)]
    pub skills: HashMap<String, SkillConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillConfig {
    pub id: SkillId,
    pub name: String,
    pub description: String,

    #[serde(default = "default_true")]
    pub enabled: bool,

    #[serde(default)]
    pub tags: Vec<String>,

    #[serde(default)]
    pub instructions: Option<IncludableString>,

    #[serde(default)]
    pub assigned_agents: Vec<String>,

    #[serde(default)]
    pub mcp_servers: Vec<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_config: Option<ToolModelConfig>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct DiskSkillConfig {
    pub id: SkillId,
    pub name: String,
    pub description: String,
    #[serde(default = "default_true")]
    pub enabled: bool,
    #[serde(default)]
    pub file: String,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub category: Option<String>,
}

impl DiskSkillConfig {
    pub fn content_file(&self) -> &str {
        if self.file.is_empty() {
            DEFAULT_SKILL_CONTENT_FILE
        } else {
            &self.file
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SkillSummary {
    pub skill_id: SkillId,
    pub name: String,
    pub display_name: String,
    pub enabled: bool,
    pub file_path: Option<String>,
    pub tags: Vec<String>,
}

impl From<&DiskSkillConfig> for SkillSummary {
    fn from(config: &DiskSkillConfig) -> Self {
        let file_path = if config.file.is_empty() {
            None
        } else {
            Some(config.file.clone())
        };
        Self {
            skill_id: config.id.clone(),
            name: config.name.clone(),
            display_name: config.name.clone(),
            enabled: config.enabled,
            file_path,
            tags: config.tags.clone(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SkillDetail {
    pub skill_id: SkillId,
    pub name: String,
    pub display_name: String,
    pub description: String,
    pub enabled: bool,
    pub tags: Vec<String>,
    pub category: Option<String>,
    pub file_path: Option<String>,
    pub instructions_preview: String,
}

impl From<&DiskSkillConfig> for SkillDetail {
    fn from(config: &DiskSkillConfig) -> Self {
        let file_path = if config.file.is_empty() {
            None
        } else {
            Some(config.file.clone())
        };
        Self {
            skill_id: config.id.clone(),
            name: config.name.clone(),
            display_name: config.name.clone(),
            description: config.description.clone(),
            enabled: config.enabled,
            tags: config.tags.clone(),
            category: config.category.clone(),
            file_path,
            instructions_preview: String::new(),
        }
    }
}

pub fn strip_frontmatter(content: &str) -> String {
    let parts: Vec<&str> = content.splitn(3, "---").collect();
    if parts.len() >= 3 {
        parts[2].trim().to_string()
    } else {
        content.to_string()
    }
}