Backup #1358

ID1358
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Infrastructure/AI/ClaudeService.php
Version4
Typ modified
Größe10.2 KB
Hash4cf5e1db869fd4422237b69cd6cfc4e6cffddf62e9c4b6819c65209162d11303
Datum2025-12-25 16:55:10
Geändert vonclaude-code-hook
GrundClaude Code Pre-Hook Backup vor Edit-Operation
Datei existiert Ja

Dateiinhalt

<?php

declare(strict_types=1);

namespace Infrastructure\AI;

// @responsibility: Anthropic Claude API für RAG-Antworten

use RuntimeException;

final readonly class ClaudeService
{
    /**
     * Default timeout for HTTP requests in seconds (LLM responses can take time).
     */
    private const int DEFAULT_TIMEOUT = 120;

    /**
     * Health check timeout in seconds.
     */
    private const int HEALTH_CHECK_TIMEOUT = 5;

    /**
     * Anthropic API base URL.
     */
    private const string API_BASE_URL = 'https://api.anthropic.com/v1';

    /**
     * Anthropic API version header.
     */
    private const string API_VERSION = '2023-06-01';

    /**
     * Constructs a new ClaudeService instance.
     *
     * @param string $apiKey The Anthropic API key (sk-ant-...)
     */
    public function __construct(
        private string $apiKey
    ) {
    }

    /**
     * Sends a prompt to Claude and receives a text response.
     *
     * Makes a request to the Anthropic Messages API with the specified prompt,
     * optional system prompt, model, token limit, and temperature. Returns the
     * generated text along with token usage statistics.
     *
     * @param string      $prompt       The user prompt to send
     * @param string|null $systemPrompt Optional system prompt for instructions (default: null)
     * @param string      $model        The Claude model to use (default: claude-opus-4-5-20251101)
     * @param int         $maxTokens    Maximum tokens in response (default: 4000)
     * @param float       $temperature  Sampling temperature 0.0-1.0 (default: 0.7)
     *
     * @return array{text: string, usage: array{input_tokens: int, output_tokens: int}} Response text and token usage
     *
     * @throws RuntimeException If the API request fails or returns invalid data
     *
     * @example
     * $service = new ClaudeService('sk-ant-...');
     * $result = $service->ask('Explain quantum computing', 'You are a physics teacher.', 'claude-opus-4-5-20251101', 4000, 0.7);
     * // Returns: [
     * //   'text' => 'Quantum computing is...',
     * //   'usage' => ['input_tokens' => 123, 'output_tokens' => 456]
     * // ]
     */
    public function ask(
        string $prompt,
        ?string $systemPrompt = null,
        string $model = 'claude-opus-4-5-20251101',
        int $maxTokens = 4000,
        float $temperature = 0.7
    ): array {
        $url = self::API_BASE_URL . '/messages';

        $payload = [
            'model' => $model,
            'max_tokens' => $maxTokens,
            'temperature' => max(0.0, min(1.0, $temperature)),
            'messages' => [
                [
                    'role' => 'user',
                    'content' => $prompt,
                ],
            ],
        ];

        if ($systemPrompt !== null) {
            $payload['system'] = $systemPrompt;
        }

        $response = $this->makeRequest($url, $payload, self::DEFAULT_TIMEOUT);

        if (!isset($response['content']) || !is_array($response['content'])) {
            throw new RuntimeException('Invalid response structure from Claude API');
        }

        if (!isset($response['content'][0]['text'])) {
            throw new RuntimeException('Missing text content in Claude API response');
        }

        $text = (string) $response['content'][0]['text'];

        $usage = [
            'input_tokens' => (int) ($response['usage']['input_tokens'] ?? 0),
            'output_tokens' => (int) ($response['usage']['output_tokens'] ?? 0),
        ];

        return [
            'text' => $text,
            'usage' => $usage,
        ];
    }

    /**
     * Builds a RAG-specific user prompt combining question and context.
     *
     * Creates a formatted prompt that presents retrieved context to Claude
     * along with the user's question, suitable for RAG (Retrieval-Augmented Generation).
     *
     * @param string $question The user's question
     * @param string $context  The retrieved context from vector search
     *
     * @return string The formatted RAG prompt
     *
     * @example
     * $service = new ClaudeService('sk-ant-...');
     * $prompt = $service->buildRagPrompt(
     *     'Was ist systemisches Coaching?',
     *     'Kontext: Systemisches Coaching betrachtet...'
     * );
     * // Returns: "Kontext aus den Dokumenten:\n\n...\n\n---\n\nFrage: Was ist systemisches Coaching?"
     */
    public function buildRagPrompt(string $question, string $context): string
    {
        return sprintf(
            "Kontext aus den Dokumenten:\n\n%s\n\n---\n\nFrage: %s",
            $context,
            $question
        );
    }

    /**
     * Returns the default system prompt for RAG use cases.
     *
     * Provides a standard German system prompt instructing Claude to answer
     * questions based on provided context for teamcoaching and team development topics.
     *
     * @return string The default system prompt
     *
     * @example
     * $service = new ClaudeService('sk-ant-...');
     * $systemPrompt = $service->getDefaultSystemPrompt();
     * $result = $service->ask($userPrompt, $systemPrompt);
     */
    public function getDefaultSystemPrompt(): string
    {
        return <<<'PROMPT'
            Du bist ein hilfreicher Assistent für Fragen zu systemischem Teamcoaching und Teamentwicklung.

            Beantworte die Frage des Nutzers basierend auf dem bereitgestellten Kontext.
            - Antworte auf Deutsch
            - Sei präzise und hilfreich
            - Wenn der Kontext die Frage nicht beantwortet, sage das ehrlich
            - Verweise auf die Quellen wenn passend
            PROMPT;
    }

    /**
     * Checks if the Claude API is available and responding.
     *
     * Performs a minimal health check by attempting to connect to the API.
     * Note: This is optional as Claude is an external service with its own status page.
     * Returns false on any error to avoid exposing API key in logs.
     *
     * @return bool True if the API appears available, false otherwise
     *
     * @example
     * $service = new ClaudeService('sk-ant-...');
     * if ($service->isAvailable()) {
     *     echo "Claude API is accessible";
     * } else {
     *     echo "Claude API is not available";
     * }
     */
    public function isAvailable(): bool
    {
        try {
            $ch = curl_init(self::API_BASE_URL . '/messages');

            if ($ch === false) {
                return false;
            }

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_CONNECTTIMEOUT => self::HEALTH_CHECK_TIMEOUT,
                CURLOPT_CUSTOMREQUEST => 'HEAD',
                CURLOPT_NOBODY => true,
            ]);

            $result = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

            curl_close($ch);

            // API returns 4xx for HEAD without auth, but connectivity is proven
            return $result !== false && ($httpCode === 400 || $httpCode === 401);
        } catch (\Throwable) {
            return false;
        }
    }

    /**
     * Makes an HTTP POST request to the Claude API.
     *
     * Internal helper method that handles cURL initialization, request execution,
     * error handling, and response parsing. API key is sent via x-api-key header
     * and is never logged or included in exception messages.
     *
     * @param string               $url     The API endpoint URL
     * @param array<string, mixed> $payload The JSON payload to send
     * @param int                  $timeout The request timeout in seconds
     *
     * @return array<string, mixed> The decoded JSON response
     *
     * @throws RuntimeException If the request fails or returns an error (without exposing API key)
     */
    private function makeRequest(string $url, array $payload, int $timeout): array
    {
        $ch = curl_init($url);

        if ($ch === false) {
            throw new RuntimeException('Failed to initialize cURL');
        }

        $jsonPayload = json_encode($payload);

        if ($jsonPayload === false) {
            curl_close($ch);

            throw new RuntimeException('Failed to encode JSON payload');
        }

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $jsonPayload,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'x-api-key: ' . $this->apiKey,
                'anthropic-version: ' . self::API_VERSION,
            ],
        ]);

        $result = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);

        curl_close($ch);

        if ($result === false) {
            throw new RuntimeException(
                sprintf('cURL request failed: %s', $curlError !== '' ? $curlError : 'Unknown error')
            );
        }

        if ($httpCode !== 200) {
            // Never expose API key in error messages - sanitize response
            $sanitizedResponse = $this->sanitizeErrorResponse((string) $result);

            throw new RuntimeException(
                sprintf('Claude API returned HTTP %d: %s', $httpCode, $sanitizedResponse)
            );
        }

        $decoded = json_decode((string) $result, true);

        if (!is_array($decoded)) {
            throw new RuntimeException('Failed to decode JSON response from Claude API');
        }

        return $decoded;
    }

    /**
     * Sanitizes error responses to prevent API key leakage.
     *
     * Removes or masks any potential API key references from error messages
     * before they are included in exceptions or logs.
     *
     * @param string $response The raw error response
     *
     * @return string The sanitized error response
     */
    private function sanitizeErrorResponse(string $response): string
    {
        // Limit response length to prevent verbose error dumps
        if (strlen($response) > 500) {
            $response = substr($response, 0, 500) . '... (truncated)';
        }

        // Remove any potential API key patterns (sk-ant-...)
        return preg_replace('/sk-ant-[a-zA-Z0-9_-]+/', '[API_KEY_REDACTED]', $response) ?? $response;
    }
}

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
1386 16 modified 10.5 KB 2025-12-25 16:57
1383 15 modified 10.6 KB 2025-12-25 16:57
1377 14 modified 10.6 KB 2025-12-25 16:56
1376 13 modified 10.7 KB 2025-12-25 16:56
1375 12 modified 10.8 KB 2025-12-25 16:56
1374 11 modified 10.8 KB 2025-12-25 16:56
1370 10 modified 10.8 KB 2025-12-25 16:56
1366 9 modified 10.8 KB 2025-12-25 16:55
1365 8 modified 10.7 KB 2025-12-25 16:55
1363 7 modified 10.7 KB 2025-12-25 16:55
1361 6 modified 10.6 KB 2025-12-25 16:55
1359 5 modified 10.3 KB 2025-12-25 16:55
1358 4 modified 10.2 KB 2025-12-25 16:55
750 3 modified 10.6 KB 2025-12-23 07:59
746 2 modified 10.8 KB 2025-12-23 07:58
50 1 modified 10.5 KB 2025-12-20 18:29

← Zurück zur Übersicht