haystack/test/components/converters/test_openapi_functions.py
2025-05-26 16:22:51 +00:00

261 lines
11 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import json
import sys
import tempfile
import pytest
from haystack.components.converters import OpenAPIServiceToFunctions
from haystack.dataclasses import ByteStream
@pytest.fixture
def json_serperdev_openapi_spec():
serper_spec = """
{
"openapi": "3.0.0",
"info": {
"title": "SerperDev",
"version": "1.0.0",
"description": "API for performing search queries"
},
"servers": [
{
"url": "https://google.serper.dev"
}
],
"paths": {
"/search": {
"post": {
"operationId": "search",
"description": "Search the web with Google",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"q": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"searchParameters": {
"type": "undefined"
},
"knowledgeGraph": {
"type": "undefined"
},
"answerBox": {
"type": "undefined"
},
"organic": {
"type": "undefined"
},
"topStories": {
"type": "undefined"
},
"peopleAlsoAsk": {
"type": "undefined"
},
"relatedSearches": {
"type": "undefined"
}
}
}
}
}
}
},
"security": [
{
"apikey": []
}
]
}
}
},
"components": {
"securitySchemes": {
"apikey": {
"type": "apiKey",
"name": "x-api-key",
"in": "header"
}
}
}
}
"""
return serper_spec
@pytest.fixture
def yaml_serperdev_openapi_spec():
serper_spec = """
openapi: 3.0.0
info:
title: SerperDev
version: 1.0.0
description: API for performing search queries
servers:
- url: 'https://google.serper.dev'
paths:
/search:
post:
operationId: search
description: Search the web with Google
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
q:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
searchParameters:
type: undefined
knowledgeGraph:
type: undefined
answerBox:
type: undefined
organic:
type: undefined
topStories:
type: undefined
peopleAlsoAsk:
type: undefined
relatedSearches:
type: undefined
security:
- apikey: []
components:
securitySchemes:
apikey:
type: apiKey
name: x-api-key
in: header
"""
return serper_spec
class TestOpenAPIServiceToFunctions:
# test we can parse openapi spec given in json
def test_openapi_spec_parsing_json(self, json_serperdev_openapi_spec):
service = OpenAPIServiceToFunctions()
serper_spec_json = service._parse_openapi_spec(json_serperdev_openapi_spec)
assert serper_spec_json["openapi"] == "3.0.0"
assert serper_spec_json["info"]["title"] == "SerperDev"
# test we can parse openapi spec given in yaml
def test_openapi_spec_parsing_yaml(self, yaml_serperdev_openapi_spec):
service = OpenAPIServiceToFunctions()
serper_spec_yaml = service._parse_openapi_spec(yaml_serperdev_openapi_spec)
assert serper_spec_yaml["openapi"] == "3.0.0"
assert serper_spec_yaml["info"]["title"] == "SerperDev"
# test we can extract functions from openapi spec given
def test_run_with_bytestream_source(self, json_serperdev_openapi_spec):
service = OpenAPIServiceToFunctions()
spec_stream = ByteStream.from_string(json_serperdev_openapi_spec)
result = service.run(sources=[spec_stream])
assert len(result["functions"]) == 1
fc = result["functions"][0]
# check that fc definition is as expected
assert fc == {
"name": "search",
"description": "Search the web with Google",
"parameters": {"type": "object", "properties": {"q": {"type": "string"}}},
}
@pytest.mark.skipif(
sys.platform in ["win32", "cygwin"],
reason="Can't run on Windows Github CI, need access temp file but windows does not allow it",
)
def test_run_with_file_source(self, json_serperdev_openapi_spec):
# test we can extract functions from openapi spec given in file
service = OpenAPIServiceToFunctions()
# write the spec to NamedTemporaryFile and check that it is parsed correctly
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(json_serperdev_openapi_spec.encode("utf-8"))
tmp.seek(0)
result = service.run(sources=[tmp.name])
assert len(result["functions"]) == 1
fc = result["functions"][0]
# check that fc definition is as expected
assert fc == {
"name": "search",
"description": "Search the web with Google",
"parameters": {"type": "object", "properties": {"q": {"type": "string"}}},
}
def test_run_with_invalid_file_source(self, caplog):
# test invalid source
service = OpenAPIServiceToFunctions()
result = service.run(sources=["invalid_source"])
assert result["functions"] == []
assert "not found" in caplog.text
def test_run_with_invalid_bytestream_source(self, caplog):
# test invalid source
service = OpenAPIServiceToFunctions()
result = service.run(sources=[ByteStream.from_string("")])
assert result["functions"] == []
assert "Invalid OpenAPI specification" in caplog.text
def test_complex_types_conversion(self, test_files_path):
# ensure that complex types from OpenAPI spec are converted to the expected format in OpenAI function calling
service = OpenAPIServiceToFunctions()
result = service.run(sources=[test_files_path / "json" / "complex_types_openapi_service.json"])
assert len(result["functions"]) == 1
with open(test_files_path / "json" / "complex_types_openai_spec.json") as openai_spec_file:
desired_output = json.load(openai_spec_file)
assert result["functions"][0] == desired_output
def test_simple_and_complex_at_once(self, test_files_path, json_serperdev_openapi_spec):
# ensure multiple functions are extracted from multiple paths in OpenAPI spec
service = OpenAPIServiceToFunctions()
sources = [
ByteStream.from_string(json_serperdev_openapi_spec),
test_files_path / "json" / "complex_types_openapi_service.json",
]
result = service.run(sources=sources)
assert len(result["functions"]) == 2
with open(test_files_path / "json" / "complex_types_openai_spec.json") as openai_spec_file:
desired_output = json.load(openai_spec_file)
assert result["functions"][0] == {
"name": "search",
"description": "Search the web with Google",
"parameters": {"type": "object", "properties": {"q": {"type": "string"}}},
}
assert result["functions"][1] == desired_output