Back to cookbook

Assistants with function tools

What you'll build

A weather assistant. You register an assistant with one function tool (get_current_weather). Your code creates a thread, adds a user message, starts a run, and polls the run's state. When the run enters requires_action, you call the tool locally and submit the outputs back; the run resumes and completes. Finally you read the last assistant message off the thread. This is the Assistants API OpenAI shipped, Ringside implements the same surface, wire-compat.

What you need

  • An FC API key with scope api:chat
  • pip install openai
  • Python 3.10+

Full code

python
# weather_agent.py import json, os, time from openai import OpenAI client = OpenAI(api_key=os.environ["FC_API_KEY"], base_url="https://api.fightclub.pro/v1") def get_current_weather(location: str, unit: str = "fahrenheit") -> dict: """Your real implementation would call a weather API. Here's a stub.""" fake = {"San Francisco": 62, "Tokyo": 54, "Paris": 48} temp = fake.get(location, 70) if unit == "celsius": temp = round((temp - 32) * 5 / 9, 1) return {"location": location, "temp": temp, "unit": unit} # 1. Create (or reuse) the assistant — this is a one-time setup call. assistant = client.beta.assistants.create( name="Weather Bot", instructions="You're a terse weather agent. Always call the tool when asked about weather.", model="fc:openai/gpt-4o-mini", tools=[ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "parameters": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["location"], }, }, } ], ) print(f"assistant: {assistant.id}") def ask(question: str) -> str: # 2. Thread + user message. thread = client.beta.threads.create( metadata={"customer_external_id": "ext:demo_user"}, ) client.beta.threads.messages.create( thread_id=thread.id, role="user", content=question, ) # 3. Start the run. run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id) # 4. Poll until terminal. while run.status in ("queued", "in_progress", "requires_action"): if run.status == "requires_action": outputs = [] for call in run.required_action.submit_tool_outputs.tool_calls: args = json.loads(call.function.arguments) result = get_current_weather(**args) outputs.append({ "tool_call_id": call.id, "output": json.dumps(result), }) run = client.beta.threads.runs.submit_tool_outputs( thread_id=thread.id, run_id=run.id, tool_outputs=outputs, ) continue time.sleep(0.5) run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id) # 5. Read the final assistant message. msgs = client.beta.threads.messages.list(thread_id=thread.id, order="desc", limit=1) return msgs.data[0].content[0].text.value if __name__ == "__main__": print(ask("What's the weather in Tokyo?"))

Walkthrough

The state machine on a run has 8 states; in this recipe we handle the three active ones (queued, in_progress, requires_action) and fall out on the terminal completed state. failed / cancelled / expired would all break the while loop too, check run.status before reading the message to surface errors to the caller.

Ringside enforces a 10-iteration cap on tool loops. If your assistant keeps calling tools in a cycle, you'll get 400 max_tool_iterations_exceeded, tighten your instructions or the tool's return schema.

The metadata.customer_external_id on thread-create routes usage to a specific Customer. Without it, thread usage falls into the dev's implicit _<dev>_assistants_default Customer, fine for dev, but in production you want explicit attribution.

Run it

bash
export FC_API_KEY=sk_live_xxx python weather_agent.py

You should see The current temperature in Tokyo is 54°F. or similar.

What's next