├── LICENSE ├── MANIFEST.in ├── README.md ├── captainfunction ├── __init__.py ├── dynamic_loader.py └── functions │ ├── __init__.py │ ├── list_directory_files.py │ ├── web_content_scraper.py │ └── web_search.py ├── poetry.lock ├── pyproject.toml └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yohei Nakajima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include captainfunction/functions *.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to CaptainFunction! 🚀 2 | 3 | CaptainFunction is a dynamic Python library for loading custom functions into OpenAI's powerful AI models. This toolkit makes integrating custom functionalities into OpenAI Assistants both easy and flexible. 4 | 5 | I'm releasing this with two basic functions to start, a web search using Metaphor and a web scrape using BeautifulSoup. Both can be improved, but this way when I improve a function in one place, it improves everywhere I use it. You are more than welcome to contribute! 6 | 7 | ## Getting Started 🌟 8 | 9 | ### Prerequisites 10 | 11 | - Python 3.6 or later. 12 | - Access to OpenAI's API (API key). 13 | - Other API keys depending on the function you load. 14 | 15 | ### Installation 16 | 17 | 1. **Install CaptainFunction:** 18 | ``` 19 | pip install git+https://github.com/yoheinakajima/captainfunction.git 20 | ``` 21 | 22 | *Note, you currently need to separately install required libraries from each function you load. 23 | 24 | ### Contributing Functions 25 | 26 | Contribute your best functions in the `functions` directory. Each function should be in its separate file. The names get_function_schema() and handle_response() should not be changed. For example: 27 | ``` 28 | import os 29 | 30 | def get_function_schema(): 31 | return { 32 | "name": "function_name", 33 | "description": "What your function does.", 34 | "parameters": { 35 | "type": "object", 36 | "properties": { 37 | "argument1": { 38 | "type": "string", 39 | "description": "The first argument." 40 | }, 41 | ... 42 | } 43 | } 44 | } 45 | 46 | def handle_response(arguments): 47 | arguments = json.loads(arguments) 48 | argument1 = arguments["argument1"] 49 | # Your function logic here 50 | return "Function response" 51 | ``` 52 | 53 | ## Using CaptainFunction 🛠️ 54 | 55 | ### Loading Functions into OpenAI Assistant 56 | 57 | First, load your functions using the `load_functions` method. For instance: 58 | ``` 59 | loaded_funcs = load_functions('web_search','web_content_scraper') 60 | ``` 61 | 62 | This loads the functions 'web_search' and 'web_content_scraper' from the `functions` directory. 63 | 64 | ### Integrating with OpenAI Assistant 65 | 66 | Create the function schemas and initialize the OpenAI assistant (this examples is for Assistants API, need to adjust for other endpoints): 67 | ``` 68 | function_schemas = [{"type": "function", "function": func['schema']} for func in loaded_funcs.values()] 69 | 70 | assistant = openai_client.beta.assistants.create( 71 | instructions="Respond to user queries.", 72 | model="gpt-3.5-turbo", 73 | tools=function_schemas 74 | ) 75 | ``` 76 | 77 | ### Handling OpenAI API Calls 78 | 79 | Handle the function calls in your application logic (this examples is for Assistants API, need to adjust for other endpoints): 80 | ``` 81 | # Wait for run to complete 82 | while True: 83 | run_response = openai_client.beta.threads.runs.retrieve( 84 | thread_id=thread.id, 85 | run_id=run.id 86 | ) 87 | if run_response.status == "completed": 88 | break 89 | elif run_response.status == "requires_action": 90 | # Handle function calls 91 | tool_outputs = [] 92 | for tool_call in run_response.required_action.submit_tool_outputs.tool_calls: 93 | function_name = tool_call.function.name 94 | arguments = tool_call.function.arguments 95 | 96 | if function_name in loaded_funcs: 97 | handle_response_func = loaded_funcs[function_name]['handle_response'] 98 | output = handle_response_func(arguments) 99 | tool_outputs.append({ 100 | "tool_call_id": tool_call.id, 101 | "output": output, 102 | }) 103 | 104 | openai_client.beta.threads.runs.submit_tool_outputs( 105 | thread_id=thread.id, 106 | run_id=run.id, 107 | tool_outputs=tool_outputs 108 | ) 109 | pass 110 | time.sleep(1) # Avoid spamming the API too quickly 111 | ``` 112 | 113 | ## Contributing 🤝 114 | 115 | We encourage contributions! Feel free to add new functions or improve existing ones. To contribute: 116 | 117 | - Fork the repository. 118 | - Create a new branch for your contribution. 119 | - Add your new function or improvements. 120 | - Submit a pull request. 121 | 122 | All contributions are welcome (including helping manage any repo, because I'm candidly not great at it). 123 | 124 | ## License 📄 125 | 126 | MIT 127 | -------------------------------------------------------------------------------- /captainfunction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoheinakajima/captainfunction/7a56be921a85cb45dbc5533738bbca6e0716d07e/captainfunction/__init__.py -------------------------------------------------------------------------------- /captainfunction/dynamic_loader.py: -------------------------------------------------------------------------------- 1 | # dynamic_loader.py 2 | 3 | import os 4 | import importlib 5 | import logging 6 | from types import ModuleType 7 | from typing import Dict, Callable 8 | 9 | # Configure basic logging 10 | logging.basicConfig(level=logging.INFO) 11 | 12 | def load_modules_from_dir(directory: str) -> Dict[str, ModuleType]: 13 | """ 14 | Dynamically loads Python modules from a specified directory. 15 | 16 | Args: 17 | directory (str): The directory to load modules from, relative to this file. 18 | 19 | Returns: 20 | Dict[str, ModuleType]: A dictionary of module names to module objects. 21 | """ 22 | modules = {} 23 | # Get the directory of the current file (__file__ is the path to dynamic_loader.py) 24 | current_dir = os.path.dirname(__file__) 25 | full_directory_path = os.path.join(current_dir, directory) 26 | 27 | # Construct the full package path for the functions directory 28 | package_path = f'captainfunction.{directory}'.replace('/', '.') 29 | 30 | for filename in os.listdir(full_directory_path): 31 | if filename.endswith('.py') and not filename.startswith('__'): 32 | module_name = filename[:-3] 33 | try: 34 | # Use the full package path for importing 35 | full_module_path = f"{package_path}.{module_name}" 36 | module = importlib.import_module(full_module_path) 37 | modules[module_name] = module 38 | logging.info(f"Successfully loaded module: {module_name}") 39 | except Exception as e: 40 | logging.error(f"Error loading module {module_name}: {e}") 41 | 42 | return modules 43 | 44 | 45 | class FunctionRegistry: 46 | """ 47 | A registry for dynamically loaded functions. 48 | """ 49 | def __init__(self): 50 | self._functions: Dict[str, Callable] = {} 51 | 52 | def register_function(self, name: str, function: Callable): 53 | """ 54 | Registers a function in the registry. 55 | 56 | Args: 57 | name (str): The name to register the function under. 58 | function (Callable): The function to register. 59 | """ 60 | self._functions[name] = function 61 | logging.info(f"Function registered: {name}") 62 | 63 | def get_function(self, name: str) -> Callable: 64 | """ 65 | Retrieves a function from the registry. 66 | 67 | Args: 68 | name (str): The name of the function to retrieve. 69 | 70 | Returns: 71 | Callable: The retrieved function. 72 | """ 73 | return self._functions.get(name, None) 74 | 75 | def load_functions(*function_names: str) -> Dict[str, Dict]: 76 | """ 77 | Loads specified functions and their schemas from the 'functions' directory. 78 | 79 | Args: 80 | *function_names (str): Names of the modules to load. 81 | 82 | Returns: 83 | Dict[str, Dict]: A dictionary of module names to their 'handle_response' function callables and schemas. 84 | """ 85 | loaded_modules = load_modules_from_dir('functions') 86 | 87 | functions = {} 88 | for module_name, module in loaded_modules.items(): 89 | if hasattr(module, 'handle_response') and hasattr(module, 'get_function_schema'): 90 | functions[module_name] = { 91 | 'handle_response': module.handle_response, 92 | 'schema': module.get_function_schema() 93 | } 94 | else: 95 | logging.warning(f"Module '{module_name}' does not have the required functions.") 96 | 97 | return functions 98 | 99 | -------------------------------------------------------------------------------- /captainfunction/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoheinakajima/captainfunction/7a56be921a85cb45dbc5533738bbca6e0716d07e/captainfunction/functions/__init__.py -------------------------------------------------------------------------------- /captainfunction/functions/list_directory_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def get_function_schema(): 4 | return { 5 | "name": "list_directory_files", 6 | "description": "Outputs the file and folder structure of the top parent folder.", 7 | "parameters": { 8 | "type": "object", 9 | "properties": {} 10 | } 11 | } 12 | 13 | def handle_response(arguments): 14 | # Function to get the top parent path 15 | def get_top_parent_path(current_path): 16 | while True: 17 | new_path = os.path.dirname(current_path) 18 | if new_path == '/home/runner/BabyGhostAPI-1': # Adjust this to your specific path 19 | return new_path 20 | current_path = new_path 21 | 22 | # Function to get directory structure 23 | def get_directory_structure(start_path): 24 | dir_structure = {} 25 | ignore_dirs = ['.', '__pycache__', 'venv'] # Exclude specific directories 26 | 27 | for root, dirs, files in os.walk(start_path): 28 | dirs[:] = [d for d in dirs if not any(d.startswith(i) for i in ignore_dirs)] 29 | files = [f for f in files if not f.startswith('.')] # Exclude hidden files 30 | 31 | current_dict = dir_structure 32 | path_parts = os.path.relpath(root, start_path).split(os.sep) 33 | for part in path_parts: 34 | if part: 35 | if part not in current_dict: 36 | current_dict[part] = {} 37 | current_dict = current_dict[part] 38 | for f in files: 39 | current_dict[f] = None 40 | return dir_structure 41 | 42 | # Get the current script path 43 | current_script_path = os.path.realpath(__file__) 44 | 45 | # Get the top parent directory and directory structure 46 | top_parent_path = get_top_parent_path(current_script_path) 47 | dir_structure = get_directory_structure(top_parent_path) 48 | 49 | return str(dir_structure) 50 | -------------------------------------------------------------------------------- /captainfunction/functions/web_content_scraper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import re 4 | import json 5 | 6 | def get_function_schema(): 7 | return { 8 | "name": "web_content_scraper", 9 | "description": "Scrapes a given URL using Beautiful Soup library, strips out HTML tags, and only reads the content inside the header (h1, h2, etc.) and paragraph (p) tags. Returns the stripped content.", 10 | "parameters": { 11 | "type": "object", 12 | "properties": { 13 | "url": { 14 | "type": "string", 15 | "description": "URL of the webpage to scrape" 16 | } 17 | }, 18 | "required": ["url"] 19 | } 20 | } 21 | 22 | def handle_response(arguments): 23 | arguments = json.loads(arguments) 24 | url = arguments["url"] 25 | 26 | try: 27 | response = requests.get(url) 28 | soup = BeautifulSoup(response.content, "html.parser") 29 | 30 | # Extract header and paragraph tags 31 | header_tags = soup.find_all(re.compile(r"^h\d$")) 32 | paragraph_tags = soup.find_all("p") 33 | 34 | # Strip HTML tags and collect text content 35 | stripped_content = "" 36 | for tag in header_tags + paragraph_tags: 37 | stripped_content += " " + tag.get_text() 38 | 39 | return stripped_content.strip() 40 | except Exception as e: 41 | return f"Error scraping URL '{url}': {str(e)}" 42 | -------------------------------------------------------------------------------- /captainfunction/functions/web_search.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import json 4 | from metaphor_python import Metaphor 5 | 6 | def get_function_schema(): 7 | return { 8 | "name": "web_search", 9 | "description": "This function performs a web search using the Metaphor Search API and returns the results.", 10 | "parameters": { 11 | "type": "object", 12 | "properties": { 13 | "searchInput": { 14 | "type": "string", 15 | "description": "The input for the search." 16 | } 17 | }, 18 | "required": ["searchInput"] 19 | } 20 | } 21 | 22 | def handle_response(arguments): 23 | arguments = json.loads(arguments) 24 | searchInput = arguments["searchInput"] 25 | 26 | metaphor = Metaphor(api_key=os.getenv('METAPHOR_API_KEY')) 27 | 28 | try: 29 | results = metaphor.search(searchInput, use_autoprompt=True) 30 | print(results) 31 | return str(results) 32 | except requests.exceptions.RequestException as e: 33 | return f"An error occurred during the search: {str(e)}" 34 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | package = [] 3 | 4 | [metadata] 5 | lock-version = "2.0" 6 | python-versions = ">=3.10.0,<3.11" 7 | content-hash = "2bacfc8cb08893ad9860b2d44fdd9a74b1ce839d88fa5cd3cbd7e07eaa053d65" 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "captainfunction" 3 | version = "0.1.0" 4 | description = "A Python package to dynamically load functions for OpenAI Assistant" 5 | authors = ["Yohei Nakajima "] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.10.0,<3.11" 9 | 10 | [tool.pyright] 11 | # https://github.com/microsoft/pyright/blob/main/docs/configuration.md 12 | useLibraryCodeForTypes = true 13 | exclude = [".cache"] 14 | 15 | [tool.ruff] 16 | # https://beta.ruff.rs/docs/configuration/ 17 | select = ['E', 'W', 'F', 'I', 'B', 'C4', 'ARG', 'SIM'] 18 | ignore = ['W291', 'W292', 'W293'] 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='captainfunction', 7 | version='0.1', 8 | packages=find_packages(), 9 | install_requires=[ 10 | # Add your dependencies here, e.g., 'requests', 'numpy', etc. 11 | ], 12 | package_data={ 13 | 'captainfunction': ['functions/*.py'], 14 | }, 15 | include_package_data=True, 16 | description='A Python package to dynamically load functions for OpenAI Assistant', 17 | long_description=open('README.md').read(), 18 | long_description_content_type='text/markdown', 19 | author='Yohei Nakajima', 20 | author_email='info@untapped.vc', 21 | url='https://github.com/yoheinakajima', 22 | # More metadata can be added here (license, classifiers, etc.) 23 | ) 24 | --------------------------------------------------------------------------------