16: AI
Phlo bundelt een aantal AI-providers (OpenAI, Claude, Gemini, DeepSeek, Grok) achter een enkele façade. Jij kiest een model, Phlo kiest de juiste engine. Streaming naar de DOM gebruikt dezelfde apply() mechanismen als de rest van Phlo: geen aparte client-side bibliotheek, geen aparte event bus.
16.1: Hulpmiddelen
Voeg toe aan data/app.json:
{
"resources": [..., "AI/AI", "AI/OpenAI"]
}
Één bestand per provider. AI/AI is de facade, die naar de juiste engine routeert op basis van het model:
| Model bevat | Engine |
|---|---|
gpt-*, o1-*, o3-*, o4-*, chatgpt-* |
OpenAI |
claude-* |
Claude |
deepseek-* |
DeepSeek |
gemini-* |
Gemini |
grok-* |
Grok |
Of expliciet via het via: argument: %AI->chat(via: 'claude', model: ...).
Het model argument is optioneel. Zonder een model valt de facade terug op %app->model en vervolgens op zijn eigen standaard, gpt-5.4-mini (dat naar OpenAI routeert), zodat een app met alleen een OpenAI sleutel in creds.ini direct werkt. Stel %app->model in om de standaard per app te wijzigen, of overschrijf %AI.model met een build mod om het systeemwijd te veranderen, inclusief de standaard engine.
De facade exposeert dezelfde methoden voor elke engine, maar niet elke provider ondersteunt elke methode:
| Engine | chat | stream | tools | vision | embeddings | transcribe |
|---|---|---|---|---|---|---|
| OpenAI | ja | ja | ja | ja | native | ja |
| Claude | ja | ja | ja | ja | OpenAI | nee |
| Gemini | ja | ja | ja | ja | native | nee |
| DeepSeek | ja | ja | ja | nee | OpenAI | nee |
| Grok | ja | ja | ja | ja | OpenAI | nee |
OpenAI in de embeddings kolom betekent dat de engine geen eigen embedding model heeft en delegeert naar OpenAI, dus het heeft ook een OpenAI sleutel nodig. DeepSeek en Grok zijn dunne lagen bovenop OpenAI (zelfde protocol, andere endpoint en sleutel), dus ze delen dezelfde methodenset; een nee cel betekent dat de provider geen model of endpoint achter die oproep heeft en het zal een fout geven. De matrix is de bron van waarheid: roep alleen een mogelijkheid aan die gemarkeerd is voor de engine die je target.
Inloggegevens gaan in data/creds.ini:
OpenAI = sk-...
Claude = sk-ant-...
Grok = xai-...
Phlo's security/creds laadt ze automatisch in %creds->OpenAI enz. Zie Configuratie voor het volledige inloggegevensformaat, omgevingsvariabelen en prioriteit.
16.2: Een enkel antwoord
Korte vraag, één antwoord:
$answer = %AI->chat(
model: 'gpt-4o-mini',
user: 'Vat dit artikel samen: '.$article->text,
)
echo $answer->answer
Of nog korter, via de answer helper:
$verdict = answer('Is "wortel" een groente?', 'ja', 'nee', 'misschien')
answer() is ingebouwd in AI/answer. Het doet één oproep met een lage temperatuur en retourneert alleen het puurste antwoord. Met opties wordt het een keuze uit de gegeven mogelijkheden.
16.3: Streamen naar de DOM
Dit is waar Phlo's apply() protocol echt tot zijn recht komt. Een async route die token voor token in een element schrijft:
route async POST chat::ask {
%res->streaming = true
foreach (%AI->stream(user: %payload->question) AS $chunk){
if (isset($chunk->text)) apply(append: arr('#answer' => $chunk->text))
}
}
Stel %res->streaming = true in en elke apply() wordt onmiddellijk naar de client geflusht op het moment dat je het aanroept, in plaats van gebufferd te worden totdat de respons eindigt. Elk token wordt aan #answer toegevoegd via hetzelfde apply() protocol dat je overal elders gebruikt: geen SSE plumbing, geen handmatige flush(), geen JS om te schrijven en geen status om te beheren, een streaming UI direct beschikbaar.
16.4: Hulpmiddelen (functieaanroep)
$tool = obj(
name: 'get_weather',
desc: 'Get the current weather for a location',
args: arr(
location: arr(type: 'string', desc: 'City and country, e.g. "Paris, FR"'),
),
)
$res = %AI->chat(
model: 'gpt-4o-mini',
user: 'What is the weather in Amsterdam?',
tools: [OpenAI::tool($tool)],
)
foreach ($res->tools ?? [] AS $call){
if ($call->name === 'get_weather') weather::fetch($call->args['location'])
}
Toolaanroepen komen terug onder $res->tools als een array van {name, args}. Phlo's facade normaliseert de verschillen tussen de providers.
16.5: Visie
Werkt met OpenAI, Claude, Gemini en Grok.
16.6: Insluitingen
$vector = %AI->embedding('Phlo is a compile-to-PHP framework', model: 'text-embedding-3-small')
%vectors->store(id: 'doc-1', vector: $vector, meta: ['source' => 'about'])
Het standaardmodel is specifiek voor de provider. Voor OpenAI is het text-embedding-3-small.
16.7: Transcribe
$file = %files->save(%payload->file('audio'))
$res = %AI->transcribe($file, model: 'whisper-1', language: 'nl')
echo $res->text16.8: Veiligheid
- AI-aanroepen zijn duur en niet-deterministisch. Cache agressief,
%apcuvoor sessiescope,JSONDBvoor langere TTL's. - Filter gebruikersinvoer voordat je het in een prompt plaatst. Phlo's
esc()is voor HTML; voor prompts gebruik je je eigen sanitizer of een strikte tool schema. - Het loggen van prompts/antwoorden kan privacy-implicaties hebben. Standaard logt Phlo niet; je
data/errors.jsonziet alleen uitzonderingen.