feat: add LLM prediction providers with structured output support
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-07 14:42:44 +01:00
parent e9612666e3
commit 6a80c11f38
18 changed files with 1101 additions and 484 deletions

View File

@@ -30,22 +30,25 @@ class NationalFuelPredictionService
* signals: array
* }
*/
public function predict(FuelType $fuelType, ?float $lat = null, ?float $lng = null): array
public function predict(?float $lat = null, ?float $lng = null): array
{
$currentAvg = $this->getCurrentNationalAverage($fuelType);
$fuelType = FuelType::E10;
$hasCoordinates = $lat !== null && $lng !== null;
$currentAvg = $this->getCurrentAverage($fuelType, $lat, $lng);
$trend = $this->computeTrendSignal($fuelType);
$dayOfWeek = $this->computeDayOfWeekSignal($fuelType);
$brandBehaviour = $this->computeBrandBehaviourSignal($fuelType);
$stickiness = $this->computeStickinessSignal($fuelType);
$nationalMomentum = $this->disabledSignal('National momentum disabled for national predictions');
$regionalMomentum = $lat !== null && $lng !== null
$regionalMomentum = $hasCoordinates
? $this->computeRegionalMomentumSignal($fuelType, $lat, $lng)
: $this->disabledSignal('No coordinates provided for regional momentum analysis');
$signals = compact('trend', 'dayOfWeek', 'brandBehaviour', 'nationalMomentum', 'regionalMomentum', 'stickiness');
[$direction, $confidenceScore] = $this->aggregateSignals($signals);
[$direction, $confidenceScore] = $this->aggregateSignals($signals, $hasCoordinates);
$slope = $trend['slope'] ?? 0.0;
$predictedChangePence = round($slope * self::PREDICTION_HORIZON_DAYS, 1);
@@ -72,7 +75,7 @@ class NationalFuelPredictionService
'action' => $action,
'reasoning' => $this->buildReasoning($direction, $slope, $trend, $brandBehaviour),
'prediction_horizon_days' => self::PREDICTION_HORIZON_DAYS,
'region_key' => 'national',
'region_key' => $hasCoordinates ? 'regional' : 'national',
'methodology' => 'multi_signal_live_fallback',
'signals' => [
'trend' => $trend,
@@ -85,8 +88,20 @@ class NationalFuelPredictionService
];
}
private function getCurrentNationalAverage(FuelType $fuelType): float
private function getCurrentAverage(FuelType $fuelType, ?float $lat, ?float $lng): float
{
if ($lat !== null && $lng !== null) {
$avg = DB::table('station_prices_current')
->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
->where('station_prices_current.fuel_type', $fuelType->value)
->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat])
->avg('station_prices_current.price_pence');
if ($avg !== null) {
return round((float) $avg / 100, 1);
}
}
$avg = StationPriceCurrent::where('fuel_type', $fuelType->value)->avg('price_pence');
return $avg !== null ? round((float) $avg / 100, 1) : 0.0;
@@ -391,14 +406,22 @@ class NationalFuelPredictionService
* @param array<string, array{score: float, confidence: float, enabled: bool}> $signals
* @return array{0: string, 1: float}
*/
private function aggregateSignals(array $signals): array
private function aggregateSignals(array $signals, bool $hasCoordinates = false): array
{
$weights = [
'trend' => 0.45,
'dayOfWeek' => 0.20,
'brandBehaviour' => 0.25,
'stickiness' => 0.10,
];
$weights = $hasCoordinates
? [
'regionalMomentum' => 0.50,
'trend' => 0.20,
'dayOfWeek' => 0.15,
'brandBehaviour' => 0.10,
'stickiness' => 0.05,
]
: [
'trend' => 0.45,
'dayOfWeek' => 0.20,
'brandBehaviour' => 0.25,
'stickiness' => 0.10,
];
$weightedSum = 0.0;
$totalWeight = 0.0;