diff --git a/.changeset/quiet-cars-clap.md b/.changeset/quiet-cars-clap.md new file mode 100644 index 0000000000000000000000000000000000000000..2d9b933d4e99976687175cc4edb921d067533bda --- /dev/null +++ b/.changeset/quiet-cars-clap.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +Add OpenAPI action tool diff --git a/helpers/tools.ts b/helpers/tools.ts index c12cbd59ea841964f4515a34b78e49f278a68628..501b320bc3ca5473cc4f9e4d1942604171cd3e1e 100644 --- a/helpers/tools.ts +++ b/helpers/tools.ts @@ -117,6 +117,37 @@ export const supportedTools: Tool[] = [ }, ], }, + { + display: "OpenAPI action", + name: "openapi_action.OpenAPIActionToolSpec", + dependencies: [ + { + name: "llama-index-tools-openapi", + version: "0.1.3", + }, + { + name: "jsonschema", + version: "^4.22.0", + }, + { + name: "llama-index-tools-requests", + version: "0.1.3", + }, + ], + config: { + openapi_uri: "The URL or file path of the OpenAPI schema", + }, + supportedFrameworks: ["fastapi"], + type: ToolType.LOCAL, + envVars: [ + { + name: TOOL_SYSTEM_PROMPT_ENV_VAR, + description: "System prompt for openapi action tool.", + value: + "You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.", + }, + ], + }, ]; export const getTool = (toolName: string): Tool | undefined => { diff --git a/templates/components/engines/python/agent/tools/__init__.py b/templates/components/engines/python/agent/tools/__init__.py index d2e5da6942af4f01780cd3367366f4b0701363bb..f2c6c98536206d7f30d818e562635be05c201900 100644 --- a/templates/components/engines/python/agent/tools/__init__.py +++ b/templates/components/engines/python/agent/tools/__init__.py @@ -1,7 +1,8 @@ import os import yaml +import json import importlib - +from cachetools import cached, LRUCache from llama_index.core.tools.tool_spec.base import BaseToolSpec from llama_index.core.tools.function_tool import FunctionTool @@ -19,6 +20,14 @@ class ToolFactory: } @staticmethod + @cached( + LRUCache(maxsize=100), + key=lambda tool_type, tool_name, config: ( + tool_type, + tool_name, + json.dumps(config, sort_keys=True), + ), + ) def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]: source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type] try: diff --git a/templates/components/engines/python/agent/tools/openapi_action.py b/templates/components/engines/python/agent/tools/openapi_action.py new file mode 100644 index 0000000000000000000000000000000000000000..e9f1933224a713f9dea3ca3461c8894d02ea63a5 --- /dev/null +++ b/templates/components/engines/python/agent/tools/openapi_action.py @@ -0,0 +1,71 @@ +from typing import Dict, List, Tuple +from llama_index.tools.openapi import OpenAPIToolSpec +from llama_index.tools.requests import RequestsToolSpec + + +class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec): + """ + A combination of OpenAPI and Requests tool specs that can parse OpenAPI specs and make requests. + + openapi_uri: str: The file path or URL to the OpenAPI spec. + domain_headers: dict: Whitelist domains and the headers to use. + """ + + spec_functions = OpenAPIToolSpec.spec_functions + RequestsToolSpec.spec_functions + + def __init__(self, openapi_uri: str, domain_headers: dict = {}, **kwargs): + # Load the OpenAPI spec + openapi_spec, servers = self.load_openapi_spec(openapi_uri) + + # Add the servers to the domain headers if they are not already present + for server in servers: + if server not in domain_headers: + domain_headers[server] = {} + + OpenAPIToolSpec.__init__(self, spec=openapi_spec) + RequestsToolSpec.__init__(self, domain_headers) + + @staticmethod + def load_openapi_spec(uri: str) -> Tuple[Dict, List[str]]: + """ + Load an OpenAPI spec from a URI. + + Args: + uri (str): A file path or URL to the OpenAPI spec. + + Returns: + List[Document]: A list of Document objects. + """ + import yaml + from urllib.parse import urlparse + + if uri.startswith("http"): + import requests + + response = requests.get(uri) + if response.status_code != 200: + raise ValueError( + "Could not initialize OpenAPIActionToolSpec: " + f"Failed to load OpenAPI spec from {uri}, status code: {response.status_code}" + ) + spec = yaml.safe_load(response.text) + elif uri.startswith("file"): + filepath = urlparse(uri).path + with open(filepath, "r") as file: + spec = yaml.safe_load(file) + else: + raise ValueError( + "Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI URI provided. " + "Only HTTP and file path are supported." + ) + # Add the servers to the whitelist + try: + servers = [ + urlparse(server["url"]).netloc for server in spec.get("servers", []) + ] + except KeyError as e: + raise ValueError( + "Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI spec provided. " + "Could not get `servers` from the spec." + ) from e + return spec, servers