first commit

This commit is contained in:
team 1
2026-02-11 14:15:08 +01:00
parent a4742c2c38
commit aa7d362bc3
58 changed files with 9999 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure;
use Generator;
use JsonException;
use RuntimeException;
use Throwable;
/**
* OllamaClient
*
* Production-ready streaming client for Ollama-compatible LLM backends.
*
* Key properties:
* - True live streaming (tokens are yielded while the request is running)
* - PHP-safe (no yield inside cURL callbacks)
* - Works for both HTTP streaming and CLI usage
* - Deterministic and resource-safe
*
* Implementation strategy:
* - Use curl_multi_* to keep control of the execution loop
* - Accumulate partial chunks into a rolling buffer
* - Extract JSON lines incrementally
* - Yield tokens immediately when they arrive
*/
final class OllamaClient
{
private string $apiUrl;
private string $model;
private int $timeoutSeconds;
public function __construct(
string $apiUrl,
string $model,
int $timeoutSeconds,
) {
$this->apiUrl = $apiUrl;
$this->model = $model;
$this->timeoutSeconds = $timeoutSeconds;
}
/**
* Streams tokens from the LLM backend in real time.
*
* @param string $prompt Fully constructed prompt
*
* @return Generator<string>
* @throws JsonException
*/
public function stream(string $prompt): Generator
{
$payload = json_encode([
'model' => $this->model,
'prompt' => $prompt,
'stream' => true,
], JSON_THROW_ON_ERROR);
$buffer = '';
$done = false;
$ch = curl_init($this->apiUrl);
if ($ch === false) {
throw new RuntimeException('Failed to initialize cURL');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_TIMEOUT => $this->timeoutSeconds,
CURLOPT_WRITEFUNCTION => function ($curl, string $data) use (&$buffer, &$done): int {
$buffer .= $data;
return strlen($data);
},
]);
$mh = curl_multi_init();
if ($mh === false) {
curl_close($ch);
throw new RuntimeException('Failed to initialize cURL multi handle');
}
curl_multi_add_handle($mh, $ch);
try {
do {
// Execute the multi handle
do {
$status = curl_multi_exec($mh, $running);
} while ($status === CURLM_CALL_MULTI_PERFORM);
// Read incoming data from the buffer
while (($pos = strpos($buffer, "\n")) !== false) {
$line = trim(substr($buffer, 0, $pos));
$buffer = substr($buffer, $pos + 1);
if ($line === '') {
continue;
}
try {
$json = json_decode($line, true, flags: JSON_THROW_ON_ERROR);
} catch (Throwable) {
continue;
}
if (isset($json['response'])) {
yield $json['response'];
}
if (!empty($json['done'])) {
$done = true;
}
}
// Wait for network activity
if ($running) {
curl_multi_select($mh, 0.2);
}
} while ($running && !$done);
// Flush remaining buffer (edge case)
if (!$done && trim($buffer) !== '') {
try {
$json = json_decode(trim($buffer), true, flags: JSON_THROW_ON_ERROR);
if (isset($json['response'])) {
yield $json['response'];
}
} catch (Throwable) {
// ignore
}
}
if (curl_errno($ch)) {
$error = curl_error($ch);
throw new RuntimeException('LLM connection error: ' . $error);
}
} finally {
curl_multi_remove_handle($mh, $ch);
curl_multi_close($mh);
curl_close($ch);
}
}
}