validate([ 'message' => 'required|string|max:2000', 'conversation_history' => 'array', 'conversation_history.*.role' => 'required_with:conversation_history|in:user,assistant', 'conversation_history.*.content' => 'required_with:conversation_history|string', 'with_audio' => 'boolean', ]); $user = $request->user(); $user->load('subscription.plan'); // Credit-Limit prüfen if ($user->effective_limit > 0 && $user->monthly_usage >= $user->effective_limit * 1.05) { return response()->json([ 'success' => false, 'message' => 'Dein monatliches Credit-Limit ist erreicht.', 'errors' => ['credits' => 'Limit überschritten'], ], 429); } $aiConfig = $user->subscription?->plan?->ai_config ?? [ 'model' => config('services.openai.model', 'gpt-4o-mini'), 'temperature' => 0.5, 'max_tokens' => 1500, 'input_cost' => 0.00015, 'output_cost' => 0.0006, ]; // Konversation aufbauen $history = $request->input('conversation_history', []); $history[] = ['role' => 'user', 'content' => $request->message]; // User-Kontext erstellen (geteilter Service → Web + API identisch) $userContext = app(AgentContextService::class)->build($user); $startTime = microtime(true); // AI aufrufen \Log::info('AgentChat: Calling AI', [ 'user_id' => $user->id, 'message' => mb_substr($request->message, 0, 100), 'history_count' => count($history), 'model' => $aiConfig['model'] ?? 'default', ]); try { $aiResult = AgentAIService::chat($history, $aiConfig, $userContext); \Log::info('AgentChat: AI result', [ 'type' => $aiResult['type'] ?? 'unknown', 'has_message' => !empty($aiResult['data']['message'] ?? $aiResult['message'] ?? ''), ]); } catch (\Throwable $e) { \Log::error('AgentChat: AI exception', [ 'error' => $e->getMessage(), 'class' => get_class($e), ]); report($e); return response()->json([ 'success' => true, 'data' => [ 'message' => 'Entschuldigung, ich konnte gerade nicht antworten. Bitte versuche es nochmal.', 'action' => null, 'type' => 'chat', 'credits_used' => 0, ], ]); } // _error-Flag: OpenAI API down oder Timeout — 0 Credits, Fehler loggen if (!empty($aiResult['_error'])) { $durationMs = (int) ((microtime(true) - $startTime) * 1000); AgentLog::create([ 'user_id' => $user->id, 'type' => 'chat', 'input' => mb_substr($request->message, 0, 100), 'status' => 'error', 'output' => null, 'credits' => 0, 'ai_response' => null, 'model' => $aiConfig['model'] ?? null, 'duration_ms' => $durationMs, 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0, ]); return response()->json([ 'success' => true, 'data' => [ 'message' => $aiResult['data']['message'] ?? 'Entschuldigung, bitte versuche es nochmal.', 'action' => null, 'type' => 'chat', 'credits_used' => 0, 'usage' => [ 'credits_used' => $user->monthly_usage, 'credits_limit' => $user->effective_limit, 'usage_percent' => $user->usage_percent, ], ], ]); } // AgentAIService gibt direkt das parsed-Objekt zurück (kein 'parsed'-Wrapper) // Keys: type, data, _usage, _multi (bei Multi-Actions) $parsed = $aiResult; $usage = $aiResult['_usage'] ?? []; $durationMs = (int) ((microtime(true) - $startTime) * 1000); $actionResult = null; $assistantMessage = $parsed['data']['message'] ?? $parsed['message'] ?? $parsed['text'] ?? ''; // Aktion ausführen falls vorhanden try { if (isset($parsed['_multi'])) { $actionService = new AgentActionService(); $results = []; foreach ($parsed['_multi'] as $action) { $results[] = $actionService->handle($user, $action); } $actionResult = [ 'status' => collect($results)->every(fn ($r) => $r['status'] === 'success') ? 'success' : 'partial', 'results' => $results, ]; } elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') { $actionService = new AgentActionService(); $actionResult = $actionService->handle($user, $parsed); } } catch (\Throwable $e) { report($e); $actionResult = ['status' => 'error', 'message' => 'Aktion fehlgeschlagen']; } // Wenn Aktion ausgeführt aber keine Message von der AI → Action-Message verwenden if (empty($assistantMessage) && $actionResult) { $assistantMessage = $actionResult['message'] ?? 'Erledigt!'; } // Letzter Schutz: JSON darf NIE vorgelesen werden if (is_string($assistantMessage) && $assistantMessage !== '') { $trim = ltrim($assistantMessage); $isJsonLike = $trim !== '' && ($trim[0] === '{' || $trim[0] === '[') || str_contains($assistantMessage, '"type":') || str_contains($assistantMessage, '"data":'); if ($isJsonLike) { \Log::warning('AgentChat: JSON leaked to assistant message — replaced', [ 'preview' => mb_substr($assistantMessage, 0, 200), ]); $assistantMessage = $actionResult['message'] ?? 'Erledigt!'; } } // Credits berechnen — Flat-Rate-Logik // - Aktionen: tokenbasiert (wie bisher) // - Erster Chat einer Session (history leer): pauschal 5 Credits // - Folge-Chat-Nachrichten: 0 Credits, kein Log $type = $parsed['type'] ?? 'chat'; $isAction = $type !== 'chat'; $historyCount = count($request->input('conversation_history', [])); $shouldLog = true; if ($isAction) { $credits = (($actionResult['status'] ?? '') === 'error') ? 0 : $this->calculateCredits($usage, $aiConfig, $type); } elseif ($historyCount === 0) { $credits = 5; } else { $credits = 0; $shouldLog = false; } if ($shouldLog) { // Input für Log bestimmen (wie im Web) $logInput = match ($type) { 'chat' => 'Konversation', 'event', 'event_update' => $parsed['data']['title'] ?? $request->message, 'note', 'note_update' => mb_substr($parsed['data']['content'] ?? $request->message, 0, 100), 'task', 'task_update' => $parsed['data']['title'] ?? $request->message, 'contact' => $parsed['data']['name'] ?? $request->message, 'email' => 'E-Mail an ' . ($parsed['data']['contact'] ?? '') . ': ' . mb_substr($parsed['data']['subject'] ?? '', 0, 50), 'multi' => $request->message, default => mb_substr($request->message, 0, 200), }; $logStatus = ($actionResult['status'] ?? 'success') === 'error' ? 'failed' : ($actionResult['status'] ?? 'success'); AgentLog::create([ 'user_id' => $user->id, 'type' => $type, 'input' => $logInput, 'status' => $logStatus, 'output' => $actionResult['meta'] ?? $actionResult ?? null, 'credits' => $credits, 'ai_response' => $parsed, 'model' => $aiConfig['model'] ?? null, 'duration_ms' => $durationMs, 'prompt_tokens' => $usage['prompt_tokens'] ?? 0, 'completion_tokens' => $usage['completion_tokens'] ?? 0, 'total_tokens' => $usage['total_tokens'] ?? 0, 'cost_usd' => $this->calculateCostUsd($usage, $aiConfig), ]); } // TTS falls gewünscht (Fehler darf Chat nicht blockieren) $audio = null; if ($request->boolean('with_audio') && $assistantMessage) { try { $audio = AgentAIService::textToSpeech($assistantMessage, $aiConfig); } catch (\Throwable $e) { report($e); } } // Usage neu berechnen $user->refresh(); $responseData = [ 'message' => $assistantMessage, 'action' => $actionResult, 'type' => $parsed['type'] ?? 'chat', 'credits_used' => $credits, 'usage' => [ 'credits_used' => $user->monthly_usage, 'credits_limit' => $user->effective_limit, 'usage_percent' => $user->usage_percent, ], ]; if ($audio) { $responseData['audio'] = $audio; } return response()->json([ 'success' => true, 'data' => $responseData, ]); } public function synthesize(Request $request): JsonResponse { $request->validate([ 'text' => 'required|string|max:500', 'locale' => 'nullable|string', ]); $user = $request->user(); $aiConfig = $user->subscription?->plan?->ai_config ?? []; if (is_string($aiConfig)) { $aiConfig = json_decode($aiConfig, true); } // Voice je nach Locale wählen $voice = str_starts_with($request->input('locale', 'de'), 'en') ? 'shimmer' : 'nova'; $overrideConfig = array_merge($aiConfig, ['tts_voice' => $voice]); $audio = AgentAIService::textToSpeech($request->text, $overrideConfig); return response()->json([ 'success' => true, 'data' => ['audio' => $audio], ]); } private function calculateCredits(array $usage, array $aiConfig, string $type): int { // Flat-Basis + Output-Tokens. Kontext (prompt_tokens) wird ignoriert, // damit Credits nicht mit Kalendergröße skalieren. Cap bei 100. $completionTokens = (int) ($usage['completion_tokens'] ?? 0); $credits = 20 + (int) ceil($completionTokens * 0.3); return min(100, max(1, $credits)); } private function calculateCostUsd(array $usage, array $aiConfig): float { $inputCost = $aiConfig['input_cost'] ?? 0.00015; $outputCost = $aiConfig['output_cost'] ?? 0.0006; return (($usage['prompt_tokens'] ?? 0) / 1000) * $inputCost + (($usage['completion_tokens'] ?? 0) / 1000) * $outputCost; } public function logs(Request $request): JsonResponse { $logs = $request->user() ->agentLogs() ->latest() ->take(20) ->get(['id', 'type', 'input', 'output', 'status', 'credits', 'duration_ms', 'created_at']); return response()->json([ 'success' => true, 'data' => $logs, ]); } }