Skip to main content

systemprompt_cli/commands/admin/agents/
create.rs

1use anyhow::{Context, Result};
2use clap::Args;
3use std::path::Path;
4
5use super::shared::AgentArgs;
6use super::types::AgentCreateOutput;
7use crate::CliConfig;
8use crate::interactive::{Prompter, resolve_required};
9use crate::shared::CommandOutput;
10use systemprompt_agent::services::config_authoring::{
11    AgentConfigAuthoringService, AgentCreateRequest,
12};
13use systemprompt_config::ProfileBootstrap;
14use systemprompt_loader::ConfigLoader;
15use systemprompt_logging::CliService;
16
17#[derive(Debug, Args)]
18pub struct CreateArgs {
19    #[arg(long, help = "Agent name")]
20    pub name: Option<String>,
21
22    #[arg(long, help = "Enable the agent after creation")]
23    pub enabled: bool,
24
25    #[command(flatten)]
26    pub agent: AgentArgs,
27}
28
29struct ResolvedAgentInput {
30    name: String,
31    port: u16,
32    display_name: String,
33    description: String,
34    system_prompt: String,
35}
36
37pub(super) fn execute(
38    args: CreateArgs,
39    prompter: &dyn Prompter,
40    config: &CliConfig,
41) -> Result<CommandOutput> {
42    let mut agent_args = args.agent;
43
44    let name = resolve_required(args.name, "name", config, || prompt_name(prompter))?;
45    AgentConfigAuthoringService::validate_agent_name(&name)?;
46
47    let port = resolve_required(agent_args.port, "port", config, || prompt_port(prompter))?;
48    AgentConfigAuthoringService::validate_port(port)?;
49
50    let display_name =
51        resolve_display_name(agent_args.display_name.take(), &name, prompter, config);
52    let description = resolve_description(agent_args.description.take(), prompter, config);
53    let system_prompt = AgentConfigAuthoringService::resolve_system_prompt(
54        agent_args.system_prompt_file.as_deref(),
55        agent_args.system_prompt.take(),
56        &display_name,
57        &description,
58    )?;
59
60    CliService::info(&format!(
61        "Creating agent '{}' on port {} (display: {})...",
62        name, port, display_name
63    ));
64
65    let input = ResolvedAgentInput {
66        name: name.clone(),
67        port,
68        display_name,
69        description,
70        system_prompt,
71    };
72    let request = build_create_request(input, args.enabled, agent_args);
73
74    let agent_file = write_agent_config(request)?;
75
76    CliService::success(&format!(
77        "Agent '{}' created at {}",
78        name,
79        agent_file.display()
80    ));
81
82    let output = AgentCreateOutput {
83        name: name.clone(),
84        message: format!(
85            "Agent '{}' created successfully at {}",
86            name,
87            agent_file.display()
88        ),
89    };
90
91    Ok(CommandOutput::card_value("Agent Created", &output))
92}
93
94fn resolve_display_name(
95    arg: Option<String>,
96    name: &str,
97    prompter: &dyn Prompter,
98    config: &CliConfig,
99) -> String {
100    arg.unwrap_or_else(|| {
101        if config.is_interactive() {
102            prompt_display_name(prompter, name).unwrap_or_else(|_| name.to_owned())
103        } else {
104            name.to_owned()
105        }
106    })
107}
108
109fn resolve_description(arg: Option<String>, prompter: &dyn Prompter, config: &CliConfig) -> String {
110    arg.unwrap_or_else(|| {
111        if config.is_interactive() {
112            prompt_description(prompter).unwrap_or_else(|e| {
113                tracing::warn!(error = %e, "Failed to prompt for description");
114                String::new()
115            })
116        } else {
117            String::new()
118        }
119    })
120}
121
122fn build_create_request(
123    input: ResolvedAgentInput,
124    enabled: bool,
125    agent: AgentArgs,
126) -> AgentCreateRequest {
127    AgentCreateRequest {
128        name: input.name,
129        port: input.port,
130        display_name: input.display_name,
131        description: input.description,
132        system_prompt: input.system_prompt,
133        enabled,
134        endpoint: agent.endpoint,
135        dev_only: agent.dev_only,
136        is_primary: agent.is_primary,
137        default: agent.default,
138        version: agent.version,
139        icon_url: agent.icon_url,
140        documentation_url: agent.documentation_url,
141        streaming: agent.streaming,
142        push_notifications: agent.push_notifications,
143        state_transition_history: agent.state_transition_history,
144        provider: agent.provider,
145        model: agent.model,
146        mcp_servers: agent.mcp_servers,
147        skills: agent.skills,
148    }
149}
150
151fn write_agent_config(request: AgentCreateRequest) -> Result<std::path::PathBuf> {
152    let profile = ProfileBootstrap::get().context("Failed to get profile")?;
153    let services_dir = Path::new(&profile.paths.services);
154    let name = request.name.clone();
155
156    let service = AgentConfigAuthoringService::new(services_dir);
157    let agent_file = service
158        .create(request)
159        .with_context(|| format!("Failed to create agent '{}'", name))?;
160
161    ConfigLoader::load().with_context(|| {
162        format!(
163            "Agent file created at {} but validation failed. Please check the configuration.",
164            agent_file.display()
165        )
166    })?;
167
168    Ok(agent_file)
169}
170
171pub fn validate_name_input(input: &str) -> Result<(), &'static str> {
172    if input.len() < 3 {
173        return Err("Name must be at least 3 characters");
174    }
175    if !input
176        .chars()
177        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
178    {
179        return Err("Name must be lowercase alphanumeric with underscores only");
180    }
181    Ok(())
182}
183
184pub const fn validate_port_input(port: u16) -> Result<(), &'static str> {
185    if port == 0 {
186        return Err("Port cannot be 0");
187    }
188    if port < 1024 {
189        return Err("Port should be >= 1024 (non-privileged)");
190    }
191    Ok(())
192}
193
194pub fn prompt_name(prompter: &dyn Prompter) -> Result<String> {
195    loop {
196        let input = prompter
197            .input("Agent name")
198            .context("Failed to get agent name")?;
199        match validate_name_input(&input) {
200            Ok(()) => return Ok(input),
201            Err(msg) => CliService::warning(msg),
202        }
203    }
204}
205
206pub fn prompt_port(prompter: &dyn Prompter) -> Result<u16> {
207    loop {
208        let raw = prompter
209            .input_with_default("Port", "8001")
210            .context("Failed to get port")?;
211        let port: u16 = match raw.trim().parse() {
212            Ok(port) => port,
213            Err(e) => {
214                CliService::warning(&format!("'{}' is not a valid port: {}", raw.trim(), e));
215                continue;
216            },
217        };
218        match validate_port_input(port) {
219            Ok(()) => return Ok(port),
220            Err(msg) => CliService::warning(msg),
221        }
222    }
223}
224
225fn prompt_display_name(prompter: &dyn Prompter, default: &str) -> Result<String> {
226    prompter
227        .input_with_default("Display name", default)
228        .context("Failed to get display name")
229}
230
231fn prompt_description(prompter: &dyn Prompter) -> Result<String> {
232    prompter
233        .input_with_default("Description", "")
234        .context("Failed to get description")
235}