Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

AI Chat

An assistant that reads and modifies your notes — streamed token by token.

You will learn

  • What Server-Sent Events (SSE) are and how they differ from HTTP
  • Stream events from ZealPHP with $response->sse()
  • How tool calls let an AI agent modify your data
  • The architecture: PHP spawns Python, Python streams SSE, browser renders live

The problem

Your notes app works, but the user does everything manually. What if they could talk to an assistant that reads, searches, creates, and deletes their notes? And what if the response streamed in token by token, like ChatGPT, instead of making the user wait for the full answer?

What is SSE?

Regular HTTP is like texting: you send a message, get a reply, conversation over. Server-Sent Events is like calling someone and saying "read me the news". They talk continuously, you listen. You can't interrupt (that would be WebSocket). But for streaming AI tokens, you don't need to — you just need to listen.

On the wire, SSE looks like this:

event: token
data: {"token":"Hello"}

event: token
data: {"token":" world"}

event: done
data: {"done":true}

Each event is two lines (event: + data:) followed by a blank line. The browser receives them one by one as they arrive.

Streaming with $response->sse()

ZealPHP makes SSE a one-liner. The sse() helper sets the right headers and gives you an $emit callback:

$response->sse(function($emit) {
    $emit(json_encode(['token' => 'Hello']), 'token');
    usleep(100000);
    $emit(json_encode(['token' => ' world']), 'token');
    $emit(json_encode(['done' => true]), 'done');
});

The architecture

sequenceDiagram
    participant B as Browser
    participant PHP as ZealPHP
    participant PY as Python Agent
    participant AI as OpenAI API
    participant API as Notes API
    B->>PHP: POST /api/learn/chat
    PHP->>PY: proc_open (spawn)
    PY->>AI: Runner.run_streamed()
    AI-->>PY: token deltas
    PY-->>PHP: SSE: event: token
    PHP-->>B: SSE: token (streamed live)
    AI->>PY: tool_call: create_note
    PY->>API: POST /api/learn/notes
    API-->>PY: {"id": 42}
    Note over API: WS::broadcast()
    API-->>B: WebSocket: note_changed
    PY-->>PHP: SSE: tool_done
    PHP-->>B: SSE: tool_done + notes_changed

Tool calls

The AI doesn't just generate text. It can call functions that interact with your data. The Python agent defines six tools: list_notes, read_note, search_notes, create_note, update_note, delete_note.

When the model decides to use a tool, the browser shows an expandable card with the tool name, arguments, and result. You see the AI's reasoning process in real time.

Introducing App::renderStream()

SSE is one form of streaming. ZealPHP also supports streaming HTML — sending chunks of a page as they're generated, rather than waiting for the entire page to render.

// A streaming template
return function($items) {
    yield "<section>";
    foreach ($items as $item) {
        yield "<div>{$item->name}</div>";
    }
    yield "</section>";
};

App::renderStream() returns a Generator. Each yield is flushed to the browser immediately. For AI responses, database-heavy pages, or any slow render, this means the user sees content arriving progressively instead of staring at a blank screen.

Why use SSE instead of returning JSON after the full response is ready?

🔎 Why pipe Python instead of calling OpenAI from PHP?

The OpenAI Agents SDK (Python) handles tool dispatch, conversation memory, and streaming out of the box. Replicating that in PHP would be hundreds of lines. By spawning a subprocess, ZealPHP stays thin — it's a streaming proxy, not an AI framework. This pattern lets you swap the agent (different model, different tools) without touching PHP.

Key Takeaways

  • SSE streams events from server to browser — perfect for AI token streaming
  • $response->sse($emit) handles headers and formatting automatically
  • Tool calls let AI agents interact with your data — shown as expandable cards
  • App::renderStream() streams HTML chunks for progressive page rendering