formal-ai 0.266.0

Formal symbolic AI implementation with OpenAI-compatible APIs
use super::embedded::CLIENT_INTEGRATIONS_LINO;
use super::parser::{parse_lino, split_pipe_list};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigFormat {
    Toml,
    Json,
    ShellEnv,
}

impl ConfigFormat {
    fn from_seed(value: &str) -> Option<Self> {
        match value {
            "toml" => Some(Self::Toml),
            "json" => Some(Self::Json),
            "shell_env" => Some(Self::ShellEnv),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelArgPosition {
    BeforeArgs,
    AfterFirstArg,
}

impl ModelArgPosition {
    fn from_seed(value: &str) -> Option<Self> {
        match value {
            "before_args" => Some(Self::BeforeArgs),
            "after_first_arg" => Some(Self::AfterFirstArg),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct TemplateEnv {
    pub key: String,
    pub value: String,
}

#[derive(Debug, Clone, Default)]
pub struct ClientIntegrationInvocation {
    pub prepend_args: Vec<String>,
    pub args: Vec<String>,
    pub env: Vec<TemplateEnv>,
    pub config_content_env: String,
    pub config_env: String,
    pub config_dir_env: String,
    pub config_json_settings: Vec<(String, String)>,
    pub temp_home_env: String,
    pub temp_home_config_path: String,
    pub temp_home_json_settings: Vec<(String, String)>,
    pub model_arg: String,
    pub model_arg_position: Option<ModelArgPosition>,
}

#[derive(Debug, Clone)]
pub struct ClientIntegrationGlobalConfig {
    pub format: ConfigFormat,
    pub path: String,
    pub backup_suffix: String,
    pub toml_settings: Vec<(String, String)>,
    pub json_settings: Vec<(String, String)>,
    pub shell_env: Vec<TemplateEnv>,
}

#[derive(Debug, Clone)]
pub struct ClientIntegration {
    pub id: String,
    pub label: String,
    pub command: String,
    pub provider_id: String,
    pub default_protocol: String,
    pub supported_protocols: Vec<String>,
    pub endpoints: Vec<(String, String)>,
    pub api_key_env: String,
    pub api_key_default: String,
    pub model_selector: String,
    pub invocation: ClientIntegrationInvocation,
    pub global_config: ClientIntegrationGlobalConfig,
}

impl ClientIntegration {
    #[must_use]
    pub fn endpoint_path_for(&self, protocol: &str) -> Option<&str> {
        self.endpoints
            .iter()
            .find_map(|(candidate, path)| (candidate == protocol).then_some(path.as_str()))
    }
}

#[must_use]
pub fn client_integrations() -> Vec<ClientIntegration> {
    let tree = parse_lino(CLIENT_INTEGRATIONS_LINO);
    let mut out = Vec::new();
    for root in &tree.children {
        if root.name != "client_integrations" {
            continue;
        }
        for tool in root.children.iter().filter(|node| node.name == "tool") {
            let Some(integration) = parse_tool(tool) else {
                continue;
            };
            out.push(integration);
        }
    }
    out
}

fn parse_tool(tool: &super::parser::LinoNode) -> Option<ClientIntegration> {
    let id = tool.id.clone();
    if id.is_empty() {
        return None;
    }
    let label = tool.find_child_value("label").to_string();
    let command = tool.find_child_value("command").to_string();
    let provider_id = tool.find_child_value("provider_id").to_string();
    let default_protocol = tool.find_child_value("default_protocol").to_string();
    let supported_protocols = split_pipe_list(tool.find_child_value("supported_protocols"));
    let api_key_env = tool.find_child_value("api_key_env").to_string();
    let api_key_default = tool.find_child_value("api_key_default").to_string();
    let mut endpoints = Vec::new();
    for child in &tool.children {
        if let Some(protocol) = child.name.strip_prefix("endpoint_") {
            endpoints.push((protocol.to_string(), child.id.clone()));
        }
    }
    let model_selector = tool.find_child_value("model_selector").to_string();
    let invocation = tool
        .children
        .iter()
        .find(|node| node.name == "ephemeral")
        .map(parse_invocation)
        .unwrap_or_default();
    let global_config = tool
        .children
        .iter()
        .find(|node| node.name == "global")
        .and_then(parse_global_config)?;
    Some(ClientIntegration {
        id,
        label,
        command,
        provider_id,
        default_protocol,
        supported_protocols,
        endpoints,
        api_key_env,
        api_key_default,
        model_selector,
        invocation,
        global_config,
    })
}

fn parse_invocation(node: &super::parser::LinoNode) -> ClientIntegrationInvocation {
    let mut invocation = ClientIntegrationInvocation::default();
    for child in &node.children {
        match child.name.as_str() {
            "prepend_arg" => invocation.prepend_args.push(child.id.clone()),
            "arg" => invocation.args.push(child.id.clone()),
            "env" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    invocation.env.push(TemplateEnv { key, value });
                }
            }
            "config_content_env" => invocation.config_content_env.clone_from(&child.id),
            "config_env" => invocation.config_env.clone_from(&child.id),
            "config_dir_env" => invocation.config_dir_env.clone_from(&child.id),
            "config_json_set" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    invocation.config_json_settings.push((key, value));
                }
            }
            "temp_home_env" => invocation.temp_home_env.clone_from(&child.id),
            "temp_home_config_path" => invocation.temp_home_config_path.clone_from(&child.id),
            "temp_home_json_set" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    invocation.temp_home_json_settings.push((key, value));
                }
            }
            "model_arg" => invocation.model_arg.clone_from(&child.id),
            "model_arg_position" => {
                invocation.model_arg_position = ModelArgPosition::from_seed(&child.id);
            }
            _ => {}
        }
    }
    invocation
}

fn parse_global_config(node: &super::parser::LinoNode) -> Option<ClientIntegrationGlobalConfig> {
    let format = ConfigFormat::from_seed(node.find_child_value("kind"))?;
    let mut config = ClientIntegrationGlobalConfig {
        format,
        path: node.find_child_value("path").to_string(),
        backup_suffix: node.find_child_value("backup_suffix").to_string(),
        toml_settings: Vec::new(),
        json_settings: Vec::new(),
        shell_env: Vec::new(),
    };
    for child in &node.children {
        match child.name.as_str() {
            "toml_set" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    config.toml_settings.push((key, value));
                }
            }
            "json_set" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    config.json_settings.push((key, value));
                }
            }
            "shell_env" => {
                if let Some((key, value)) = split_once_equals(&child.id) {
                    config.shell_env.push(TemplateEnv { key, value });
                }
            }
            _ => {}
        }
    }
    Some(config)
}

fn split_once_equals(value: &str) -> Option<(String, String)> {
    let (key, value) = value.split_once('=')?;
    let key = key.trim();
    if key.is_empty() {
        return None;
    }
    Some((key.to_string(), value.trim().to_string()))
}