feat: add generateLlmPredictionWithContext with web search geopolitical context

- New method uses web_search_20260209 server-side tool so Claude fetches
  48h of oil/geopolitical news autonomously before predicting direction
- Prompt uses raw prices only — no pre-computed EWMA indicators
- pause_turn loop handles server-side search continuation (up to 5 iters)
- generatePrediction() now tries context method first, falls back to
  generateLlmPrediction(), then EWMA
- Default model updated to claude-sonnet-4-6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-04 15:32:53 +01:00
parent 1d2eb12e83
commit fb4c413926
3 changed files with 229 additions and 2 deletions

View File

@@ -104,7 +104,8 @@ class OilPriceService
$prediction = null;
if (config('services.anthropic.api_key')) {
$prediction = $this->generateLlmPrediction($prices);
$prediction = $this->generateLlmPredictionWithContext($prices);
$prediction ??= $this->generateLlmPrediction($prices);
}
$prediction ??= $this->generateEwmaPrediction($prices);
@@ -204,6 +205,104 @@ class OilPriceService
}
}
/**
* LLM prediction with 48h geopolitical context via Anthropic web search.
* Claude searches for recent oil/geopolitical news before answering.
* Reasons from raw prices only no pre-computed indicators in prompt.
*/
public function generateLlmPredictionWithContext(Collection $prices): ?PricePrediction
{
$priceList = $prices->sortBy('date')
->map(fn (BrentPrice $p) => "{$p->date->toDateString()}: \${$p->price_usd}")
->implode("\n");
$prompt = <<<PROMPT
You are analyzing Brent crude oil price data for a UK fuel price alert service.
Your goal is to predict the short-term direction over the next 35 days.
First, search for recent news (last 48 hours) about:
- Brent crude oil price movements
- OPEC+ production decisions or announcements
- Major geopolitical events affecting oil supply (Middle East, Russia, US sanctions)
- Global demand signals (China economic data, US inventory reports)
Then, combining the news context with the price history below, predict the direction.
Recent Brent crude prices (USD/barrel):
{$priceList}
Respond with JSON only, no other text:
{"direction": "rising|falling|flat", "confidence": 0-85, "reasoning": "one sentence combining price trend and key news factor"}
PROMPT;
$url = 'https://api.anthropic.com/v1/messages';
$messages = [['role' => 'user', 'content' => $prompt]];
$response = null;
try {
for ($i = 0; $i < 5; $i++) {
$response = $this->apiLogger->send('anthropic', 'POST', $url, fn () => Http::timeout(30)
->withHeaders([
'x-api-key' => config('services.anthropic.api_key'),
'anthropic-version' => '2023-06-01',
])
->post($url, [
'model' => config('services.anthropic.model', 'claude-sonnet-4-6'),
'max_tokens' => 1024,
'tools' => [['type' => 'web_search_20260209', 'name' => 'web_search']],
'messages' => $messages,
]));
if (! $response->successful()) {
Log::error('OilPriceService: Anthropic context request failed', ['status' => $response->status()]);
return null;
}
if ($response->json('stop_reason') !== 'pause_turn') {
break;
}
$messages[] = ['role' => 'assistant', 'content' => $response->json('content')];
}
$text = collect($response->json('content') ?? [])
->firstWhere('type', 'text')['text'] ?? '';
$text = preg_replace('/^```(?:json)?\s*/m', '', trim($text));
$text = preg_replace('/```\s*$/m', '', $text);
$data = json_decode(trim($text), true);
if (! isset($data['direction'], $data['confidence'], $data['reasoning'])) {
Log::error('OilPriceService: unexpected context LLM response format', ['text' => $text]);
return null;
}
$direction = TrendDirection::tryFrom($data['direction']);
$confidence = min((int) $data['confidence'], self::LLM_MAX_CONFIDENCE);
if ($direction === null) {
Log::error('OilPriceService: invalid direction in context LLM response', ['direction' => $data['direction']]);
return null;
}
return new PricePrediction([
'predicted_for' => now()->toDateString(),
'source' => PredictionSource::Llm,
'direction' => $direction,
'confidence' => $confidence,
'reasoning' => $data['reasoning'],
'generated_at' => now(),
]);
} catch (Throwable $e) {
Log::error('OilPriceService: generateLlmPredictionWithContext failed', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Option A EWMA-based trend extrapolation. Used as fallback when LLM is unavailable.
* Compares the 3-day EWMA against the 7-day EWMA to detect direction.