Skip to main content

Meeting Insights

Meeting Insights are AI-generated analyses of interview meeting transcripts. When a meeting on book.aiqlick.com has a transcript (produced by the Transcription System), an insight can be generated either manually by the user or automatically when the meeting ends. The LLM produces a structured report covering skills assessment, communication analysis, key topics, and red flags.

How It Works

There are two trigger paths for generating an insight: manual (user-initiated) and automatic (webhook-triggered when a meeting ends).

Manual Trigger

  1. The user clicks Generate Insight in the frontend meeting detail view.
  2. The frontend calls the backend initializeMeetingInsight GraphQL mutation, which creates a MeetingInsight record with status GENERATING and increments the version number. Previous versions are marked STALE.
  3. The frontend connects to background-tasks via graphql-ws and subscribes to the generateMeetingInsight subscription.
  4. Background-tasks streams real-time progress updates (MeetingInsightProcessing) with percentage and status messages as the LLM processes the transcript.
  5. On success, it emits MeetingInsightSuccess with the full insight data. The frontend then refetches the latest insight from the backend.
  6. On failure, it emits MeetingInsightFailure with an error code and message.

Auto Trigger

  1. When a Jitsi meeting ends, Prosody fires the muc-room-destroyed event.
  2. The mod_event_sync_component Lua plugin sends a POST request to https://ai.aiqlick.com/api/v1/jitsi/events/room/destroyed with a 2-second delay (to allow occupant-left events to fire first).
  3. The background-tasks handler validates the request, skips breakout rooms, and checks that auto-generation is enabled (AUTO_GENERATE_MEETING_INSIGHTS env var, default: true).
  4. A background task runs _trigger_insight_generation(), which:
    • Verifies the transcript exists and exceeds 100 characters
    • Checks no insight is already GENERATING or COMPLETED (idempotency)
    • Looks up the interviewId via MeetingInterviewBookingInterview
    • Updates Meeting.status to COMPLETED
    • Calls MeetingInsightService.generate_insight()

Prosody Event Sync Plugin

The mod_event_sync_component.lua plugin is the bridge between Jitsi's XMPP layer and the background-tasks webhook API. It runs on Prosody's MUC component (muc.meet.jitsi).

Configuration

The plugin is loaded via the XMPP_MUC_MODULES environment variable in docker-compose.yml:

XMPP_MUC_MODULES=event_sync_component

The plugin file lives in prosody-plugins-custom/ and is mounted into the Prosody container via a Docker Compose volume.

Config OptionDefaultDescription
event_sync_webhook_urlhttps://ai.aiqlick.com/api/v1/jitsi/eventsBase URL for webhook POSTs
event_sync_webhook_secret(empty)Secret sent as X-Webhook-Secret header

Hooks

The plugin registers four Prosody hooks:

HookWebhook PathPayload
muc-room-created/room/createdroom_name, room_jid, created_at
muc-room-destroyed/room/destroyedroom_name, room_jid, occupant_count, occupants, destroyed_at
muc-occupant-joined/occupant/joinedroom_name, occupant_jid, nick, joined_at
muc-occupant-left/occupant/leftroom_name, occupant_jid, nick, left_at

Behavior Details

  • Hidden domain filtering: Occupants with JIDs containing hidden.meet.jitsi (transcriber bots like Jigasi) are excluded from join/leave events.
  • Delayed room-destroyed: The muc-room-destroyed event uses a 2-second timer.add_task delay to ensure all occupant-left events have fired before the webhook is sent.
  • Room name extraction: The room name is extracted from the room JID by taking the local part before @ (e.g., myroom@muc.meet.jitsi becomes myroom).

Meeting ID Correlation

Jigasi connects via WebSocket with a random UUID, but Prosody sends the actual room_name in webhooks. The background-tasks jitsi_events.py module maintains an in-memory correlation map:

  1. On room/created, the room_name is registered as a pending room with a timestamp.
  2. When Jigasi connects via WebSocket at /transcription/ws/{meeting_id}, correlate_websocket_meeting_id() matches the UUID to the most recent pending room within a 60-second window.
  3. Transcriptions are stored under the room_name (not the UUID), enabling the insight generator to find them.

Meeting Record Auto-Creation

The MeetingInsight table has a foreign key to Meeting.id. If the interview booking flow did not create a Meeting record (e.g., due to a unique constraint conflict or a transient error), the insight service auto-creates one on demand.

The auto-creation logic in MeetingInsightModel._get_or_create_meeting_uuid() tries two strategies in order:

  1. From InterviewBooking — Looks up InterviewBooking.externalBookingId (which stores the Jitsi room name). If found, creates a Meeting record with the booking's organizer, company, schedule, and interview link.
  2. From Transcription data — If no booking exists, looks for a user in the Transcription table for that room. Creates a minimal Meeting with that user as organizer. If no transcription user is found, auto-creation fails and the insight generation returns an error.
note

