Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Adding ChatGPT plugin tool #405

Merged
merged 2 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions llama_hub/tools/chatgpt_plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ChatGPT Plugin Tool

This tool allows Agents to load a plugin using a ChatGPT manifest file, and have the Agent interact with it.

## Usage

This tool has more extensive example usage documented in a Jupyter notebook [here](https://github.com/emptycrown/llama-hub/tree/main/llama_hub/tools/notebooks/chatgpt_plugin.ipynb)

```python
# Load the manifest
import requests
import yaml
f = requests.get('https://raw.githubusercontent.com/sisbell/chatgpt-plugin-store/main/manifests/today-currency-converter.oiconma.repl.co.json').text
manifest = yaml.load(f, Loader=yaml.Loader)

from llama_hub.tools.chatgpt_plugin.base import ChatGPTPluginToolSpec
from llama_index.agent import OpenAIAgent
from llama_hub.tools.requests.base import RequestsToolSpec

requests_spec = RequestsToolSpec()
plugin_spec = ChatGPTPluginToolSpec(manifest)
# OR
plugin_spec = ChatGPTPluginToolSpec(manifest_url='https://raw.githubusercontent.com/sisbell/chatgpt-plugin-store/main/manifests/today-currency-converter.oiconma.repl.co.json')

agent = OpenAIAgent.from_tools([*plugin_spec.to_tool_list(), *requests_spec.to_tool_list()], verbose=True)
print(agent.chat("Convert 100 euros to CAD"))
```

`describe_plugin`: Describe the plugin that has been loaded.
`load_openapi_spec`: Returns the parsed OpenAPI spec that the class was initialized with

In addition to the above method, this tool makes all of the tools available from the OpenAPI Tool Spec and Requests Tool Spec available to the agent. The plugin OpenAPI defintion is loaded into the OpenAPI tool spec, and authentication headers are passed in to the Requests tool spec


This loader is designed to be used as a way to load data as a Tool in a Agent. See [here](https://github.com/emptycrown/llama-hub/tree/main) for examples.
1 change: 1 addition & 0 deletions llama_hub/tools/chatgpt_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""init.py"""
67 changes: 67 additions & 0 deletions llama_hub/tools/chatgpt_plugin/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""ChatGPT Plugiun Tool."""

from typing import List, Optional

from llama_index.readers.schema.base import Document
from llama_index.tools.tool_spec.base import BaseToolSpec
from llama_hub.tools.openapi.base import OpenAPIToolSpec
import requests
import yaml

class ChatGPTPluginToolSpec(BaseToolSpec):
"""ChatGPT Plugin Tool

This tool leverages the OpenAPI tool spec to automatically load ChatGPT
plugins from a manifest file.
You should also provide the Requests tool spec to allow the Agent to make calls to the OpenAPI endpoints
To use endpoints with authorization, use the Requests tool spec with the authorization headers
"""

spec_functions = ['load_openapi_spec', 'describe_plugin']

def __init__(self, manifest: Optional[dict] = None, manifest_url: Optional[str] = None):
if manifest and manifest_url:
raise ValueError('You cannot provide both a manifest and a manifest_url')
elif manifest:
pass
elif manifest_url:
response = requests.get(manifest_url).text
manifest = yaml.load(response, Loader=yaml.Loader)
else:
raise ValueError('You must provide either a manifest or a manifest_url')

if manifest['api']['type'] != 'openapi':
raise ValueError(f'API type must be "openapi", not "{manifest["api"]["type"]}"')

if manifest['auth']['type'] != 'none':
raise ValueError(f'Authentication cannot be supported for ChatGPT plugins')

self.openapi = OpenAPIToolSpec(url=manifest['api']['url'])

self.plugin_description = f"""
'human_description': {manifest['description_for_human']}
'model_description': {manifest['description_for_model']}
"""

def load_openapi_spec(self) -> List[Document]:
"""
You are an AI agent specifically designed to retrieve information by making web requests to an API based on an OpenAPI specification.

Here's a step-by-step guide to assist you in answering questions:

1. Determine the base URL required for making the request

2. Identify the relevant paths necessary to address the question

3. Find the required parameters for making the request

4. Perform the necessary requests to obtain the answer

Returns:
Document: A List of Document objects describing the OpenAPI spec
"""
return self.openapi.load_openapi_spec()

def describe_plugin(self) -> List[Document]:
return self.plugin_description

8 changes: 8 additions & 0 deletions llama_hub/tools/library.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"ChatGPTPluginToolSpec": {
"id": "tools/chatgpt_plugin",
"author": "ajhofmann"
},
"CodeInterpreterToolSpec": {
"id": "tools/code_interpreter",
"author": "ajhofmann"
Expand Down Expand Up @@ -41,6 +45,10 @@
"id": "tools/slack",
"author": "jerryjliu"
},
"TextToImageToolSpec": {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag along fix because I missed adding this in the initial text to image PR

"id": "tools/text_to_image",
"author": "ajhofmann"
},
"VectorDBToolSpec": {
"id": "tools/vector_db",
"author": "jerryjliu"
Expand Down
138 changes: 138 additions & 0 deletions llama_hub/tools/notebooks/chatgpt_plugin.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "5f976f46-9d51-407e-8eeb-e46cfdd22ae1",
"metadata": {},
"outputs": [],
"source": [
"import openai\n",
"openai.api_key = 'sk-your-key'\n",
"from llama_index.agent import OpenAIAgent"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "538e1519-2cfe-40f4-a861-65139156d2fe",
"metadata": {},
"outputs": [],
"source": [
"# Load the Plugin\n",
"import requests\n",
"import yaml\n",
"f = requests.get('https://raw.githubusercontent.com/sisbell/chatgpt-plugin-store/main/manifests/today-currency-converter.oiconma.repl.co.json').text\n",
"manifest = yaml.load(f, Loader=yaml.Loader)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "b988f94e-6559-4404-9ba8-b41d4e4da770",
"metadata": {},
"outputs": [],
"source": [
"from llama_hub.tools.chatgpt_plugin.base import ChatGPTPluginToolSpec\n",
"from llama_hub.tools.requests.base import RequestsToolSpec\n",
"\n",
"requests_spec = RequestsToolSpec()\n",
"plugin_spec = ChatGPTPluginToolSpec(manifest)\n",
"# OR\n",
"plugin_spec = ChatGPTPluginToolSpec(manifest_url='https://raw.githubusercontent.com/sisbell/chatgpt-plugin-store/main/manifests/today-currency-converter.oiconma.repl.co.json')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "475cd741-e53e-4e9c-857b-6cb52641af0d",
"metadata": {},
"outputs": [],
"source": [
"agent = OpenAIAgent.from_tools([*plugin_spec.to_tool_list(), *requests_spec.to_tool_list()], verbose=True)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "88175b8a-0345-44d2-8b78-983d7e7415bb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== Calling Function ===\n",
"Calling function: describe_plugin with args: {}\n",
"Got output: \n",
" 'human_description': Converts currency values based on the latest exchange rates.\n",
" 'model_description': Converts currency values based on the latest exchange rates.\n",
" \n",
"========================\n",
"The OpenAPI plugin that was loaded is a plugin that converts currency values based on the latest exchange rates. It provides functionality to retrieve exchange rates and perform currency conversions.\n"
]
}
],
"source": [
"print(agent.chat(\"Can you give me info on the OpenAPI plugin that was loaded\"))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "39e42a58-a6db-47f0-8bd7-a4c3728f0839",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== Calling Function ===\n",
"Calling function: load_openapi_spec with args: {}\n",
"Got output: [Document(id_='bd92f5ee-3c31-4938-a75f-004d6e451181', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, hash='b43045dea83f8c9bbefe5516329054c5ec962524078a7c9f6055af71666dd4bb', text=\"{'servers': [{'url': 'https://today-currency-converter.oiconma.repl.co/'}], 'description': 'Allows users to convert currency values based on the latest exchange rates.', 'endpoints': [('GET /currency-converter', None, {'parameters': [{'name': 'from', 'in': 'query', 'description': 'The currency to convert from', 'required': True, 'schema': {'type': 'string'}}, {'name': 'to', 'in': 'query', 'description': 'The currency to convert to', 'required': True, 'schema': {'type': 'string'}}, {'name': 'amount', 'in': 'query', 'description': 'The amount to convert', 'required': True, 'schema': {'type': 'string'}}], 'responses': {'description': 'OK', 'content': {'application/json': {'schema': {'type': 'object', 'properties': {'result': {'type': 'object', 'properties': {'info': {'type': 'object', 'properties': {'rate': {'type': 'number', 'format': 'float', 'description': 'The conversion rate between the two currencies'}}}, 'date': {'type': 'string', 'description': 'The date of the conversion'}, 'result': {'type': 'number', 'format': 'float', 'description': 'The converted amount'}}}}}}}}})]}\", start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\\n\\n{content}', metadata_template='{key}: {value}', metadata_seperator='\\n')]\n",
"========================\n",
"=== Calling Function ===\n",
"Calling function: get_request with args: {\n",
" \"url\": \"https://today-currency-converter.oiconma.repl.co/currency-converter?from=EUR&to=CAD&amount=100\"\n",
"}\n",
"Got output: {'date': '2023-07-24', 'historical': False, 'info': {'rate': 1.469716}, 'motd': {'msg': 'If you or your company use this project or like what we doing, please consider backing us so we can continue maintaining and evolving this project.', 'url': 'https://exchangerate.host/#/donate'}, 'query': {'amount': 100, 'from': 'EUR', 'to': 'CAD'}, 'result': 146.971621, 'success': True}\n",
"========================\n",
"The conversion of 100 euros to CAD is 146.971621 CAD.\n"
]
}
],
"source": [
"print(agent.chat(\"Can you convert 100 euros to CAD\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b11e0621-b486-4f03-8c91-b16029a928c3",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
5 changes: 4 additions & 1 deletion llama_hub/tools/notebooks/openapi_and_requests.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"from llama_index.tools.tool_spec.load_and_search.base import LoadAndSearchToolSpec\n",
"\n",
"open_spec = OpenAPIToolSpec(open_api_spec)\n",
"# OR \n",
"open_spec = OpenAPIToolSpec(url='https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/openai.com/1.2.0/openapi.yaml')\n",
"\n",
"requests_spec = RequestsToolSpec({\n",
" \"Authorization\": \"Bearer sk-your-key\",\n",
" \"Content-Type\": \"application/json\",\n",
Expand Down Expand Up @@ -220,7 +223,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.10"
"version": "3.9.12"
}
},
"nbformat": 4,
Expand Down
3 changes: 3 additions & 0 deletions llama_hub/tools/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ from llama_index.agent import OpenAIAgent

f = requests.get('https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/openai.com/1.2.0/openapi.yaml').text
open_api_spec = yaml.load(f, Loader=yaml.Loader)
# OR
open_spec = OpenAPIToolSpec(url='https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/openai.com/1.2.0/openapi.yaml')


tool_spec = OpenAPIToolSpec(open_api_spec)

Expand Down
20 changes: 16 additions & 4 deletions llama_hub/tools/openapi/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""OpenAPI Tool."""

from typing import List
from typing import List, Optional

from llama_index.readers.schema.base import Document
from llama_index.tools.tool_spec.base import BaseToolSpec

import requests
import yaml

class OpenAPIToolSpec(BaseToolSpec):
"""OpenAPI Tool
Expand All @@ -15,8 +16,19 @@ class OpenAPIToolSpec(BaseToolSpec):

spec_functions = ["load_openapi_spec"]

def __init__(self, spec: dict):
self.spec = Document(text=str(self.process_api_spec(spec)))
def __init__(self, spec: Optional[dict] = None, url: Optional[str] = None):
if spec and url:
raise ValueError("Only provide one of OpenAPI dict or url")
elif spec:
pass
elif url:
response = requests.get(url).text
spec = yaml.load(response, Loader=yaml.Loader)
else:
raise ValueError("You must provide a url or OpenAPI spec as a dict")

parsed_spec = self.process_api_spec(spec)
self.spec = Document(text=str(parsed_spec))

def load_openapi_spec(self) -> List[Document]:
"""
Expand Down