Files
MtoRagSystem/src/Infrastructure/OllamaClient.php
team2 6822c8f3f8 optimize ui
add new ki endpoint params
2026-02-17 20:36:47 +01:00

204 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Infrastructure;
use App\Entity\ModelGenerationConfig;
use App\Service\ModelGenerationConfigProvider;
use Generator;
use JsonException;
use RuntimeException;
use Throwable;
final class OllamaClient
{
private ?ModelGenerationConfig $cachedConfig = null;
public function __construct(
private string $apiUrl,
private string $model,
private int $timeoutSeconds,
private ModelGenerationConfigProvider $configProvider
) {}
/**
* Public Streaming API
*/
public function stream(string $prompt): Generator
{
$config = $this->getConfig();
if ($config->isStream()) {
yield from $this->streamInternal($prompt);
return;
}
// Fallback: Blocking generate → Generator-kompatibel ausgeben
yield $this->generateInternal($prompt);
}
/**
* Public Blocking API
*/
public function generate(string $prompt): string
{
return $this->generateInternal($prompt);
}
/**
* Internal streaming transport
*/
private function streamInternal(string $prompt): Generator
{
$payload = $this->buildPayload($prompt, true);
$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): 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 {
do {
$status = curl_multi_exec($mh, $running);
} while ($status === CURLM_CALL_MULTI_PERFORM);
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;
}
}
if ($running) {
curl_multi_select($mh, 0.2);
}
} while ($running && !$done);
if (curl_errno($ch)) {
throw new RuntimeException('LLM connection error: ' . curl_error($ch));
}
} finally {
curl_multi_remove_handle($mh, $ch);
curl_multi_close($mh);
curl_close($ch);
}
}
/**
* Internal blocking transport
*/
private function generateInternal(string $prompt): string
{
$payload = $this->buildPayload($prompt, 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 => true,
CURLOPT_TIMEOUT => $this->timeoutSeconds,
]);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('LLM error: ' . curl_error($ch));
}
curl_close($ch);
$json = json_decode($response, true, flags: JSON_THROW_ON_ERROR);
return $json['response'] ?? '';
}
/**
* Central Payload Builder (DRY)
*/
private function buildPayload(string $prompt, bool $stream): string
{
return json_encode([
'model' => $this->model,
'prompt' => $prompt,
'stream' => $stream,
'options' => $this->buildOptions()
], JSON_THROW_ON_ERROR);
}
/**
* Central Options Builder (DRY)
*/
private function buildOptions(): array
{
$config = $this->getConfig();
return [
'temperature' => $config->getTemperature(),
'top_k' => $config->getTopK(),
'top_p' => $config->getTopP(),
'repeat_penalty' => $config->getRepeatPenalty(),
'num_ctx' => $config->getNumCtx(),
];
}
/**
* Config caching per request
*/
private function getConfig(): ModelGenerationConfig
{
if ($this->cachedConfig === null) {
$this->cachedConfig = $this->configProvider->getActiveForModel($this->model);
}
return $this->cachedConfig;
}
}