openfunctions-rs 0.1.0

A universal framework for creating and managing LLM tools and agents
Documentation
//! JavaScript parser for extracting tool definitions from JSDoc comments.
//!
//! This parser extracts tool definitions from a JSDoc comment block associated
//! with a function. It expects a single, well-formed JSDoc block in the file.
//!
//! The following annotations are supported:
//! - A multiline description of the tool.
//! - `@property {type} [name] - description`: Defines a parameter. `[]` make it optional.
//! - `@env {VAR_NAME} [description]`: Defines a required environment variable.
//! - `@meta require-tools <tool1> <tool2>`: Lists required external tools.

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

/// Parses a JavaScript file and extracts a `ToolDefinition` from its JSDoc.
pub fn parse(source: &str) -> Result<ToolDefinition> {
    let jsdoc_re = Regex::new(r"/\*\*([\s\S]*?)\*/")?;
    let jsdoc = jsdoc_re
        .captures(source)
        .ok_or_else(|| anyhow::anyhow!("No JSDoc comment found"))?
        .get(1)
        .unwrap()
        .as_str();

    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 property_re =
        Regex::new(r"@property\s+\{([^}]+)\}\s+(\[?)([a-zA-Z0-9_]+)\]?\s*-?\s*(.*)$")?;
    let env_re = Regex::new(r"@env\s+\{([A-Z_][A-Z0-9_]*)\}\s*(.*)")?;
    let meta_re = Regex::new(r"@meta\s+require-tools\s+(.+)$")?;

    for line in jsdoc.lines() {
        let line = line.trim().trim_start_matches('*').trim();

        if line.is_empty() || line.starts_with('@') {
            if description.is_empty() && !line.starts_with('@') {
                continue;
            }
        } else if description.is_empty() {
            description = line.to_string();
        }

        if let Some(caps) = property_re.captures(line) {
            let type_str = caps.get(1).unwrap().as_str();
            let optional_bracket = caps.get(2).unwrap().as_str();
            let name = caps.get(3).unwrap().as_str().to_string();
            let desc = caps.get(4).unwrap().as_str().to_string();

            let required = optional_bracket.is_empty();
            let param_type = parse_js_type(type_str)?;

            parameters.push(ParameterDefinition {
                name,
                param_type,
                description: desc,
                required,
                default: None,
                enum_values: None,
            });
        } else if let Some(caps) = env_re.captures(line) {
            let name = caps.get(1).unwrap().as_str().to_string();
            let desc = caps.get(2).unwrap().as_str().to_string();
            let required = !desc.contains("[optional]");

            env_vars.push(EnvVarDefinition {
                name,
                description: desc.replace("[optional]", "").trim().to_string(),
                required,
                default: None,
            });
        } 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 description found in JSDoc");
    }

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

fn parse_js_type(type_str: &str) -> Result<ParameterType> {
    let type_str = type_str.trim();

    if type_str.contains('|') {
        let values: Vec<String> = type_str
            .split('|')
            .map(|s| s.trim().trim_matches('\'').trim_matches('"').to_string())
            .collect();
        return Ok(ParameterType::Enum(values));
    }

    if type_str.ends_with("[]") {
        return Ok(ParameterType::Array);
    }

    match type_str.to_lowercase().as_str() {
        "string" => Ok(ParameterType::String),
        "number" => Ok(ParameterType::Number),
        "integer" => Ok(ParameterType::Integer),
        "boolean" => Ok(ParameterType::Boolean),
        "array" => Ok(ParameterType::Array),
        "object" => Ok(ParameterType::Object),
        _ => Ok(ParameterType::String),
    }
}