openfunctions-rs 0.1.0

A universal framework for creating and managing LLM tools and agents
Documentation
//! Tool management and representation.
//!
//! This module defines the `Tool` struct, which represents a single executable
//! function, and related components like `ToolLanguage`.

use crate::models::ToolDefinition;
use crate::parser::ToolParser;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

/// Represents a single, executable tool that can be called by an LLM or user.
///
/// A `Tool` is loaded from a script file (e.g., Bash, Python, JavaScript) and
/// contains its definition, which is parsed from comments in the source code.
#[derive(Debug, Clone)]
pub struct Tool {
    /// The name of the tool, derived from its filename.
    pub name: String,

    /// The path to the tool's source file.
    pub path: PathBuf,

    /// The programming language of the tool's script.
    pub language: ToolLanguage,

    /// The detailed definition of the tool, parsed from its source code.
    pub definition: ToolDefinition,

    /// The function declaration for the LLM, generated from the `ToolDefinition`.
    pub declaration: crate::core::function::FunctionDeclaration,
}

impl Tool {
    /// Loads a tool from a source file.
    ///
    /// This function reads a file, determines its language, parses the tool
    /// definition from its comments, and creates a `Tool` instance.
    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        let name = path
            .file_stem()
            .ok_or_else(|| anyhow::anyhow!("Invalid tool filename: {}", path.display()))?
            .to_string_lossy()
            .to_string();

        let language = ToolLanguage::from_extension(
            path.extension().and_then(|ext| ext.to_str()).unwrap_or(""),
        )?;

        let content = tokio::fs::read_to_string(&path).await?;
        let parser = ToolParser::new(language);
        let definition = parser.parse(&content)?;

        let declaration = definition.to_function_declaration(&name)?;

        Ok(Self {
            name,
            path,
            language,
            definition,
            declaration,
        })
    }

    /// Returns the command and arguments needed to execute the tool.
    pub fn get_command(&self) -> Vec<String> {
        match self.language {
            ToolLanguage::Bash => vec!["bash".to_string(), self.path.to_string_lossy().to_string()],
            ToolLanguage::JavaScript => {
                vec!["node".to_string(), self.path.to_string_lossy().to_string()]
            }
            ToolLanguage::Python => vec![
                "python".to_string(),
                self.path.to_string_lossy().to_string(),
            ],
        }
    }
}

/// An enumeration of the programming languages supported for tools.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolLanguage {
    /// Bash shell script
    Bash,
    /// JavaScript (Node.js)
    JavaScript,
    /// Python script
    Python,
}

impl ToolLanguage {
    /// Determines the `ToolLanguage` from a file extension.
    pub fn from_extension(ext: &str) -> Result<Self> {
        match ext {
            "sh" => Ok(Self::Bash),
            "js" => Ok(Self::JavaScript),
            "py" => Ok(Self::Python),
            _ => anyhow::bail!("Unsupported file extension for a tool: {}", ext),
        }
    }

    /// Returns the canonical file extension for the language.
    pub fn extension(&self) -> &'static str {
        match self {
            Self::Bash => "sh",
            Self::JavaScript => "js",
            Self::Python => "py",
        }
    }
}

impl std::fmt::Display for ToolLanguage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Bash => "bash",
            Self::JavaScript => "javascript",
            Self::Python => "python",
        })
    }
}