├── .gitignore ├── README.md ├── chatbot.py ├── openai_decorator ├── __init__.py └── openai_decorator.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPT-4 Function Python Decorator 2 | 3 | This Python package creates a function decorator `@openaifunc` which can be used to automatically generate the `functions` parameter for the ChatGPT API. 4 | 5 | The original code was generated [with GPT-4](https://chat.openai.com/share/d32b7a1c-1b5d-4d2f-895e-edc2e6576164). I'm new to Python and have never created a Python package. 6 | 7 | Inspired by @memespdf on @sentdex [YouTube-video](https://www.youtube.com/watch?v=0lOSvOoF2to) comments 8 | 9 | ## How to use 10 | 11 | First, import the package at the top of your Python code: 12 | ```python 13 | from openai_decorator.openai_decorator import openaifunc, get_openai_funcs 14 | ``` 15 | 16 | Then, add a `@openaifunc` decorator to the functions you want to use with ChatGPT: 17 | ```python 18 | @openaifunc 19 | def add_numbers(a: int, b: int): 20 | """ 21 | This function adds two numbers. 22 | """ 23 | return a + b 24 | ``` 25 | 26 | Then, you can get a list of all the functions and their definitions for ChatGPT with `get_openai_funcs()` like so: 27 | 28 | ```python 29 | response = openai.ChatCompletion.create( 30 | model="gpt-4-0613", 31 | messages=messages, 32 | functions=get_openai_funcs(), 33 | function_call="auto", 34 | ) 35 | ``` 36 | 37 | ## Parameter descriptions 38 | 39 | As far as I know, there is no "official" way to add docstrings for parameters in Python, but you can add the parameter definitions to the docstring in PHP DocBlock style, and GPT-4 seems to obey them. 40 | 41 | ```python 42 | @openaifunc 43 | def get_current_weather(location: str, country: str) -> str: 44 | """ 45 | Gets the current weather information 46 | @param location: The location for which to get the weather 47 | @param country: The country in which to look for the location 48 | """ 49 | 50 | if location is None: 51 | return "A location must be provided. Please ask the user which location they want the weather for" 52 | else: 53 | return "The weather is nice and sunny" 54 | ``` 55 | 56 | Currently, this will not populate the `description` of the parameters in the API request, but GPT-4 still adheres to the rules. 57 | 58 | ## Pydantic Models 59 | 60 | You can also set descriptions for the function parameters with Pydantic models. This will actually populate the `description` of the parameters in the API request. 61 | 62 | ```python 63 | from pydantic import BaseModel, Field 64 | 65 | class LocationModel(BaseModel): 66 | location: str = Field( 67 | description="The location for which to get the weather" 68 | ) 69 | country: str = Field( 70 | description="The country in which to look for the location" 71 | ) 72 | 73 | @openaifunc 74 | def get_current_weather(location: LocationModel) -> str: 75 | """ 76 | Gets the current weather information 77 | """ 78 | location = LocationModel.parse_obj(location) 79 | 80 | if location is None: 81 | return "A location must be provided. Please ask the user which location they want the weather for" 82 | else: 83 | return "The weather is nice and sunny" 84 | ``` 85 | 86 | ## Chatbot 87 | 88 | There's a demo chatbot that uses the GPT-4 API with function calling. You can run it by exporting your OpenAI API key first: 89 | 90 | ```console 91 | $ export OPENAI_API_KEY=YOUR_API_KEY 92 | ``` 93 | 94 | And then running the script: 95 | ```console 96 | $ ./chatbot.py 97 | ``` 98 | 99 | You can test it by asking it about the weather, some YouTube channel recommendations or to calculate the length of a string. 100 | 101 | You can also modify the functions in the chatbot code, to test your own functions. 102 | -------------------------------------------------------------------------------- /chatbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from openai_decorator.openai_decorator import openaifunc, get_openai_funcs 3 | 4 | import openai 5 | import os 6 | import sys 7 | import json 8 | 9 | openai.api_key = os.getenv("OPENAI_API_KEY") 10 | 11 | @openaifunc 12 | def get_current_weather(location: str, country: str) -> str: 13 | """ 14 | Gets the current weather information 15 | @param location: The location for which to get the weather 16 | @param country: The ISO 3166-1 alpha-2 country code 17 | """ 18 | 19 | if country == "FR": 20 | return "The weather is terrible, as always" 21 | elif location == "California": 22 | return "The weather is nice and sunny" 23 | else: 24 | return "It's rainy and windy" 25 | 26 | @openaifunc 27 | def recommend_youtube_channel() -> str: 28 | """ 29 | Gets a really good recommendation for a YouTube channel to watch 30 | """ 31 | return "Unconventional Coding" 32 | 33 | @openaifunc 34 | def calculate_str_length(string: str) -> str: 35 | """ 36 | Calculates the length of a string 37 | """ 38 | return str(len(string)) 39 | 40 | # ChatGPT API Function 41 | def send_message(message, messages): 42 | # add user message to message list 43 | messages.append(message) 44 | 45 | try: 46 | # send prompt to chatgpt 47 | response = openai.ChatCompletion.create( 48 | # model="gpt-4-0613", 49 | model="gpt-3.5-turbo-0613", 50 | messages=messages, 51 | functions=get_openai_funcs(), 52 | function_call="auto", 53 | ) 54 | except openai.error.AuthenticationError: 55 | print("AuthenticationError: Check your API-key") 56 | sys.exit(1) 57 | 58 | # add response to message list 59 | messages.append(response["choices"][0]["message"]) 60 | 61 | return messages 62 | 63 | # MAIN FUNCTION 64 | def run_conversation(prompt, messages=[]): 65 | # add user prompt to chatgpt messages 66 | messages = send_message({"role": "user", "content": prompt}, messages) 67 | 68 | # get chatgpt response 69 | message = messages[-1] 70 | 71 | # loop until project is finished 72 | while True: 73 | if message.get("function_call"): 74 | # get function name and arguments 75 | function_name = message["function_call"]["name"] 76 | arguments = json.loads(message["function_call"]["arguments"]) 77 | 78 | # call function dangerously 79 | function_response = globals()[function_name](**arguments) 80 | 81 | # send function result to chatgpt 82 | messages = send_message( 83 | { 84 | "role": "function", 85 | "name": function_name, 86 | "content": function_response, 87 | }, 88 | messages, 89 | ) 90 | else: 91 | # if chatgpt doesn't respond with a function call, ask user for input 92 | print("ChatGPT: " + message["content"]) 93 | 94 | user_message = input("You: ") 95 | 96 | # send user message to chatgpt 97 | messages = send_message( 98 | { 99 | "role": "user", 100 | "content": user_message, 101 | }, 102 | messages, 103 | ) 104 | 105 | # save last response for the while loop 106 | message = messages[-1] 107 | 108 | # ASK FOR PROMPT 109 | print( 110 | "Go ahead, ask for the weather, a YouTube channel recommendation or to calculate the length of a string!" 111 | ) 112 | prompt = input("You: ") 113 | 114 | # RUN CONVERSATION 115 | run_conversation(prompt) 116 | -------------------------------------------------------------------------------- /openai_decorator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/gpt-pydecorator/e61e6eeaffb826b064ebde4c47e8c8c41e813c71/openai_decorator/__init__.py -------------------------------------------------------------------------------- /openai_decorator/openai_decorator.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import functools 3 | import importlib.util 4 | 5 | # Global variable to store all functions decorated with @openaifunc 6 | openai_functions = [] 7 | 8 | # Map python types to JSON schema types 9 | type_mapping = { 10 | "int": "integer", 11 | "float": "number", 12 | "str": "string", 13 | "bool": "boolean", 14 | "list": "array", 15 | "tuple": "array", 16 | "dict": "object", 17 | "None": "null", 18 | } 19 | 20 | def get_type_mapping(param_type): 21 | param_type = param_type.replace("", '') 23 | return type_mapping.get(param_type, "string") 24 | 25 | def get_params_dict(params): 26 | params_dict = {} 27 | # Add optional pydantic support 28 | pydantic_found = importlib.util.find_spec("pydantic") 29 | if pydantic_found: 30 | from pydantic import BaseModel 31 | for k, v in params.items(): 32 | if pydantic_found: 33 | if issubclass(v.annotation, BaseModel): 34 | # Consider BaseModel fields as dictionaries 35 | params_dict[k] = { 36 | "type": "object", 37 | "properties": { 38 | field_name: { 39 | "type": property.get("type", "unknown"), 40 | "description": property.get("description", ""), 41 | } 42 | for field_name, property in v.annotation.schema()[ 43 | "properties" 44 | ].items() 45 | }, 46 | } 47 | continue 48 | else: 49 | annotation = str(v.annotation).split("[") 50 | 51 | try: 52 | param_type = annotation[0] 53 | except IndexError: 54 | param_type = "string" 55 | 56 | try: 57 | array_type = annotation[1].strip("]") 58 | except IndexError: 59 | array_type = "string" 60 | 61 | param_type = get_type_mapping(param_type) 62 | params_dict[k] = { 63 | "type": param_type, 64 | "description": "", 65 | } 66 | 67 | if param_type == "array": 68 | if "," in array_type: 69 | array_types = array_type.split(", ") 70 | params_dict[k]["prefixItems"] = [] 71 | for i, array_type in enumerate(array_types): 72 | array_type = get_type_mapping(array_type) 73 | params_dict[k]["prefixItems"].append({ 74 | "type": array_type, 75 | }) 76 | else: 77 | array_type = get_type_mapping(array_type) 78 | params_dict[k]["items"] = { 79 | "type": array_type, 80 | } 81 | return params_dict 82 | 83 | def openaifunc(func): 84 | @functools.wraps(func) 85 | def wrapper(*args, **kwargs): 86 | return func(*args, **kwargs) 87 | 88 | # Get information about function parameters 89 | params = inspect.signature(func).parameters 90 | 91 | param_dict = get_params_dict(params) 92 | 93 | openai_functions.append( 94 | { 95 | "name": func.__name__, 96 | "description": inspect.cleandoc(func.__doc__ or ""), 97 | "parameters": { 98 | "type": "object", 99 | "properties": param_dict, 100 | "required": list(param_dict.keys()), 101 | }, 102 | } 103 | ) 104 | 105 | return wrapper 106 | 107 | def get_openai_funcs(): 108 | return openai_functions 109 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TEST FOR GPT-PYDECORATOR USAGE: 4 | # 5 | # Test format of get_openai_funcs(): 6 | # ./test.py 7 | # 8 | # Test response from ChatGPT: 9 | # ./test.py api 10 | 11 | from openai_decorator.openai_decorator import openaifunc, get_openai_funcs 12 | 13 | import json 14 | import sys 15 | 16 | @openaifunc 17 | def add_numbers(a: int, b: int): 18 | """ 19 | This function adds two numbers. 20 | """ 21 | return a + b 22 | 23 | @openaifunc 24 | def say_hello(name: str): 25 | """ 26 | This function greets the user. 27 | """ 28 | return f"Hello, {name}!" 29 | 30 | @openaifunc 31 | def save_numbers(count: int, numbers: tuple[int]): 32 | """ 33 | Save some numbers to the database. Set count to the number of numbers the user asked for and numbers to the list of actual numbers 34 | @param count: Number of numbers to save 35 | @param numbers: The numbers 36 | """ 37 | return (count, numbers) 38 | 39 | @openaifunc 40 | def list_synonyms(synonyms: list[str]): 41 | """ 42 | Show a list of synonyms to the user 43 | """ 44 | return synonyms 45 | 46 | # OpenAI API doesn't seem to support this yet 47 | # @openaifunc 48 | # def triplet_test(triplet: tuple[int, str, float]): 49 | # """ 50 | # Test a multi-type tuple 51 | # """ 52 | # return triplet 53 | 54 | # OpenAI API doesn't seem to support this yet 55 | # @openaifunc 56 | # def triple_list_test(triple_list: list[int, str, float]): 57 | # """ 58 | # Test a multi-type list 59 | # """ 60 | # return triple_list 61 | 62 | funcs = get_openai_funcs() 63 | print(json.dumps(funcs, indent=4)) 64 | 65 | expected = [ 66 | { 67 | "name": "add_numbers", 68 | "description": "This function adds two numbers.", 69 | "parameters": { 70 | "type": "object", 71 | "properties": { 72 | "a": { 73 | "type": "integer", 74 | "description": "" 75 | }, 76 | "b": { 77 | "type": "integer", 78 | "description": "" 79 | } 80 | }, 81 | "required": [ 82 | "a", 83 | "b" 84 | ] 85 | } 86 | }, 87 | { 88 | "name": "say_hello", 89 | "description": "This function greets the user.", 90 | "parameters": { 91 | "type": "object", 92 | "properties": { 93 | "name": { 94 | "type": "string", 95 | "description": "" 96 | } 97 | }, 98 | "required": [ 99 | "name" 100 | ] 101 | } 102 | }, 103 | { 104 | "name": "save_numbers", 105 | "description": "Save some numbers to the database. Set count to the number of numbers the user asked for and numbers to the list of actual numbers\n@param count: Number of numbers to save\n@param numbers: The numbers", 106 | "parameters": { 107 | "type": "object", 108 | "properties": { 109 | "count": { 110 | "type": "integer", 111 | "description": "" 112 | }, 113 | "numbers": { 114 | "type": "array", 115 | "description": "", 116 | "items": { 117 | "type": "integer" 118 | } 119 | } 120 | }, 121 | "required": [ 122 | "count", 123 | "numbers" 124 | ] 125 | } 126 | }, 127 | { 128 | "name": "list_synonyms", 129 | "description": "Show a list of synonyms to the user", 130 | "parameters": { 131 | "type": "object", 132 | "properties": { 133 | "synonyms": { 134 | "type": "array", 135 | "description": "", 136 | "items": { 137 | "type": "string" 138 | } 139 | } 140 | }, 141 | "required": [ 142 | "synonyms" 143 | ] 144 | } 145 | } 146 | ] 147 | 148 | if expected != funcs: 149 | print("Test failed!") 150 | sys.exit(1) 151 | 152 | if len(sys.argv) > 1: 153 | if sys.argv[1] != "api": 154 | print(f"ERROR: Invalid argument '{sys.argv[1]}'") 155 | sys.exit(1) 156 | 157 | import openai 158 | import os 159 | import re 160 | 161 | openai.api_key = os.getenv("OPENAI_API_KEY") 162 | 163 | test_messages = [ 164 | { 165 | "function": "list_synonyms", 166 | "regex": r'\{\s*"synonyms"\s*:\s*\[\s*"(.*?)",\s*"(.*?)",\s*"(.*?)",\s*"(.*?)",\s*"(.*?)"\s*\]\s*\}', 167 | "message": { 168 | "role": "user", 169 | "content": "Give me 5 synonyms for 'amazing'" 170 | } 171 | }, 172 | { 173 | "function": "save_numbers", 174 | "regex": r'\{\s*"count":\s*4,\s*"numbers":\s*\[0,\s*1,\s*1,\s*2\]\n\}', 175 | "message": { 176 | "role": "user", 177 | "content": "Save 4 fibonacci numbers to the database" 178 | } 179 | }, 180 | { 181 | "function": "add_numbers", 182 | "regex": r'\{\s*"a":\s*42069420,\s*"b":\s*6969420\s*\}', 183 | "message": { 184 | "role": "user", 185 | "content": "What is 42069420 + 6969420?" 186 | } 187 | }, 188 | ] 189 | 190 | for test in test_messages: 191 | response = openai.ChatCompletion.create( 192 | # model="gpt-4-0613", 193 | model="gpt-3.5-turbo-0613", 194 | messages=[test["message"]], 195 | functions=get_openai_funcs(), 196 | function_call="auto", 197 | ) 198 | 199 | print(response) 200 | 201 | if not re.search(test["regex"], response["choices"][0]["message"]["function_call"]["arguments"]): 202 | print("Test failed!") 203 | sys.exit(1) 204 | 205 | print("Test passed!") 206 | sys.exit(0) 207 | --------------------------------------------------------------------------------