Auto-creation only runs on write operations (create_insight, create_generating_insight). Read operations like get_latest_by_meeting use a pure lookup that never creates records.

Jitsi Moderator Panel Authentication

The "Generate Insights" button in the Jitsi moderator panel requires a valid JWT token. The token is extracted from the Jitsi Redux state (state['features/base/jwt'].jwt) and passed in the WebSocket connection_init payload as Authorization: Bearer <token>.

If no JWT is present (e.g., a guest user joining without authentication), the button shows "Authentication required — Please sign in through AIQLick to generate meeting insights."

Insight Generation Pipeline

The MeetingInsightService in background-tasks orchestrates the generation process through these steps:

StepProgressDescription
Initializing5%Creates a placeholder MeetingInsight record with status GENERATING
Fetching transcript10%Retrieves transcript text from the Transcription table by meetingId
Fetching context20%Queries job title, description, requirements, and candidate skills/experience
Billing pre-check20%Validates subscription and credit balance via BillingGate
Generating30%Sends transcript + context to LLM (30-60 seconds)
Saving90%Updates MeetingInsight record with structured results
Completed100%Deducts credits via BillingGate.check_and_consume()

LLM Configuration

ParameterValue
ModelClaude Haiku 4.5 (eu.anthropic.claude-haiku-4-5-20251001-v1:0)
Max tokens8192 (2x default)
Temperature0.3
ProviderAWS Bedrock (eu-north-1)

Haiku 4.5 is chosen over Sonnet for meeting insights because it is approximately 50% faster, and the structured analysis format benefits less from the additional reasoning capability of larger models.

Insight Content

A completed MeetingInsight contains the following structured data:

fullReport

A markdown-formatted comprehensive report of the meeting analysis, including all sections below in narrative form.

skillsAssessment

FieldTypeDescription
matchedSkillsArraySkills the candidate demonstrated, with skillName and demonstratedLevel
missingSkillsArrayRequired skills the candidate did not demonstrate
overallSkillMatchPercentageNumberOverall skill match score (0-100%)

communicationAnalysis

FieldTypeDescription
clarityScoreNumberHow clearly the candidate communicated (0-100)
confidenceScoreNumberConfidence level displayed (0-100)
professionalismScoreNumberProfessional conduct score (0-100)
strengthsArrayCommunication strengths observed
areasForImprovementArrayAreas where communication could improve

keyTopicsSummary

FieldTypeDescription
topicsArrayMain topics discussed during the meeting
questionsArrayKey questions asked by the interviewer
candidateQuestionsArrayQuestions asked by the candidate

redFlagsAndConcerns

FieldTypeDescription
inconsistenciesArrayInconsistencies found in candidate's responses
experienceGapsArrayGaps in experience relative to job requirements
behavioralConcernsArrayBehavioral issues noted during the interview
overallRiskLevelStringRisk assessment: LOW, MEDIUM, or HIGH

Metadata Fields

FieldDescription
versionIncrementing version number per meeting
statusGENERATING, COMPLETED, FAILED, STALE
llmModelModel ID used for generation
llmTokensUsedTotal tokens consumed
processingTimeMsWall-clock time for generation
transcriptLengthCharacter count of the transcript
generatedAtTimestamp of completion
errorMessageError details (when status is FAILED)

Database Model

The MeetingInsight table stores all generated insights:

ColumnTypeNotes
idUUIDPrimary key
meetingIdUUIDFK to Meeting
interviewIdUUID (optional)FK to Interview
versionIntAuto-incrementing per meeting
statusEnumGENERATING, COMPLETED, FAILED, STALE
fullReportTextMarkdown report
skillsAssessmentJSONStructured skills data
communicationAnalysisJSONCommunication scores
keyTopicsSummaryJSONTopics and questions
redFlagsAndConcernsJSONRisk assessment

Versioning

Each regeneration creates a new row with an incremented version. When a new insight is initialized, all previous non-STALE versions are marked STALE. The latest non-STALE insight is the active one.

Access Control

Access to insights is controlled via the meeting relationship:

  • Meeting organizer always has access
  • Meeting attendees have access
  • Users belonging to the meeting's company have access
  • Only the meeting organizer can delete insights

Frontend Integration

The useMeetingInsights hook (hooks/useMeetingInsights.ts) provides a complete interface for working with meeting insights.

Usage

const {
insight, // Current MeetingInsight or null
generationState, // { status, progress, message }
history, // All versions for this meeting
isGenerating, // Boolean shorthand
hasInsight, // Boolean: completed insight exists
canRegenerate, // Boolean: not currently generating
generateInsight, // (forceRefresh?: boolean) => Promise<void>
refreshInsight, // () => Promise<void>
deleteInsight, // (id: string) => Promise<void>
stopGeneration, // () => void — cancel active subscription
} = useMeetingInsights({
meetingId: 'room-abc123',
interviewId: 'interview-uuid', // optional
autoFetch: true, // fetch latest on mount
});

