systemprompt-agent 0.2.2

Agent-to-Agent (A2A) protocol for systemprompt.io AI governance: streaming, JSON-RPC models, task lifecycle, .well-known discovery, and governed agent orchestration.
Documentation
use crate::models::Skill;
use crate::repository::content::SkillRepository;
use anyhow::{Result, anyhow};
use systemprompt_database::DbPool;
use systemprompt_identifiers::{SkillId, SourceId};
use systemprompt_models::{IngestionReport, SkillConfig, SkillsConfig};

#[derive(Debug)]
pub struct SkillIngestionService {
    skill_repo: SkillRepository,
}

impl SkillIngestionService {
    pub fn new(db: &DbPool) -> Result<Self> {
        Ok(Self {
            skill_repo: SkillRepository::new(db)?,
        })
    }

    pub async fn ingest_config(
        &self,
        config: &SkillsConfig,
        source_id: SourceId,
        override_existing: bool,
    ) -> Result<IngestionReport> {
        let mut report = IngestionReport::new();
        report.files_found = config.skills.len();

        for (key, skill_config) in &config.skills {
            match self
                .ingest_skill(key, skill_config, source_id.clone(), override_existing)
                .await
            {
                Ok(()) => {
                    report.files_processed += 1;
                },
                Err(e) => {
                    report.errors.push(format!("{key}: {e:?}"));
                },
            }
        }

        Ok(report)
    }

    async fn ingest_skill(
        &self,
        key: &str,
        config: &SkillConfig,
        source_id: SourceId,
        override_existing: bool,
    ) -> Result<()> {
        let skill_id_str = if config.id.as_str().is_empty() {
            key.to_string()
        } else {
            config.id.as_str().to_string()
        };

        let instructions = match config.instructions.as_ref() {
            Some(includable) => includable
                .as_inline()
                .ok_or_else(|| {
                    anyhow!(
                        "Skill '{}' has unresolved !include for instructions; loader must resolve \
                         includes before ingestion",
                        skill_id_str
                    )
                })?
                .to_string(),
            None => String::new(),
        };

        let skill = Skill {
            id: SkillId::new(skill_id_str),
            file_path: String::new(),
            name: config.name.clone(),
            description: config.description.clone(),
            instructions,
            enabled: config.enabled,
            tags: config.tags.clone(),
            category_id: None,
            source_id,
            created_at: chrono::Utc::now(),
            updated_at: chrono::Utc::now(),
        };

        if self.skill_repo.get_by_skill_id(&skill.id).await?.is_some() {
            if override_existing {
                self.skill_repo.update(&skill.id, &skill).await?;
            }
        } else {
            self.skill_repo.create(&skill).await?;
        }

        Ok(())
    }
}