For the past month, I’ve been trying to make the OpenAI Assistant’s function calling work but keep running into an issue that I can’t seem to resolve. Despite following the documentation closely and searching everywhere for similar problems, I’m still stuck. I’ve even found some forum posts addressing similar issues but none have resolved my problem.
What I’m Doing
I’ve built a simple script to test the function API with OpenAI’s Assistant, and I’m using the Yr API for weather data fetching (free and doesn’t require an API key). Here’s a summarized version of the code:
import requests
import random
import time
import json
class YrService:
HEADERS = {"User-Agent": "WeatherClient/1.0"}
def __init__(self):
print("Initializing YrService.")
self.client = None
try:
from yr_weather import Locationforecast
self.client = Locationforecast(headers=self.HEADERS, use_cache=False)
print("YrService initialized successfully.")
except ImportError as e:
print("Failed to import yr_weather. Make sure the library is installed.", e)
def get_forecast(self, latitude, longitude):
if not self.client:
print("YrService client not initialized.")
return None
print(f"Fetching forecast for coordinates: ({latitude}, {longitude})")
try:
forecast = self.client.get_forecast(
lat=latitude, lon=longitude, forecast_type="compact"
)
print("Raw forecast data:", forecast.__dict__)
return forecast.__dict__
except Exception as e:
print("Error fetching forecast:", e)
return None
def get_coordinates(location):
print(f"Mock geocoding for location: {location}")
return random.uniform(-90, 90), random.uniform(-180, 180)
class OpenAIService:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.openai.com/v1"
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"OpenAI-Beta": "assistants=v2",
}
def ask_weather(self, thread_id, assistant_id, location):
print(f"Asking OpenAI about the weather in {location}.")
message_content = f"What is the weather in {location}?"
self.add_message_to_thread(thread_id, message_content)
run_id = self.create_run(thread_id, assistant_id)
self.poll_and_handle_action(thread_id, run_id, location)
def create_thread(self):
print("Creating a new OpenAI thread.")
url = f"{self.base_url}/threads"
response = requests.post(url, headers=self.headers)
response.raise_for_status()
thread_id = response.json()["id"]
print(f"Created thread ID: {thread_id}")
return thread_id
def add_message_to_thread(self, thread_id, message_content):
print(f"Adding message to thread ID {thread_id}: {message_content}")
url = f"{self.base_url}/threads/{thread_id}/messages"
data = {"role": "user", "content": message_content}
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
def create_run(self, thread_id, assistant_id):
print(f"Creating run for thread ID {thread_id} with assistant ID {assistant_id}.")
url = f"{self.base_url}/threads/{thread_id}/runs"
data = {"assistant_id": assistant_id}
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
run_id = response.json()["id"]
print(f"Created run ID: {run_id}")
return run_id
def poll_and_handle_action(self, thread_id, run_id, location):
print(f"Polling run ID {run_id} for completion.")
url = f"{self.base_url}/threads/{thread_id}/runs/{run_id}"
while True:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
status = data["status"]
print(f"Run status: {status}")
if status == "requires_action":
print("Action required. Handling action...")
self.handle_requires_action(data, thread_id, run_id, location)
break
elif status in ["completed", "failed"]:
print("Run completed or failed.")
break
time.sleep(2)
def handle_requires_action(self, data, thread_id, run_id, location):
tool_outputs = []
tool_calls = data.get("required_action", {}).get("submit_tool_outputs", {}).get("tool_calls", [])
print("Tool calls found:", tool_calls)
if not tool_calls:
print("No tool calls available in required_action.")
return
for tool in tool_calls:
print("Processing tool call:", tool)
if tool["function"]["name"] == "fetch_weather":
# Parse the location from the function arguments
arguments = tool["function"].get("arguments", "{}")
try:
location_data = json.loads(arguments)
location = location_data.get("location", location)
except json.JSONDecodeError:
print("Failed to decode arguments.")
location = location
lat, lon = get_coordinates(location)
yr_service = YrService()
weather_data = yr_service.get_forecast(lat, lon)
if weather_data and "_timeseries" in weather_data:
# Extract temperature and rain probability
temperature = weather_data.get("_timeseries", [{}])[0].get("data", {}).get("instant", {}).get("details", {}).get("air_temperature", "N/A")
rain_probability = weather_data.get("_timeseries", [{}])[0].get("data", {}).get("next_1_hours", {}).get("details", {}).get("precipitation_amount", "N/A")
# Combine results into a single output
weather_output = {
"temperature": temperature,
"rain_probability": rain_probability,
}
else:
print("Weather data is incomplete or missing.")
weather_output = {"error": "Weather data unavailable"}
print(f"Retrieved weather data: {weather_output}")
tool_outputs.append({"tool_call_id": tool["id"], "output": json.dumps(weather_output)})
print("Final tool outputs to submit:", tool_outputs)
self.submit_tool_outputs(thread_id, run_id, tool_outputs)
def submit_tool_outputs(self, thread_id, run_id, tool_outputs):
if not tool_outputs:
print("No valid tool outputs to submit.")
return
url = f"{self.base_url}/threads/{thread_id}/runs/{run_id}/submit-tool-outputs"
data = {"tool_outputs": tool_outputs}
print(f"Submitting tool outputs to URL: {url}")
print(f"Payload: {json.dumps(data, indent=2)}")
response = requests.post(url, headers=self.headers, json=data)
try:
response.raise_for_status()
print("Tool outputs submitted successfully.")
except requests.exceptions.HTTPError as e:
print(f"Error submitting tool outputs: {response.text}")
raise
if __name__ == "__main__":
OPENAI_API_KEY = "whatever key here"
ASSISTANT_ID = "assistant id here"
LOCATION = "Oslo" # Example location
print("Initializing OpenAI service.")
openai_service = OpenAIService(api_key=OPENAI_API_KEY)
print("Creating new OpenAI thread.")
thread_id = openai_service.create_thread()
print("Querying OpenAI for weather.")
openai_service.ask_weather(thread_id, ASSISTANT_ID, LOCATION)
Issue Encountered
When submitting tool outputs, I receive the following error:
{
"error": {
"message": "Invalid URL (POST /v1/threads/thread_ad7J64j4Wsyv4Y2mSishv8hS/runs/run_vgRUOGMAV1sMpRWnZAx3PRZf/submit-tool-outputs)",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
What I’ve Tried
- Following the Documentation: Referenced the Assistant API guide step-by-step.
- Adjusting API Requests: Experimented with including/excluding
requestId
in the URL. - Using Chat Completions API: This works perfectly, but I need the Assistant API for my application.
- Tried with Minimal Examples: Created a simplified test file to isolate the issue.
- Referencing Other Forum Posts: Encountered similar reports, but no definitive solutions.
Observations
- Polling Behavior: The run remains in the “requires_action” state indefinitely when submitting JSON.
- Plain Text Submission: Submitting plain text outputs results in the same error as above.
- Yr API Integration: Weather data fetching itself works fine, and the output is as expected (mock geocoding + weather details).
- Error Consistency: Regardless of request modifications, the error persists.
Question
Does anyone know the proper way to submit function outputs to the Assistant API?
Is there a parameter I might be missing or something critical I’m overlooking?
I’ve attached the test script (test.py
) and included the Yr API for reference. If you want to replicate, you only need:
- OpenAI API key (for all tests)
Playground Behavior
In the OpenAI playground, submitting JSON outputs keeps the run going indefinitely until canceled. Submitting outputs as plain text results in an error, as shown in the attached screenshot:
Help Needed
I’d greatly appreciate guidance on resolving this issue. If you’ve encountered something similar, please share any insights. Let me know if you need more details or logs.
Thanks in advance!