Generation Flow in the Hook

  1. generateInsight() calls the backend initializeMeetingInsight mutation to create a GENERATING record.
  2. It then opens a subscribeAi subscription on the same Apollo client (no separate WS), naming the operation generateMeetingInsight. The NestJS AI gateway proxies it to background-tasks.
  3. Subscribes to generateMeetingInsight which returns a union type: MeetingInsightProcessing | MeetingInsightSuccess | MeetingInsightFailure.
  4. MeetingInsightProcessing updates provide real progress percentage and status messages from the LLM pipeline.
  5. On MeetingInsightSuccess, the hook refetches the latest insight from the backend (to get all relations).
  6. On MeetingInsightFailure, the error is stored and the subscription is cleaned up.
  7. The WebSocket client is disposed on completion, failure, or component unmount.

GraphQL Operations

OperationTypePurpose
INITIALIZE_MEETING_INSIGHTMutationCreates a GENERATING record, returns insight ID
GET_MEETING_INSIGHTQueryFetches a specific insight by ID (used for polling)
GET_LATEST_MEETING_INSIGHTQueryFetches the latest non-STALE insight for a meeting
GET_MEETING_INSIGHTS_HISTORYQueryFetches all versions for a meeting
DELETE_MEETING_INSIGHTMutationDeletes an insight (organizer only)
note

The frontend subscribes via the NestJS AI gateway (subscribeAi with operation: 'generateMeetingInsight') over the same NEXT_PUBLIC_GRAPHQL_WS_URL WebSocket as the rest of the backend. The gateway proxies the upstream generateMeetingInsight subscription to background-tasks; the frontend never opens a direct connection there. See NestJS AI Gateway.

Environment Variables

VariableServiceDefaultDescription
AUTO_GENERATE_MEETING_INSIGHTSbackground-taskstrueEnable auto-generation on meeting end
JITSI_WEBHOOK_SECRETbackground-tasks(none)Bearer token to validate Prosody webhook requests
event_sync_webhook_urlProsodyhttps://ai.aiqlick.com/api/v1/jitsi/eventsWebhook base URL
event_sync_webhook_secretProsody(empty)Secret header for webhook auth
XMPP_MUC_MODULESProsodyMust include event_sync_component

Billing

Meeting insight generation is a billable AI operation. The MeetingInsightService integrates with BillingGate:

  1. Pre-check: Before the LLM call, BillingGate.pre_check(company_id, "MEETING_INSIGHT") validates the company has an active subscription and sufficient credits.
  2. Consumption: After successful generation, BillingGate.check_and_consume() deducts credits based on token usage.
  3. Company resolution: The company_id is resolved from the meeting's linked interview and job chain: MeetingInterviewBookingInterviewJob.companyId.
warning

If no company_id can be resolved (e.g., ad-hoc meetings without an interview link), the billing gate is skipped entirely. The insight is generated without credit deduction.

Troubleshooting

Insight not generated automatically after meeting ends

  1. Check Prosody plugin is loaded: Verify XMPP_MUC_MODULES includes event_sync_component in the Prosody container environment.
  2. Check webhook delivery: Look at Prosody logs for Event sent successfully or Event webhook failed messages.
    docker compose logs prosody | grep -i "event"
  3. Check background-tasks received the webhook: Look for Received room destroyed event in background-tasks logs.
  4. Check transcript length: Auto-generation requires at least 1 character of transcript. If the transcription failed entirely, the insight is skipped.
  5. Check idempotency: If an insight already exists with status GENERATING or COMPLETED, the auto-trigger skips to avoid duplicates.

Insight stuck in GENERATING status

  • The manual trigger creates a GENERATING record via the backend, then connects to background-tasks via WebSocket. If background-tasks is down, the subscription fails and the record stays in GENERATING.
  • Check background-tasks health: curl https://ai.aiqlick.com/health
  • The frontend will show the subscription error. Refresh the page and try again once background-tasks is healthy.

"Transcript too short" error

The insight generator requires at least 1 character of transcript text. This typically means:

  • The transcription service (Jigasi) was not running during the meeting.
  • There was a meeting ID correlation failure (see Meeting ID Correlation above).
  • JVB failed to forward audio to Jigasi (check Jigasi logs for "Colibri WebSocket URL unavailable").

"Meeting not found" error

This previously blocked insight generation when no Meeting record existed for the Jitsi room name. As of April 2026, the service auto-creates the Meeting record from InterviewBooking data. If this error still occurs, it means:

  • No InterviewBooking exists with a matching externalBookingId (room name).
  • No transcription users were found for the room.
  • Check if the interview was booked through a non-standard flow that didn't create a booking record.

Billing pre-check fails

If the company's subscription is inactive or credits are exhausted, the insight generation fails with a billing error. Check the company's subscription status and credit balance in the admin dashboard.

Webhook authentication fails

In production, the JITSI_WEBHOOK_SECRET must be configured on the background-tasks side. If Prosody sends requests without the matching secret, they are rejected with HTTP 401. Verify both sides have the same secret configured.