refactor: extract AbstractLlmPredictionProvider for shared boilerplate
Anthropic, Gemini, and OpenAi providers each repeated: API-key gate, chronological price-list building, response validation (direction/confidence/reasoning), TrendDirection::tryFrom, confidence cap at 85, and the top-level try/catch + Log::error. Now in AbstractLlmPredictionProvider: - LLM_MAX_CONFIDENCE constant - buildPriceList(Collection) helper - buildPrediction(input, ?source) — handles direction validation, confidence cap, model construction - defaultPrompt(priceList) — shared by Gemini and OpenAi - Default predict() flow (apiKey + callProvider + buildPrediction + try/catch). Gemini and OpenAi only implement apiKey() and callProvider(). Anthropic overrides predict() because of its multi-phase web-search + forced-tool flow but reuses the helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
99
app/Services/LlmPrediction/AbstractLlmPredictionProvider.php
Normal file
99
app/Services/LlmPrediction/AbstractLlmPredictionProvider.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\LlmPrediction;
|
||||
|
||||
use App\Enums\PredictionSource;
|
||||
use App\Enums\TrendDirection;
|
||||
use App\Models\BrentPrice;
|
||||
use App\Models\PricePrediction;
|
||||
use App\Services\ApiLogger;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
abstract class AbstractLlmPredictionProvider implements OilPredictionProvider
|
||||
{
|
||||
protected const int LLM_MAX_CONFIDENCE = 85;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ApiLogger $apiLogger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Default flow: gate on API key, call the provider, normalise the payload
|
||||
* to a PricePrediction. Subclasses with multi-phase flows (e.g. Anthropic
|
||||
* web-search) override `predict()` directly and reuse the helper methods.
|
||||
*/
|
||||
public function predict(Collection $prices): ?PricePrediction
|
||||
{
|
||||
$apiKey = $this->apiKey();
|
||||
|
||||
if ($apiKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = $this->callProvider($apiKey, $this->buildPriceList($prices));
|
||||
|
||||
return $payload === null ? null : $this->buildPrediction($payload);
|
||||
} catch (Throwable $e) {
|
||||
Log::error(static::class.': predict failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the configured API key or null if not set. */
|
||||
abstract protected function apiKey(): ?string;
|
||||
|
||||
/**
|
||||
* Make the provider HTTP call and return the normalised payload, or null
|
||||
* on failure (already logged by the implementer).
|
||||
*
|
||||
* @return array{direction: string, confidence: int, reasoning: string}|null
|
||||
*/
|
||||
abstract protected function callProvider(string $apiKey, string $priceList): ?array;
|
||||
|
||||
/** @param Collection<int, BrentPrice> $prices */
|
||||
protected function buildPriceList(Collection $prices): string
|
||||
{
|
||||
return $prices->sortBy('date')
|
||||
->map(fn (BrentPrice $p) => $p->date->toDateString().': $'.$p->price_usd)
|
||||
->implode("\n");
|
||||
}
|
||||
|
||||
/** @param array{direction: string, confidence: int, reasoning: string} $input */
|
||||
protected function buildPrediction(array $input, PredictionSource $source = PredictionSource::Llm): ?PricePrediction
|
||||
{
|
||||
$direction = TrendDirection::tryFrom($input['direction'] ?? '');
|
||||
|
||||
if ($direction === null) {
|
||||
Log::error(static::class.': invalid direction', ['input' => $input]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PricePrediction([
|
||||
'predicted_for' => now()->toDateString(),
|
||||
'source' => $source,
|
||||
'direction' => $direction,
|
||||
'confidence' => min((int) ($input['confidence'] ?? 0), self::LLM_MAX_CONFIDENCE),
|
||||
'reasoning' => $input['reasoning'] ?? '',
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function defaultPrompt(string $priceList): string
|
||||
{
|
||||
return <<<PROMPT
|
||||
You are analyzing Brent crude oil price data for a UK fuel price alert service.
|
||||
Predict the short-term direction over the next 3–5 days.
|
||||
|
||||
Recent Brent crude prices (USD/barrel):
|
||||
{$priceList}
|
||||
|
||||
Respond with direction (rising, falling, or flat), a confidence score (0–85),
|
||||
and a one-sentence reasoning.
|
||||
PROMPT;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user