Protokoll #15661
| ID | 15661 |
|---|---|
| Zeitstempel | 2025-12-25 16:56:15.159873 |
| Client | root |
| IP | Warning: Undefined array key "request_ip" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 |
| Modell | claude-sonnet-4-20250514 |
| Status | completed |
| Tokens | 2,677 (Input: 0, Output: 0) |
| Dauer | 89 ms |
| Request-Zeit | Warning: Undefined array key "request_timestamp" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 18 |
| Response-Zeit | - |
Warning: Undefined array key "error_message" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 22
Request
{
"event": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Infrastructure\/AI\/ClaudeService.php"
}
}
Response
{
"tool_response": {
"type": "text",
"file": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Infrastructure\/AI\/ClaudeService.php",
"content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\AI;\n\n\/\/ @responsibility: Anthropic Claude API für RAG-Antworten\n\nuse Infrastructure\\Config\\CredentialService;\nuse RuntimeException;\n\nfinal readonly class ClaudeService\n{\n \/**\n * Default timeout for HTTP requests in seconds (LLM responses can take time).\n *\/\n private const int DEFAULT_TIMEOUT = 120;\n\n \/**\n * Health check timeout in seconds.\n *\/\n private const int HEALTH_CHECK_TIMEOUT = 5;\n\n \/**\n * Anthropic API base URL.\n *\/\n private const string API_BASE_URL = 'https:\/\/api.anthropic.com\/v1';\n\n \/**\n * Anthropic API version header.\n *\/\n private const string API_VERSION = '2023-06-01';\n\n \/**\n * The Anthropic API key loaded from environment.\n *\/\n private readonly string $apiKey;\n\n \/**\n * Constructs a new ClaudeService instance.\n *\n * Loads the Anthropic API key from environment variables via CredentialService.\n * This ensures secrets are never hardcoded and follow security best practices.\n *\n * @param CredentialService $credentials Service for loading credentials from environment\n *\/\n public function __construct(\n CredentialService $credentials\n ) {\n $this->apiKey = $credentials->getAnthropicApiKey();\n }\n\n \/**\n * Sends a prompt to Claude and receives a text response.\n *\n * Makes a request to the Anthropic Messages API with the specified prompt,\n * optional system prompt, model, token limit, and temperature. Returns the\n * generated text along with token usage statistics.\n *\n * @param string $prompt The user prompt to send\n * @param string|null $systemPrompt Optional system prompt for instructions (default: null)\n * @param string $model The Claude model to use (default: claude-opus-4-5-20251101)\n * @param int $maxTokens Maximum tokens in response (default: 4000)\n * @param float $temperature Sampling temperature 0.0-1.0 (default: 0.7)\n *\n * @return array{text: string, usage: array{input_tokens: int, output_tokens: int}} Response text and token usage\n *\n * @throws RuntimeException If the API request fails or returns invalid data\n *\n * @example\n * $credentials = new CredentialService();\n * $service = new ClaudeService($credentials);\n * $result = $service->ask('Explain quantum computing', 'You are a physics teacher.', 'claude-opus-4-5-20251101', 4000, 0.7);\n * \/\/ Returns: [\n * \/\/ 'text' => 'Quantum computing is...',\n * \/\/ 'usage' => ['input_tokens' => 123, 'output_tokens' => 456]\n * \/\/ ]\n *\/\n public function ask(\n string $prompt,\n ?string $systemPrompt = null,\n string $model = 'claude-opus-4-5-20251101',\n int $maxTokens = 4000,\n float $temperature = 0.7\n ): array {\n $url = self::API_BASE_URL . '\/messages';\n\n $payload = [\n 'model' => $model,\n 'max_tokens' => $maxTokens,\n 'temperature' => max(0.0, min(1.0, $temperature)),\n 'messages' => [\n [\n 'role' => 'user',\n 'content' => $prompt,\n ],\n ],\n ];\n\n if ($systemPrompt !== null) {\n $payload['system'] = $systemPrompt;\n }\n\n $response = $this->makeRequest($url, $payload, self::DEFAULT_TIMEOUT);\n\n if (!isset($response['content']) || !is_array($response['content'])) {\n throw new RuntimeException('Invalid response structure from Claude API');\n }\n\n if (!isset($response['content'][0]['text'])) {\n throw new RuntimeException('Missing text content in Claude API response');\n }\n\n $text = (string) $response['content'][0]['text'];\n\n $usage = [\n 'input_tokens' => (int) ($response['usage']['input_tokens'] ?? 0),\n 'output_tokens' => (int) ($response['usage']['output_tokens'] ?? 0),\n ];\n\n return [\n 'text' => $text,\n 'usage' => $usage,\n ];\n }\n\n \/**\n * Builds a RAG-specific user prompt combining question and context.\n *\n * Creates a formatted prompt that presents retrieved context to Claude\n * along with the user's question, suitable for RAG (Retrieval-Augmented Generation).\n *\n * @param string $question The user's question\n * @param string $context The retrieved context from vector search\n *\n * @return string The formatted RAG prompt\n *\n * @example\n * $credentials = new CredentialService();\n * $service = new ClaudeService($credentials);\n * $prompt = $service->buildRagPrompt(\n * 'Was ist systemisches Coaching?',\n * 'Kontext: Systemisches Coaching betrachtet...'\n * );\n * \/\/ Returns: \"Kontext aus den Dokumenten:\\n\\n...\\n\\n---\\n\\nFrage: Was ist systemisches Coaching?\"\n *\/\n public function buildRagPrompt(string $question, string $context): string\n {\n return sprintf(\n \"Kontext aus den Dokumenten:\\n\\n%s\\n\\n---\\n\\nFrage: %s\",\n $context,\n $question\n );\n }\n\n \/**\n * Returns the default system prompt for RAG use cases.\n *\n * Provides a standard German system prompt instructing Claude to answer\n * questions based on provided context for teamcoaching and team development topics.\n *\n * @return string The default system prompt\n *\n * @example\n * $credentials = new CredentialService();\n * $service = new ClaudeService($credentials);\n * $systemPrompt = $service->getDefaultSystemPrompt();\n * $result = $service->ask($userPrompt, $systemPrompt);\n *\/\n public function getDefaultSystemPrompt(): string\n {\n return <<<'PROMPT'\n Du bist ein hilfreicher Assistent für Fragen zu systemischem Teamcoaching und Teamentwicklung.\n\n Beantworte die Frage des Nutzers basierend auf dem bereitgestellten Kontext.\n - Antworte auf Deutsch\n - Sei präzise und hilfreich\n - Wenn der Kontext die Frage nicht beantwortet, sage das ehrlich\n - Verweise auf die Quellen wenn passend\n PROMPT;\n }\n\n \/**\n * Checks if the Claude API is available and responding.\n *\n * Performs a minimal health check by attempting to connect to the API.\n * Note: This is optional as Claude is an external service with its own status page.\n * Returns false on any error to avoid exposing API key in logs.\n *\n * @return bool True if the API appears available, false otherwise\n *\n * @example\n * $credentials = new CredentialService();\n * $service = new ClaudeService($credentials);\n * if ($service->isAvailable()) {\n * echo \"Claude API is accessible\";\n * } else {\n * echo \"Claude API is not available\";\n * }\n *\/\n public function isAvailable(): bool\n {\n try {\n $ch = curl_init(self::API_BASE_URL . '\/messages');\n\n if ($ch === false) {\n return false;\n }\n\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_TIMEOUT => self::HEALTH_CHECK_TIMEOUT,\n CURLOPT_CONNECTTIMEOUT => self::HEALTH_CHECK_TIMEOUT,\n CURLOPT_CUSTOMREQUEST => 'HEAD',\n CURLOPT_NOBODY => true,\n ]);\n\n $result = curl_exec($ch);\n $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n\n curl_close($ch);\n\n \/\/ API returns 4xx for HEAD without auth, but connectivity is proven\n return $result !== false && ($httpCode === 400 || $httpCode === 401);\n } catch (\\Throwable) {\n return false;\n }\n }\n\n \/**\n * Makes an HTTP POST request to the Claude API.\n *\n * Internal helper method that handles cURL initialization, request execution,\n * error handling, and response parsing. API key is sent via x-api-key header\n * and is never logged or included in exception messages.\n *\n * @param string $url The API endpoint URL\n * @param array<string, mixed> $payload The JSON payload to send\n * @param int $timeout The request timeout in seconds\n *\n * @return array<string, mixed> The decoded JSON response\n *\n * @throws RuntimeException If the request fails or returns an error (without exposing API key)\n *\/\n private function makeRequest(string $url, array $payload, int $timeout): array\n {\n $ch = curl_init($url);\n\n if ($ch === false) {\n throw new RuntimeException('Failed to initialize cURL');\n }\n\n $jsonPayload = json_encode($payload);\n\n if ($jsonPayload === false) {\n curl_close($ch);\n\n throw new RuntimeException('Failed to encode JSON payload');\n }\n\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_POSTFIELDS => $jsonPayload,\n CURLOPT_TIMEOUT => $timeout,\n CURLOPT_CONNECTTIMEOUT => 10,\n CURLOPT_HTTPHEADER => [\n 'Content-Type: application\/json',\n 'x-api-key: ' . $this->apiKey,\n 'anthropic-version: ' . self::API_VERSION,\n ],\n ]);\n\n $result = curl_exec($ch);\n $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n $curlError = curl_error($ch);\n\n curl_close($ch);\n\n if ($result === false) {\n throw new RuntimeException(\n sprintf('cURL request failed: %s', $curlError !== '' ? $curlError : 'Unknown error')\n );\n }\n\n if ($httpCode !== 200) {\n \/\/ Never expose API key in error messages - sanitize response\n $sanitizedResponse = $this->sanitizeErrorResponse((string) $result);\n\n throw new RuntimeException(\n sprintf('Claude API r... [TRUNCATED-ea1c8386fb7777c6]",
"numLines": 325,
"startLine": 1,
"totalLines": 325
}
}
}