The model calls your own code mid-answer and you never write the loop that stitches the tool result back into the conversation. Ringside holds the thread and the run state. When it needs something from you it stops, asks and waits.
Wire-compat with the Assistants API OpenAI shipped, so the openai SDK drives it unchanged. The example is a weather bot with one function tool.
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. 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
A run has 8 states. This loop handles the three active ones (queued, in_progress, requires_action) and falls out on terminal completed. failed, cancelled and expired break the while loop too, so check run.status before you read the message or you'll surface an empty answer instead of the error.
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
bashexport FC_API_KEY=ko_0d7f2a91c4e35b86af10d2c7e94b6f3a5d81c02e7b4936af18d5c60e2a7f9b34 python weather_agent.py
You should see The current temperature in Tokyo is 54°F. or similar.