372 lines
12 KiB
PHP
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]);
|
||
|
|
}
|
||
|
|
}
|