Skip to main content

systemprompt_cli/commands/core/plugins/generate/
agents.rs

1//! Agent markdown generation for plugin bundles.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::Result;
7use std::path::Path;
8use systemprompt_models::{ComponentSource, PluginConfig};
9
10use super::DEFAULT_AGENT_TOOLS;
11
12pub fn generate_agents(
13    plugin: &PluginConfig,
14    services_path: &Path,
15    output_dir: &Path,
16    files_generated: &mut Vec<String>,
17) -> Result<()> {
18    let agents_dir = output_dir.join("agents");
19
20    let agents = resolve_agents(plugin, services_path)?;
21
22    if agents.is_empty() {
23        return Ok(());
24    }
25
26    std::fs::create_dir_all(&agents_dir)?;
27
28    let services_agents_dir = services_path.join("agents");
29
30    for agent in &agents {
31        let agent_md = build_agent_md(agent, &services_agents_dir)?;
32        let agent_path = agents_dir.join(format!("{agent}.md"));
33        std::fs::write(&agent_path, &agent_md)?;
34        files_generated.push(agent_path.to_string_lossy().to_string());
35    }
36
37    Ok(())
38}
39
40fn resolve_agents(plugin: &PluginConfig, services_path: &Path) -> Result<Vec<String>> {
41    if plugin.agents.source == ComponentSource::Explicit {
42        return Ok(plugin.agents.include.clone());
43    }
44
45    let agents_config_path = services_path.join("config").join("config.yaml");
46    if !agents_config_path.exists() {
47        return Ok(Vec::new());
48    }
49
50    let content = std::fs::read_to_string(&agents_config_path)?;
51    let config: serde_yaml::Value = serde_yaml::from_str(&content)?;
52
53    let mut ids = Vec::new();
54    if let Some(agents) = config.get("agents").and_then(|a| a.as_mapping()) {
55        for (key, _) in agents {
56            if let Some(name) = key.as_str()
57                && !plugin.agents.exclude.contains(&name.to_owned())
58            {
59                ids.push(name.to_owned());
60            }
61        }
62    }
63
64    ids.sort();
65    Ok(ids)
66}
67
68fn build_agent_md(agent: &str, services_agents_dir: &Path) -> Result<String> {
69    if services_agents_dir.exists() {
70        for entry in std::fs::read_dir(services_agents_dir)? {
71            let entry = entry?;
72            let path = entry.path();
73            let ext = path.extension().and_then(|e| e.to_str());
74            if ext != Some("yaml") && ext != Some("yml") {
75                continue;
76            }
77            let content = std::fs::read_to_string(&path)?;
78            let config: serde_yaml::Value = match serde_yaml::from_str(&content) {
79                Ok(c) => c,
80                Err(e) => {
81                    tracing::warn!(path = %path.display(), error = %e, "Failed to parse YAML");
82                    continue;
83                },
84            };
85            if let Some(agent_val) = config.get("agents").and_then(|a| a.get(agent)) {
86                let description = agent_val
87                    .get("card")
88                    .and_then(|c| c.get("description"))
89                    .and_then(|d| d.as_str())
90                    .map_or_else(|| format!("{agent} agent"), str::to_owned);
91                let system_prompt = agent_val
92                    .get("metadata")
93                    .and_then(|m| m.get("systemPrompt"))
94                    .and_then(|s| s.as_str())
95                    .map_or_else(String::new, str::to_owned);
96                return Ok(format!(
97                    "---\nname: {}\ndescription: \"{}\"\ntools: {}\n---\n\n{}\n",
98                    agent,
99                    description.replace('"', "\\\""),
100                    DEFAULT_AGENT_TOOLS,
101                    system_prompt.trim()
102                ));
103            }
104        }
105    }
106
107    Ok(format!(
108        "---\nname: {}\ndescription: \"{} agent\"\ntools: {}\n---\n\nYou are the {} agent.\n",
109        agent, agent, DEFAULT_AGENT_TOOLS, agent
110    ))
111}