config = $config; $this->logger = \OC::$server->getLogger(); } /** * Check if LLM service is configured */ public function isConfigured(): bool { $apiKey = $this->config->getAppValue('anthropic_api_key', AppInfo::APP_NAME); return !empty($apiKey); } /** * Generate report from processed data */ public function generate(array $processedData, ClientConfig $client): string { $this->logger->info("Generating LLM report for: {$client->getName()}"); try { $apiKey = $this->config->getAppValue('anthropic_api_key', AppInfo::APP_NAME); if (empty($apiKey)) { throw new \Exception('Anthropic API key not configured'); } // Build prompt $systemPrompt = $this->buildSystemPrompt($client); $userPrompt = $this->buildUserPrompt($processedData); // Call with retry $response = $this->callWithRetry($systemPrompt, $userPrompt, $apiKey); return $response; } catch (\Exception $e) { $this->logger->error("LLM generation failed: {$e->getMessage()}"); throw $e; } } /** * Build system prompt */ private function buildSystemPrompt(ClientConfig $client): string { $context = $client->getContext(); $businessType = $context['business_type'] ?? 'business'; $tone = $context['tone'] ?? 'professional'; $focusAreas = $context['focus_areas'] ?? 'overall performance'; return <<getName()}, a {$businessType}. Your role: Transform analytics data into clear, actionable insights for business owner. TONE: {$tone} but conversational - avoid jargon. FOCUS: {$focusAreas} OUTPUT FORMAT (strict): # Weekly Analytics Snapshot - {report_date} ## 📊 Headline [One sentence summary of the biggest story this week] ## ✅ Key Wins [2-3 bullet points on positive movements or validations] ## 💡 Recommendation [One specific action based on the data] ## 📈 The Numbers [Formatted table of metrics with context] RULES: - Be specific (use actual numbers from data) - Focus on "why this matters" not just "what changed" - If a change is statistically insignificant, note it - Avoid phrases like "optimization" or "synergy" PROMPT; } /** * Build user prompt */ private function buildUserPrompt(array $processedData): string { $dataJson = json_encode($processedData, JSON_PRETTY_PRINT); return <<logger->info("LLM API call attempt {$attempt}/" . self::MAX_RETRIES); try { $response = $this->makeLLMRequest($systemPrompt, $userPrompt, $apiKey); // Validate response $this->validateResponse($response); return $response; } catch (\Exception $e) { $errorMessage = $e->getMessage(); // Check for rate limit if (str_contains($errorMessage, 'rate_limit') || str_contains($errorMessage, '429') || str_contains($errorMessage, 'too_many_requests')) { $this->logger->warning("Rate limited, waiting " . self::RATE_LIMIT_DELAY . "s"); if ($attempt < self::MAX_RETRIES - 1) { sleep(self::RATE_LIMIT_DELAY * $attempt); // Exponential backoff continue; } else { throw new RateLimitException("Rate limit exceeded after retries"); } } // Check for timeout if (str_contains($errorMessage, 'timeout') || str_contains($errorMessage, 'connection') || str_contains($errorMessage, 'timed out')) { if ($attempt < self::MAX_RETRIES - 1) { $this->logger->warning("Timeout, retrying..."); sleep(2 * $attempt); continue; } else { throw new TimeoutException("API timeout after retries"); } } // Other errors - log and continue $this->logger->error("LLM API error (attempt {$attempt}): {$errorMessage}"); if ($attempt < self::MAX_RETRIES - 1) { sleep(1); // Brief pause before retry continue; } else { throw $e; } } } throw new \Exception("Failed after " . self::MAX_RETRIES . " attempts"); } /** * Make HTTP request to Anthropic API */ private function makeLLMRequest(string $systemPrompt, string $userPrompt, string $apiKey): string { $ch = curl_init(); $payload = [ 'model' => 'claude-sonnet-4-5-20250929', 'max_tokens' => 2000, 'system' => $systemPrompt, 'messages' => [ [ 'role' => 'user', 'content' => $userPrompt ] ] ]; curl_setopt($ch, CURLOPT_URL, self::ANTHROPIC_API); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-api-key: ' . $apiKey, 'anthropic-version: 2023-06-01' ]); curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT_SECONDS); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $this->logger->error("Curl error: " . curl_error($ch)); curl_close($ch); throw new \Exception('HTTP request failed'); } curl_close($ch); if ($httpCode !== 200) { $this->logger->error("LLM API HTTP {$httpCode}"); throw new \Exception("API returned HTTP {$httpCode}"); } return $response; } /** * Validate response */ private function validateResponse(string $response): void { if (strlen($response) < 200) { throw new \Exception('Response too short - likely error'); } if (!str_contains($response, '# Weekly Analytics Snapshot')) { throw new \Exception('Missing required format'); } } }