-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Tool openapi integration #151
Closed
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7c2c729
Add Tool.from_openapi_spec
vblagoje 280db90
Tool.from_openapi_spec returns multiple tools
vblagoje bc21871
typo
vblagoje 1af39a6
Update to the latest openapi-llm
vblagoje 860b558
Minor fixes
vblagoje 1e69cd6
Fix test
vblagoje 5464281
Add dedicated Tool.from_openapi_spec module
vblagoje e1712f8
Refactor openapi support from Tool
vblagoje 47b5468
Remove types.py
vblagoje 5ce6b67
Add Tool.from_openapi
vblagoje d443fe9
Merge branch 'main' into tool_openapi_integration
vblagoje 38af9c0
Linting
vblagoje 3bc124d
More linting, test fixes
vblagoje 877a3e6
Make methods internal
vblagoje ae1efed
Update tests
vblagoje File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <[email protected]> | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <[email protected]> | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from pathlib import Path | ||
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union | ||
|
||
from haystack.lazy_imports import LazyImport | ||
from haystack.logging import logging | ||
|
||
from haystack_experimental.dataclasses import Tool | ||
|
||
with LazyImport(message="Run 'pip install openapi-llm'") as openapi_llm_import: | ||
from openapi_llm.client.config import ClientConfig | ||
from openapi_llm.client.openapi import OpenAPIClient | ||
from openapi_llm.core.spec import OpenAPISpecification | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class OpenAPIKwargs(TypedDict, total=False): | ||
""" | ||
TypedDict for OpenAPI configuration kwargs. | ||
|
||
Contains all supported configuration options for Tool.from_openapi_spec() | ||
""" | ||
|
||
credentials: Any # API credentials (e.g., API key, auth token) | ||
request_sender: Callable[[Dict[str, Any]], Dict[str, Any]] # Custom HTTPrequest sender function | ||
allowed_operations: List[str] # A list of operations to include in the OpenAPI client. | ||
|
||
|
||
def _create_tool_from_openapi_spec(spec: Union[str, Path], operation_name: str, **kwargs) -> "Tool": | ||
""" | ||
Create a Tool instance from an OpenAPI specification and a specific operation name. | ||
|
||
:param spec: OpenAPI specification as URL, file path, or string content. | ||
:param operation_name: Name of the operation to create a tool for. | ||
:param kwargs: Additional configuration options for the OpenAPI client: | ||
- credentials: API credentials (e.g., API key, auth token). | ||
- request_sender: Custom callable to send HTTP requests. | ||
:returns: Tool instance for the specified operation. | ||
:raises ValueError: If the OpenAPI specification is invalid or cannot be loaded. | ||
""" | ||
# Create a config dictionary with the necessary parameters | ||
config: Dict[str, Any] = { | ||
"allowed_operations": [operation_name], | ||
"request_sender": kwargs.get("request_sender"), | ||
"credentials": kwargs.get("credentials"), | ||
} | ||
# Remove None values from the dictionary | ||
config = {k: v for k, v in config.items() if v is not None} | ||
|
||
tools = _create_tools_from_openapi_spec(spec=spec, **config) | ||
|
||
# Ensure we have only one tool | ||
if len(tools) != 1: | ||
msg = ( | ||
f"Couldn't create a tool from OpenAPI spec '{spec}' for operation '{operation_name}'. " | ||
"Please check that the operation name is correct and that the OpenAPI spec is valid." | ||
) | ||
raise ValueError(msg) | ||
|
||
return tools[0] | ||
|
||
|
||
def _create_tools_from_openapi_spec(spec: Union[str, Path], **kwargs: OpenAPIKwargs) -> List["Tool"]: | ||
""" | ||
Create Tool instances from an OpenAPI specification. | ||
|
||
The specification can be provided as: | ||
- A URL pointing to an OpenAPI spec | ||
- A local file path to an OpenAPI spec (JSON or YAML) | ||
- A string containing the OpenAPI spec content (JSON or YAML) | ||
|
||
:param spec: OpenAPI specification as URL, file path, or string content | ||
:param kwargs: Additional configuration options for the OpenAPI client: | ||
- credentials: API credentials (e.g., API key, auth token) | ||
- request_sender: Custom callable to send HTTP requests | ||
- allowed_operations: List of operations from the OpenAPI spec to include | ||
:returns: List of Tool instances configured to invoke the OpenAPI service endpoints | ||
:raises ValueError: If the OpenAPI specification is not valid or the operation name is not found | ||
""" | ||
openapi_llm_import.check() | ||
# Load the OpenAPI specification | ||
if isinstance(spec, str): | ||
if spec.startswith(("http://", "https://")): | ||
openapi_spec = OpenAPISpecification.from_url(spec) | ||
elif Path(spec).exists(): | ||
openapi_spec = OpenAPISpecification.from_file(spec) | ||
else: | ||
openapi_spec = OpenAPISpecification.from_str(spec) | ||
elif isinstance(spec, Path): | ||
openapi_spec = OpenAPISpecification.from_file(str(spec)) | ||
else: | ||
raise ValueError("OpenAPI spec must be a string (URL, file path, or content) or a Path object") | ||
|
||
# Create client configuration | ||
config = ClientConfig(openapi_spec=openapi_spec, **kwargs) | ||
|
||
# Create an OpenAPI client for invocations | ||
client = OpenAPIClient(config) | ||
|
||
# Get all tool definitions from the config | ||
tools = [] | ||
for llm_specific_tool_def in config.get_tool_definitions(): | ||
# Extract normalized tool definition | ||
standardized_tool_def = _standardize_tool_definition(llm_specific_tool_def) | ||
if not standardized_tool_def: | ||
logger.warning(f"Skipping {llm_specific_tool_def}, as required parameters not found") | ||
continue | ||
|
||
# Create a closure that captures the current value of standardized_tool_def | ||
def create_invoke_function(tool_def: Dict[str, Any]) -> Callable: | ||
""" | ||
Create an invoke function with the tool definition bound to its scope. | ||
|
||
:param tool_def: The tool definition to bind to the invoke function. | ||
:returns: Function that invokes the OpenAPI endpoint. | ||
""" | ||
|
||
def invoke_openapi(**kwargs): | ||
""" | ||
Invoke the OpenAPI endpoint with the provided arguments. | ||
|
||
:param kwargs: Arguments to pass to the OpenAPI endpoint. | ||
:returns: Response from the OpenAPI endpoint. | ||
""" | ||
return client.invoke({"name": tool_def["name"], "arguments": kwargs}) | ||
|
||
return invoke_openapi | ||
|
||
tools.append( | ||
Tool( | ||
name=standardized_tool_def["name"], | ||
description=standardized_tool_def["description"], | ||
parameters=standardized_tool_def["parameters"], | ||
function=create_invoke_function(standardized_tool_def), | ||
) | ||
) | ||
|
||
return tools | ||
|
||
|
||
def _standardize_tool_definition(llm_specific_tool_def: Dict[str, Any]) -> Optional[Dict[str, Any]]: | ||
""" | ||
Recursively extract tool parameters from different LLM provider formats. | ||
|
||
Supports various LLM provider formats including OpenAI, Anthropic, and Cohere. | ||
|
||
:param llm_specific_tool_def: Dictionary containing tool definition in provider-specific format | ||
:returns: Dictionary with normalized tool parameters or None if required fields not found | ||
""" | ||
# Mapping of provider-specific schema field names to our Tool "parameters" field | ||
SCHEMA_FIELD_NAMES = [ | ||
"parameters", # Cohere/OpenAI | ||
"input_schema", # Anthropic | ||
# any other field names that might contain a schema in other providers | ||
] | ||
|
||
def _find_in_dict(d: Dict[str, Any]) -> Optional[Dict[str, Any]]: | ||
if all(k in d for k in ["name", "description"]): | ||
schema = None | ||
for field_name in SCHEMA_FIELD_NAMES: | ||
if field_name in d: | ||
schema = d[field_name] | ||
break | ||
|
||
if schema is not None: | ||
return { | ||
"name": d["name"], | ||
"description": d["description"], | ||
"parameters": schema, | ||
} | ||
|
||
# Recurse into nested dictionaries | ||
for v in d.values(): | ||
if isinstance(v, dict): | ||
result = _find_in_dict(v) | ||
if result: | ||
return result | ||
return None | ||
|
||
return _find_in_dict(llm_specific_tool_def) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circular dep between these two otherwise