Function calling in ChatGPT is like a gift to developers — one that comes with some “assembly required.” It has some impressive capabilities, but also a few traps waiting to catch the unwary.
Yet, once you get the hang of it, it feels less like coding and more like commanding an intelligent agent. In fact, you could argue it’s ushering in a new era for AI application architecture.
The easy way
Here’s the gist: when using the function calling API, you tell ChatGPT which functions are available. It responds by telling you which function it needs to execute, along with the arguments. You then give it those arguments, and this delightful back-and-forth continues until ChatGPT can provide a final answer.
The easy way to handle this? Simply package up the functions you want to offer, make an API call, and let a handy library like pygptcalls take care of the back-and-forth for you. No sweat.
Let’s see it in action. Imagine you want to give ChatGPT the ability to interact with your filesystem. You’d need to offer functions to list, read, and write files. Simple, right?
Here’s a little code snippet to do just that:
import os
from typing import Listdef find_files_with_extension(root_dir: str, extension: str) -> List[str]:
"""
Recursively find files from a root directory with a specific extension.
Args:
root_dir (str): The root directory to search.
extension (str): The file extension to look for (e.g., '.txt').
Returns:
List[str]: A list of file paths matching the given extension.
"""
matched_files = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.endswith(extension):
matched_files.append(os.path.join(dirpath, filename))
return matched_files
def read_file_to_string(file_path: str) -> str:
"""
Read a file from a specified path and return its contents as a string.
Args:
file_path (str): The path to the file to read.
Returns:
str: The contents of the file as a string.
"""
with open(file_path, 'r') as file:
return file.read()
def write_string_to_file(file_path: str, content: str) -> None:
"""
Write a string to a file at a specified path.
Args:
file_path (str): The path to the file to write.
content (str): The string content to write into the file.
Returns:
None
"""
with open(file_path, 'w') as file:
file.write(content)
Adding basic documentation is crucial here. Why? Because that documentation becomes part of the prompt ChatGPT uses to understand and execute your functions. Think of it as giving your code a little “cheat sheet.”
Now, let’s clean up some Python files. With pygptcalls, we can execute this beautifully:
from pygptcalls import gptcall
import file_opsif __name__ == '__main__':
prompt = "update all python files in the directory sample_project. Add documentation and type annotations in every method."
gptcall(file_ops, prompt)
The prompt is straightforward:
Update all python files in the directory sample_project. Add documentation and type annotations in every method.
The result? Magic. Your Python code will transform into something much better, with every function documented and annotated. For example:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoindef fetch_webpage(url):
try:
response = requests.get(url)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"Error fetching the webpage: {e}")
return None
def parse_webpage(html_content):
soup = BeautifulSoup(html_content, 'html.parser')
return soup
def extract_links(soup, base_url):
links = []
for a_tag in soup.find_all('a', href=True):
href = a_tag.get('href')
full_url = urljoin(base_url, href)
links.append(full_url)
return links
def print_links(links):
for link in links:
print(link)
def main(url):
html_content = fetch_webpage(url)
if html_content:
soup = parse_webpage(html_content)
links = extract_links(soup, url)
print_links(links)
if __name__ == "__main__":
url = input("Enter the URL: ")
main(url)
Was updated to:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoindef fetch_webpage(url: str) -> str:
"""
Fetches the content of the webpage at the given URL.
Args:
url (str): The URL to fetch the webpage from.
Returns:
str: The content of the webpage as a string, or None if an error occurred.
"""
try:
response = requests.get(url)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"Error fetching the webpage: {e}")
return None
def parse_webpage(html_content: str) -> BeautifulSoup:
"""
Parses the HTML content and returns a BeautifulSoup object.
Args:
html_content (str): The HTML content to parse.
Returns:
BeautifulSoup: The parsed BeautifulSoup object.
"""
soup = BeautifulSoup(html_content, 'html.parser')
return soup
def extract_links(soup: BeautifulSoup, base_url: str) -> list:
"""
Extracts all the links from the given BeautifulSoup object.
Args:
soup (BeautifulSoup): The BeautifulSoup object containing the parsed HTML.
base_url (str): The base URL to resolve relative links.
Returns:
list: A list of extracted links (full URLs).
"""
links = []
for a_tag in soup.find_all('a', href=True):
href = a_tag.get('href')
full_url = urljoin(base_url, href)
links.append(full_url)
return links
def print_links(links: list) -> None:
"""
Prints the extracted links to the console.
Args:
links (list): The list of links to print.
"""
for link in links:
print(link)
def main(url: str) -> None:
"""
Main function to execute the link extraction process.
Args:
url (str): The URL to fetch and process.
"""
html_content = fetch_webpage(url)
if html_content:
soup = parse_webpage(html_content)
links = extract_links(soup, url)
print_links(links)
if __name__ == "__main__":
url = input("Enter the URL: ")
main(url)
Sure, doing this for a single file is no big deal, but imagine automating this for an entire project with hundreds of files! You can even add more advanced tasks like “find security issues”, and ChatGPT will dutifully carry out its function-calling magic. Or ever “find bugs and write snarky comments about them.”
The sequence of function calls could look like this:
find_files_with_extension('sample_project', '.py')
read_file_to_string('sample_project/extract_links.py')
write_string_to_file('sample_project/extract_links.py', ...)
....With pygptcalls handling the interaction, all you need to do is sit back and watch the show.
The Hard Way
Now, if you want to go the DIY route — without using a helper library like pygptcalls—buckle up. It’s not rocket science, but it can feel like it. Here’s how it works, step-by-step:
- Create a JSON schema that describes all the available functions and their parameters.
- Make a call to the ChatGPT API, providing this schema.
- When ChatGPT suggests a function call, execute the function locally, append the result to the message thread, and send it back to the API. This process may repeat multiple times.
- Once all function calls are handled, return the final result.
For example, your tools JSON might look like this:
[
{
"type": "function",
"function": {
"strict": true,
"name": "find_files_with_extension",
"description": "Recursively find files from a root directory with a specific extension.\n \n Args:\n root_dir (str): The root directory to search.\n extension (str): The file extension to look for (e.g., '.txt').\n \n Returns:\n List[str]: A list of file paths matching the given extension.",
"parameters": {
"type": "object",
"properties": {
"root_dir": {
"name": "root_dir",
"type": "string",
"description": "The root directory to search."
},
"extension": {
"name": "extension",
"type": "string",
"description": "The file extension to look for (e.g., '.txt')."
}
},
"required": [
"root_dir",
"extension"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
"strict": true,
"name": "read_file_to_string",
"description": "Read a file from a specified path and return its contents as a string.\n \n Args:\n file_path (str): The path to the file to read.\n \n Returns:\n str: The contents of the file as a string.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"name": "file_path",
"type": "string",
"description": "The path to the file to read."
}
},
"required": [
"file_path"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
"strict": true,
"name": "write_string_to_file",
"description": "Write a string to a file at a specified path.\n\n Args:\n file_path (str): The path to the file to write.\n content (str): The string content to write into the file.\n\n Returns:\n None",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"name": "file_path",
"type": "string",
"description": "The path to the file to write."
},
"content": {
"name": "content",
"type": "string",
"description": "The string content to write into the file."
}
},
"required": [
"file_path",
"content"
],
"additionalProperties": false
}
}
}
]Now, you’d make the API call with these tools:
response = client.beta.chat.completions.parse(
messages=[
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": prompt
}
] + messages,
model="gpt-4o-mini",#gpt-4o-mini
tools=tools_json
)where tools_json is the json file above.
When ChatGPT decides to call a function, you’ll find it in:
response.choices[0].message.tool_callsThe tool call will have the function to call and arguments. If could be more than one.
And here’s the tricky part: to chain the responses together, you’ll append the result of your local function back to the message list and include the tool_call ID. Something like:
{
"role": "tool",
"content": json.dumps(function_result),
"tool_call_id": tool_call.id
}This process can go on for several iterations before you reach the final response. It’s like ping-pong, but with code.
Conclusion
Conclusion
Function calling is a game-changer. It allows you to create intelligent, multi-functional agents with elegant architecture and full control over execution. You can even add security restrictions to ensure that these agents behave. What’s not to love?
So, whether you take the easy way or the hard way, once you’ve mastered function calling, you’ll wonder how you ever lived without it.
Like it? Drop me a comment.