systemprompt-agent 0.2.0

Core Agent protocol module for systemprompt.io
Documentation
use crate::models::a2a::{AgentCapabilities, AgentCard, AgentInterface, TransportProtocol};
use serde::{Deserialize, Serialize};

use super::card_input::AgentCardInput;
use super::validation::{is_valid_version, list_available_mcp_servers};

#[derive(Debug, Clone, Deserialize)]
pub struct CreateAgentRequestRaw {
    pub card: AgentCardInput,
    pub is_active: Option<bool>,
    pub system_prompt: Option<String>,
    pub mcp_servers: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize)]
pub struct CreateAgentRequest {
    pub card: AgentCard,
    pub is_active: Option<bool>,
    pub system_prompt: Option<String>,
    pub mcp_servers: Option<Vec<String>>,
}

impl<'de> Deserialize<'de> for CreateAgentRequest {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = CreateAgentRequestRaw::deserialize(deserializer)?;

        let url = raw
            .card
            .url
            .unwrap_or_else(|| format!("http://placeholder/api/v1/agents/{}", raw.card.name));

        let card = AgentCard {
            name: raw.card.name,
            description: raw.card.description,
            supported_interfaces: vec![AgentInterface {
                url,
                protocol_binding: raw
                    .card
                    .preferred_transport
                    .unwrap_or(TransportProtocol::JsonRpc),
                protocol_version: raw.card.protocol_version,
            }],
            version: raw.card.version,
            icon_url: None,
            provider: None,
            documentation_url: None,
            capabilities: raw.card.capabilities.normalize(),
            security_schemes: raw.card.security_schemes,
            security: raw.card.security,
            default_input_modes: if raw.card.default_input_modes.is_empty() {
                vec!["text/plain".to_string()]
            } else {
                raw.card.default_input_modes
            },
            default_output_modes: if raw.card.default_output_modes.is_empty() {
                vec!["text/plain".to_string()]
            } else {
                raw.card.default_output_modes
            },
            skills: raw.card.skills,
            supports_authenticated_extended_card: None,
            signatures: None,
        };

        Ok(Self {
            card,
            is_active: raw.is_active,
            system_prompt: raw.system_prompt,
            mcp_servers: raw.mcp_servers,
        })
    }
}

impl CreateAgentRequest {
    pub fn from_raw(raw: CreateAgentRequestRaw, api_server_url: &str) -> Self {
        let url = raw
            .card
            .url
            .unwrap_or_else(|| format!("{}/api/v1/agents/{}", api_server_url, raw.card.name));

        let card = AgentCard {
            name: raw.card.name,
            description: raw.card.description,
            supported_interfaces: vec![AgentInterface {
                url,
                protocol_binding: raw
                    .card
                    .preferred_transport
                    .unwrap_or(TransportProtocol::JsonRpc),
                protocol_version: raw.card.protocol_version,
            }],
            version: raw.card.version,
            icon_url: None,
            provider: None,
            documentation_url: None,
            capabilities: raw.card.capabilities.normalize(),
            security_schemes: raw.card.security_schemes,
            security: raw.card.security,
            default_input_modes: if raw.card.default_input_modes.is_empty() {
                vec!["text/plain".to_string()]
            } else {
                raw.card.default_input_modes
            },
            default_output_modes: if raw.card.default_output_modes.is_empty() {
                vec!["text/plain".to_string()]
            } else {
                raw.card.default_output_modes
            },
            skills: raw.card.skills,
            supports_authenticated_extended_card: None,
            signatures: None,
        };

        Self {
            card,
            is_active: raw.is_active,
            system_prompt: raw.system_prompt,
            mcp_servers: raw.mcp_servers,
        }
    }

    pub async fn validate(&self) -> Result<(), String> {
        if self.card.name.trim().is_empty() {
            return Err("Name is required".to_string());
        }

        let card_url = self.card.url().unwrap_or("");
        if !card_url.starts_with("http://") && !card_url.starts_with("https://") {
            return Err("URL must be a valid HTTP or HTTPS URL".to_string());
        }

        if !is_valid_version(&self.card.version) {
            return Err("Version must be in semantic version format (e.g., 1.0.0)".to_string());
        }

        if let Some(ref mcp_servers) = self.mcp_servers {
            if !mcp_servers.is_empty() {
                let available_servers = list_available_mcp_servers().await?;
                let mut invalid_servers = Vec::new();

                for server in mcp_servers {
                    if !available_servers.contains(server) {
                        invalid_servers.push(server.clone());
                    }
                }

                if !invalid_servers.is_empty() {
                    return Err(format!(
                        "Invalid MCP server(s): {}. Available servers: {}",
                        invalid_servers.join(", "),
                        if available_servers.is_empty() {
                            "(none)".to_string()
                        } else {
                            available_servers.join(", ")
                        }
                    ));
                }
            }
        }

        Ok(())
    }

    pub fn get_version(&self) -> String {
        self.card.version.clone()
    }

    pub fn is_active(&self) -> bool {
        self.is_active.unwrap_or(true)
    }

    pub fn extract_port(&self) -> u16 {
        self.card
            .url()
            .and_then(super::validation::extract_port_from_url)
            .unwrap_or(80)
    }

    pub const fn get_capabilities(&self) -> &AgentCapabilities {
        &self.card.capabilities
    }
}