openfunctions-rs 0.1.0

A universal framework for creating and managing LLM tools and agents
Documentation
//! Bash script parser for extracting tool definitions from comments.
//!
//! This parser uses a simple, comment-based annotation syntax to define
//! tools within Bash scripts. The syntax is designed to be readable and
//! easy to write.
//!
//! The following annotations are supported:
//! - `@describe <description>`: A description of the tool.
//! - `@option --<name>...`: Defines a parameter for the tool. See below for details.
//! - `@flag --<name>...`: Defines a boolean flag for the tool.
//! - `@env <VAR_NAME>...`: Defines a required environment variable.
//! - `@meta require-tools <tool1> <tool2>...`: Lists required external tools.
//!
//! For `@option`, the format is: `--<name>[!][<enum_values>][<type_hint>] <description>`
//! - `!` indicates a required parameter.
//! - `[...|...]` provides a list of enum values.
//! - `<TYPE>` provides a type hint (e.g., INT, NUM).

use crate::models::{EnvVarDefinition, ParameterDefinition, ParameterType, ToolDefinition};
use anyhow::Result;
use regex::Regex;
use std::collections::HashMap;

/// Parses a Bash script and extracts a `ToolDefinition`.
///
/// The parser scans for specially formatted comments (`# @...`) to build the
/// tool's definition. A tool description (`@describe`) is mandatory.
pub fn parse(source: &str) -> Result<ToolDefinition> {
    let mut description = String::new();
    let mut parameters = Vec::new();
    let mut env_vars = Vec::new();
    let mut required_tools = Vec::new();
    let metadata = HashMap::new();

    let describe_re = Regex::new(r"^#\s*@describe\s+(.+)$")?;
    let option_re =
        Regex::new(r"^#\s*@option\s+--([a-z0-9-]+)(!?)(?:\[([^\]]+)\])?(?:<([^>]+)>)?\s+(.*)$")?;
    let flag_re = Regex::new(r"^#\s*@flag\s+--([a-z0-9-]+)\s+(.*)$")?;
    let env_re = Regex::new(r"^#\s*@env\s+([A-Z0-9_]+)(!?)(?:=([^\s]+))?\s+(.*)$")?;
    let meta_re = Regex::new(r"^#\s*@meta\s+require-tools\s+(.+)$")?;

    for line in source.lines() {
        let line = line.trim();

        if let Some(caps) = describe_re.captures(line) {
            description = caps.get(1).unwrap().as_str().to_string();
        } else if let Some(caps) = option_re.captures(line) {
            let name = caps.get(1).unwrap().as_str().replace('-', "_");
            let required = !caps.get(2).unwrap().as_str().is_empty();
            let enum_values = caps.get(3).map(|m| {
                m.as_str()
                    .split('|')
                    .map(|s| s.to_string())
                    .collect::<Vec<_>>()
            });
            let type_hint = caps.get(4).map(|m| m.as_str());
            let desc = caps.get(5).unwrap().as_str().to_string();

            let (param_type, enum_values_for_def) = if let Some(values) = enum_values {
                (ParameterType::Enum(values.clone()), Some(values))
            } else if let Some(hint) = type_hint {
                let p_type = match hint.to_uppercase().as_str() {
                    "INT" => ParameterType::Integer,
                    "NUM" => ParameterType::Number,
                    _ => ParameterType::String,
                };
                (p_type, None)
            } else {
                (ParameterType::String, None)
            };

            parameters.push(ParameterDefinition {
                name,
                param_type,
                description: desc,
                required,
                default: None,
                enum_values: enum_values_for_def,
            });
        } else if let Some(caps) = flag_re.captures(line) {
            let name = caps.get(1).unwrap().as_str().replace('-', "_");
            let desc = caps.get(2).unwrap().as_str().to_string();

            parameters.push(ParameterDefinition {
                name,
                param_type: ParameterType::Boolean,
                description: desc,
                required: false,
                default: Some(serde_json::Value::Bool(false)),
                enum_values: None,
            });
        } else if let Some(caps) = env_re.captures(line) {
            let name = caps.get(1).unwrap().as_str().to_string();
            let required = !caps.get(2).unwrap().as_str().is_empty();
            let default = caps.get(3).map(|m| m.as_str().to_string());
            let desc = caps.get(4).unwrap().as_str().to_string();

            env_vars.push(EnvVarDefinition {
                name,
                description: desc,
                required,
                default,
            });
        } else if let Some(caps) = meta_re.captures(line) {
            required_tools = caps
                .get(1)
                .unwrap()
                .as_str()
                .split_whitespace()
                .map(|s| s.to_string())
                .collect();
        }
    }

    if description.is_empty() {
        anyhow::bail!("No @describe annotation found in bash script. A description is required.");
    }

    Ok(ToolDefinition {
        description,
        parameters,
        env_vars,
        required_tools,
        metadata,
    })
}