Files
ladill-meet/app/Services/Meet/MeetAiService.php
T
isaaccladandCursor 965fb992e9
Deploy Ladill Meet / deploy (push) Failing after 7s
Initial Ladill Meet release.
Phases 0–18: core meetings, webinar, breakouts, team chat, live streaming, town hall, billing, and Ladill Mail calendar wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:35:29 +00:00

147 lines
4.6 KiB
PHP

<?php
namespace App\Services\Meet;
use App\Models\AiSummary;
use App\Models\Transcript;
use App\Models\ActionItem;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class MeetAiService
{
public function enabled(): bool
{
return (bool) config('meet.ai.enabled', true)
&& ((string) config('meet.ai.api_key') !== '' || $this->hasPlatformRelay());
}
public function summarize(Transcript $transcript): AiSummary
{
$summary = AiSummary::create([
'owner_ref' => $transcript->owner_ref,
'session_id' => $transcript->session_id,
'transcript_id' => $transcript->id,
'status' => 'processing',
]);
try {
$result = $this->generateSummary((string) $transcript->content);
$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(string $transcript): array
{
if (! $this->enabled()) {
return $this->stubSummary($transcript);
}
$prompt = <<<PROMPT
Analyze this meeting transcript and respond in JSON with keys:
- summary (string, 2-4 paragraphs)
- decisions (array of strings)
- action_items (array of {assignee, description, due_date or null})
Transcript:
{$transcript}
PROMPT;
$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 stubSummary(string $transcript): array
{
$preview = Str::limit($transcript, 500);
return [
'summary' => "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(60)
->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 transcripts. Return valid JSON only.'],
['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(60)
->post($base.'/afia/chat', [
'product' => 'meet',
'message' => $prompt,
'history' => [],
]);
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') !== '';
}
}