Files

372 lines
12 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class OllamaService
{
protected string $apiUrl;
protected string $model;
protected int $timeout;
protected float $temperature;
protected int $maxTokens;
protected int $retryMaxAttempts;
protected int $retryBackoff;
protected array $options;
public function __construct()
{
$this->apiUrl = config('ollama.api_url', 'http://localhost:11434');
$this->model = config('ollama.model', 'llama3');
$this->timeout = config('ollama.timeout', 300);
$this->temperature = config('ollama.temperature', 0.3);
$this->maxTokens = config('ollama.max_tokens', 4000);
$this->retryMaxAttempts = config('ollama.retry.max_attempts', 3);
$this->retryBackoff = config('ollama.retry.backoff_multiplier', 5);
$this->options = config('ollama.options', []);
}
/**
* Generate a summary for a book.
*
* @param string $bookText The book text to summarize
* @param string $bookTitle The book title
* @param string $bookAuthor The book author
* @return string|null The generated summary or null on failure
*/
public function generateSummary(string $bookText, string $bookTitle, string $bookAuthor): ?string
{
$prompt = $this->buildPrompt('summary', [
'title' => $bookTitle,
'author' => $bookAuthor,
'text' => $bookText,
]);
Log::info('Ollama: Generating summary', [
'model' => $this->model,
'title' => $bookTitle,
'author' => $bookAuthor,
'text_length' => strlen($bookText),
]);
$startTime = microtime(true);
try {
$response = $this->makeRequest($prompt);
$duration = microtime(true) - $startTime;
if ($response) {
$wordCount = str_word_count($response);
Log::info('Ollama: Summary generated successfully', [
'title' => $bookTitle,
'duration' => round($duration, 2) . 's',
'word_count' => $wordCount,
]);
// Quality check
$minWords = config('ollama.quality_checks.min_word_count', 100);
$maxWords = config('ollama.quality_checks.max_word_count', 1000);
if ($wordCount < $minWords) {
Log::warning('Ollama: Summary too short', [
'title' => $bookTitle,
'word_count' => $wordCount,
'min_required' => $minWords,
]);
}
if ($wordCount > $maxWords) {
Log::warning('Ollama: Summary too long', [
'title' => $bookTitle,
'word_count' => $wordCount,
'max_allowed' => $maxWords,
]);
}
return $response;
}
return null;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
Log::error('Ollama: Failed to generate summary', [
'title' => $bookTitle,
'error' => $e->getMessage(),
'duration' => round($duration, 2) . 's',
]);
return null;
}
}
/**
* Generate key insights from a book summary.
*
* @param string $summary The book summary
* @param string $bookTitle The book title
* @param string $bookAuthor The book author
* @return string|null The generated key insights or null on failure
*/
public function generateKeyInsights(string $summary, string $bookTitle, string $bookAuthor): ?string
{
$prompt = $this->buildPrompt('key_insights', [
'title' => $bookTitle,
'author' => $bookAuthor,
'summary' => $summary,
]);
Log::info('Ollama: Generating key insights', [
'model' => $this->model,
'title' => $bookTitle,
]);
$startTime = microtime(true);
try {
$response = $this->makeRequest($prompt);
$duration = microtime(true) - $startTime;
if ($response) {
Log::info('Ollama: Key insights generated successfully', [
'title' => $bookTitle,
'duration' => round($duration, 2) . 's',
]);
return $response;
}
return null;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
Log::error('Ollama: Failed to generate key insights', [
'title' => $bookTitle,
'error' => $e->getMessage(),
'duration' => round($duration, 2) . 's',
]);
return null;
}
}
/**
* Check if the Ollama API is accessible.
*
* @return bool
*/
public function checkConnection(): bool
{
try {
Log::info('Ollama: Checking connection', ['url' => $this->apiUrl]);
$response = Http::timeout(10)
->get($this->apiUrl . '/api/tags');
if ($response->successful()) {
Log::info('Ollama: Connection successful');
return true;
}
Log::warning('Ollama: Connection check failed', [
'status' => $response->status(),
]);
return false;
} catch (\Exception $e) {
Log::error('Ollama: Connection check error', [
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* List available models on the Ollama instance.
*
* @return array List of model names
*/
public function listModels(): array
{
try {
Log::info('Ollama: Listing models');
$response = Http::timeout(10)
->get($this->apiUrl . '/api/tags');
if ($response->successful()) {
$data = $response->json();
$models = array_map(
fn($model) => $model['name'] ?? 'unknown',
$data['models'] ?? []
);
Log::info('Ollama: Models retrieved', [
'count' => count($models),
'models' => $models,
]);
return $models;
}
Log::warning('Ollama: Failed to list models', [
'status' => $response->status(),
]);
return [];
} catch (\Exception $e) {
Log::error('Ollama: Error listing models', [
'error' => $e->getMessage(),
]);
return [];
}
}
/**
* Test summary generation with a sample text.
*
* @return array Test results with success status and details
*/
public function testGeneration(): array
{
$testText = "This is a test book about artificial intelligence and machine learning. " .
"It explores the history of AI, from its early beginnings to modern applications. " .
"The book covers neural networks, deep learning, and practical implementations.";
$testTitle = "Test Book: Introduction to AI";
$testAuthor = "Test Author";
$startTime = microtime(true);
$summary = $this->generateSummary($testText, $testTitle, $testAuthor);
$duration = microtime(true) - $startTime;
return [
'success' => $summary !== null,
'summary' => $summary,
'duration' => round($duration, 2),
'model' => $this->model,
'word_count' => $summary ? str_word_count($summary) : 0,
];
}
/**
* Build a prompt from a template with variable substitution.
*
* @param string $templateKey The template key from config
* @param array $variables Variables to substitute
* @return string The built prompt
*/
protected function buildPrompt(string $templateKey, array $variables): string
{
$template = config("ollama.prompts.{$templateKey}", '');
if (empty($template)) {
throw new \InvalidArgumentException("Prompt template '{$templateKey}' not found in config.");
}
$prompt = $template;
foreach ($variables as $key => $value) {
$prompt = str_replace('{' . $key . '}', $value, $prompt);
}
return $prompt;
}
/**
* Make a request to the Ollama API.
*
* @param string $prompt The prompt to send
* @return string|null The response text or null on failure
*/
protected function makeRequest(string $prompt): ?string
{
$url = $this->apiUrl . '/api/generate';
$payload = [
'model' => $this->model,
'prompt' => $prompt,
'stream' => false,
'options' => array_merge($this->options, [
'temperature' => $this->temperature,
]),
];
try {
$response = Http::timeout($this->timeout)
->retry(
$this->retryMaxAttempts,
function ($exception, $request) {
// Exponential backoff in milliseconds
return $this->retryBackoff * 1000;
},
function ($exception, $request) {
// Retry on connection errors and 5xx server errors
if ($exception instanceof ConnectionException) {
return true;
}
if (method_exists($exception, 'response')) {
$response = $exception->response;
return $response && $response->status() >= 500;
}
return false;
}
)
->post($url, $payload);
if ($response->successful()) {
$data = $response->json();
return $data['response'] ?? null;
}
Log::warning('Ollama: API request failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
} catch (ConnectionException $e) {
Log::error('Ollama: Connection error', [
'error' => $e->getMessage(),
'url' => $url,
]);
throw $e;
} catch (RequestException $e) {
Log::error('Ollama: Request exception', [
'error' => $e->getMessage(),
'url' => $url,
]);
throw $e;
} catch (\Exception $e) {
Log::error('Ollama: Unexpected error', [
'error' => $e->getMessage(),
'type' => get_class($e),
]);
throw $e;
}
}
/**
* Get the currently configured model name.
*
* @return string
*/
public function getModel(): string
{
return $this->model;
}
/**
* Set a different model for the next requests.
*
* @param string $model Model name
* @return void
*/
public function setModel(string $model): void
{
$this->model = $model;
Log::info('Ollama: Model changed', ['model' => $model]);
}
}