Build a code-review assistant with the Assistants API
30 minMost chat integrations are amnesiacs. Every request starts from nothing, so you end up shipping the whole conversation back and forth yourself. The Assistants API takes that bookkeeping off you: an Assistant is the reusable part, a Thread is one conversation that remembers itself, a Run is you pressing go. By the end you'll have all three, plus a streaming run that calls your own review_pull_request tool and a tiny Next.js UI that watches the review arrive.
- • A Ringside API key
- • Node 20+ and Next.js 14 or 15
- • A GitHub token if you want to hit real PRs in the tool implementation
- • Familiarity with SSE + fetch streaming helps but isn't required
- • 30 minutes
Create the Assistant
// register tools once, use forever
An Assistant is a named config: model + instructions + tool schema. Create it once and reference it from every run. You can update instructions later without touching your call sites.
The tool review_pull_request takes a PR URL. Ringside will ask the model when to invoke it. You run the actual fetch in your own code.
Function tools don't execute on Ringside's side. The model decides when to call them; you receive a requires_action event; you run the tool yourself and post the result back. This keeps secrets (like GitHub tokens) on your infra.
Create a Thread for the PR
// one thread per PR
Threads hold message history. Use one thread per PR so multi-turn conversations (e.g. "also check the test coverage") pick up where the last one left off.
Stash any useful context on metadata the PR URL, the author, the repo. Ringside passes it through unchanged and it shows up in your dashboard's thread browser.
Post the PR diff as a message
// user turn = your data
Append a user message containing the diff. The full message object comes back on create, so hold onto the id if you want to re-fetch it later; every message is addressable by id on GET /v1/threads/:thread_id/messages.
For very large PRs, summarize or split the diff across several user messages rather than posting one enormous blob. The thread keeps the order.
Create a streaming Run
// stream: true
Kick off the run with stream: true. Ringside returns SSE chunks in the OpenAI Assistants event taxonomy: run lifecycle (created/in_progress/completed) interleaved with message deltas.
When the model wants to call a tool, the stream emits thread.run.requires_action and pauses. The next move is yours.
A run can sit idle for up to 10 minutes waiting on tool output. If you miss the requires_action event, the run expires and emits run.failed with the code run_expired - safer than hanging forever.
Handle requires_action
// run the tool, submit outputs
Read the tool_calls array, execute each call in your own code (this is where you'd call the GitHub API), and POST the JSON-stringified outputs back to submit_tool_outputs.
The stream resumes immediately and the model writes its review using the tool output. Terminal: thread.run.completed.
A run can loop tool calls up to 10 times. Go past that and the run fails with max_tool_iterations_exceeded, delivered as a run.failed event rather than an error on the call you just made. Design your tool schemas so the model reaches an answer in a few hops.
Tiny Next.js UI
// input + button + stream
Your /api/review route proxies the Assistant run and streams back plain text. The client just reads from response.body and appends to a <pre>.
30-line component, no frameworks, no chat-ui library. Paste a PR URL, hit Review, watch the assistant's analysis appear character-by-character.
You built it.
An Assistant with a custom tool, a Thread per PR, a streaming Run that calls out to your tool implementation, and a working UI. The same scaffold swaps in for any function-tool agent. Replace the tool schema and your implementation, done.