hasPlatformRelay()); } public function summarize(Transcript $transcript): AiSummary { $transcript->loadMissing('session.room'); if ($transcript->session) { app(TranscriptService::class)->refreshContent($transcript, $transcript->session); $transcript->refresh(); } $summary = AiSummary::query() ->where('transcript_id', $transcript->id) ->first(); if ($summary) { $summary->actionItems()->delete(); $summary->update([ 'status' => 'processing', 'summary' => '', 'decisions' => [], ]); } else { $summary = AiSummary::create([ 'owner_ref' => $transcript->owner_ref, 'session_id' => $transcript->session_id, 'transcript_id' => $transcript->id, 'status' => 'processing', ]); } try { $result = $this->generateSummary($transcript); $summary->update([ 'status' => 'ready', 'summary' => $result['summary'] ?? '', 'decisions' => $result['decisions'] ?? [], ]); foreach ($result['action_items'] ?? [] as $item) { ActionItem::create([ 'owner_ref' => $transcript->owner_ref, 'ai_summary_id' => $summary->id, 'assignee_name' => $item['assignee'] ?? null, 'description' => $item['description'] ?? '', 'due_date' => $item['due_date'] ?? null, 'status' => 'open', ]); } } catch (\Throwable $e) { report($e); $summary->update(['status' => 'failed', 'summary' => 'AI summary could not be generated.']); } app(MeetNotificationService::class)->aiSummaryReady($summary->fresh()); return $summary->fresh(); } /** * @return array{summary: string, decisions: array, action_items: array} */ protected function generateSummary(Transcript $transcript): array { $content = trim((string) $transcript->content); if (! $this->enabled()) { return $this->stubSummary($content); } $prompt = $this->buildPrompt($transcript, $content); $reply = $this->hasLocalKey() ? $this->viaOpenAi($prompt) : $this->viaPlatform($prompt); $parsed = json_decode($reply, true); if (! is_array($parsed)) { return ['summary' => $reply, 'decisions' => [], 'action_items' => []]; } return $parsed; } protected function buildPrompt(Transcript $transcript, string $content): string { $session = $transcript->session; $room = $session?->room; $title = $room?->title ?? 'Meeting'; $duration = 'unknown'; if ($session?->started_at && $session?->ended_at) { $minutes = max(1, $session->started_at->diffInMinutes($session->ended_at)); $duration = $minutes.' minutes'; } $participantCount = $session?->participants()->where('status', 'joined')->count() ?? 0; $segments = $transcript->segments ?? []; $chatLineCount = collect($segments)->where('source', 'chat')->count(); $captionLineCount = collect($segments)->filter( fn (array $segment) => ($segment['source'] ?? 'caption') === 'caption', )->count(); $isSparse = $content === '' || $content === 'No transcript content available.' || strlen($content) < 120; $sourceNote = "The transcript combines in-meeting chat ({$chatLineCount} messages) " ."and live speech captions ({$captionLineCount} lines). " ."Caption lines are the primary record of spoken conversation — use them when chat is minimal.\n\n"; $sparseNote = $isSparse ? "IMPORTANT: The transcript below is very limited and does not include a full audio transcription. " ."If it does not contain enough detail to summarize the meeting accurately, say so clearly in the summary. " ."Do not invent topics, decisions, or action items that are not supported by the transcript.\n\n" : "Only summarize what is explicitly supported by the transcript below.\n\n"; return << "Meeting summary (AI not configured).\n\nTranscript preview:\n".$preview, 'decisions' => [], 'action_items' => [], ]; } protected function viaOpenAi(string $prompt): string { $res = Http::withToken((string) config('meet.ai.api_key'))->acceptJson()->timeout(120) ->post('https://api.openai.com/v1/chat/completions', [ 'model' => config('meet.ai.model', 'gpt-4o-mini'), 'temperature' => 0.2, 'response_format' => ['type' => 'json_object'], 'messages' => [ [ 'role' => 'system', 'content' => 'You summarize Ladill Meet sessions from text transcripts. Return valid JSON only. Never fabricate content missing from the transcript.', ], ['role' => 'user', 'content' => $prompt], ], ]); if ($res->failed()) { throw new RuntimeException('OpenAI request failed: '.$res->status()); } return trim((string) $res->json('choices.0.message.content', '')); } protected function viaPlatform(string $prompt): string { $base = rtrim((string) config('meet.ai.platform_api_url'), '/'); $res = Http::withToken((string) config('meet.ai.platform_api_key'))->acceptJson()->timeout(120) ->post($base.'/afia/chat', [ 'product' => 'meet', 'message' => $prompt, 'history' => [], 'system_prompt' => 'You summarize Ladill Meet sessions from text transcripts. Return valid JSON only. Never fabricate content missing from the transcript.', ]); if ($res->failed()) { throw new RuntimeException('Platform AI relay failed.'); } return trim((string) $res->json('reply', '')); } protected function hasLocalKey(): bool { return (string) config('meet.ai.api_key') !== ''; } protected function hasPlatformRelay(): bool { return rtrim((string) config('meet.ai.platform_api_url'), '/') !== '' && (string) config('meet.ai.platform_api_key') !== ''; } }