world-game/site/plugins/kirby-seo/classes/Ai/Drivers/Gemini.php
isUnknown 58c31ea391
All checks were successful
Deploy / Deploy to Production (push) Successful in 22s
feat: intégration plugin Kirby SEO
- Ajout de tobimori/kirby-seo via Composer
- snippet('seo/head') dans header.php (remplace les meta manuels)
- snippet('seo/schemas') dans footer.php pour JSON-LD
- Onglet SEO ajouté dans site.yml et tous les blueprints de pages
- Configuration SEO dans config.php (sitemap, robots, canonicalBase TODO)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:59:18 +01:00

149 lines
3.4 KiB
PHP

<?php
namespace tobimori\Seo\Ai\Drivers;
use Generator;
use tobimori\Seo\Ai\Chunk;
use tobimori\Seo\Ai\Content;
use tobimori\Seo\Ai\Driver;
use tobimori\Seo\Ai\SseStream;
class Gemini extends Driver
{
protected const string DEFAULT_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta';
protected const string DEFAULT_MODEL = 'gemini-3.1-flash-lite-preview';
/**
* @inheritDoc
*/
public function stream(
array $content,
string|null $model = null,
): Generator {
$apiKey = $this->config('apiKey', required: true);
$model = $model ?? $this->config('model', static::DEFAULT_MODEL);
$baseEndpoint = $this->config('endpoint', static::DEFAULT_ENDPOINT);
$endpoint = "{$baseEndpoint}/models/{$model}:streamGenerateContent?alt=sse&key={$apiKey}";
$headers = [
'Content-Type: application/json',
];
$payload = [
'contents' => $this->buildContents($content),
];
$systemInstruction = $this->buildSystemInstruction($content);
if ($systemInstruction !== null) {
$payload['systemInstruction'] = $systemInstruction;
}
$stream = new SseStream($endpoint, $headers, $payload, (int)$this->config('timeout', 120));
$started = false;
yield from $stream->stream(function (array $event) use (&$started): Generator {
$candidates = $event['candidates'] ?? [];
$candidate = $candidates[0] ?? null;
if ($candidate === null) {
$error = $event['error'] ?? null;
if ($error) {
yield Chunk::error($error['message'] ?? 'Unknown Gemini error.', $event);
}
return;
}
if (!$started) {
yield Chunk::streamStart($event);
yield Chunk::textStart($event);
$started = true;
}
$finishReason = $candidate['finishReason'] ?? null;
if ($finishReason === 'SAFETY') {
yield Chunk::error('Response blocked by safety filters.', $event);
return;
}
$parts = $candidate['content']['parts'] ?? [];
foreach ($parts as $part) {
$text = $part['text'] ?? '';
if ($text !== '') {
yield Chunk::textDelta($text, $event);
}
}
if ($finishReason !== null) {
yield Chunk::textComplete($event);
yield Chunk::streamEnd($event);
}
});
}
/**
* Translates an array of Content messages into the Gemini contents format.
*
* @param array<Content> $content
*/
private function buildContents(array $content): array
{
$contents = [];
foreach ($content as $message) {
if ($message->role() === 'system') {
continue;
}
$parts = [];
foreach ($message->blocks() as $block) {
if ($block['type'] === 'image') {
$parts[] = [
'inline_data' => [
'mime_type' => $block['mediaType'],
'data' => $block['data'],
],
];
} elseif ($block['type'] === 'text') {
$parts[] = [
'text' => $block['text'],
];
}
}
$contents[] = [
'role' => $message->role() === 'assistant' ? 'model' : 'user',
'parts' => $parts,
];
}
return $contents;
}
/**
* Extracts system messages into a Gemini systemInstruction object.
*
* @param array<Content> $content
*/
private function buildSystemInstruction(array $content): array|null
{
$parts = [];
foreach ($content as $message) {
if ($message->role() !== 'system') {
continue;
}
foreach ($message->blocks() as $block) {
if ($block['type'] === 'text') {
$parts[] = ['text' => $block['text']];
}
}
}
if ($parts === []) {
return null;
}
return ['parts' => $parts];
}
}