Skip to main content

Meeting Insights

Meeting Insights generates AI-powered analysis of interview transcripts. Given a meeting ID, the service fetches the transcript, sends it to AWS Bedrock Claude, and returns structured insight data including skills assessment, communication analysis, key topics, red flags, and an overall hire recommendation.

How Insights Are Triggered

There are two paths for generating insights:

  1. Automatic (recommended) -- When a Jitsi meeting ends, the Prosody Event Sync plugin sends a muc-room-destroyed webhook to POST /api/events/room/destroyed on background-tasks. If AUTO_GENERATE_MEETING_INSIGHTS=true (default), the service automatically generates an insight when the transcript has more than 100 characters.

  2. On-demand -- Users can trigger generation via the generateMeetingInsight GraphQL subscription or the POST /api/meeting-insights/generate REST endpoint.

info

Check for existing insights with GET /api/meeting-insights/{meeting_id} before triggering manual generation. Automatic generation may have already produced an insight by the time the user navigates to the meeting results page.

GraphQL Subscription

The subscription provides real-time progress updates and is the recommended approach for frontend integration.

subscription GenerateMeetingInsight($input: MeetingInsightInput!) {
generateMeetingInsight(input: $input) {
... on MeetingInsightProcessing {
id
status
message
progress
}
... on MeetingInsightSuccess {
id
status
insight {
id
meetingId
version
status
fullReport
skillsAssessment {
overallSkillMatchPercentage
skillSummary
matchedSkills { skillName demonstratedLevel evidence meetsRequirement }
missingSkills
additionalSkillsDemonstrated
}
communicationAnalysis {
clarityScore
confidenceScore
professionalismScore
communicationStyle
strengths
areasForImprovement
notableQuotes
overallAssessment
}
keyTopicsSummary {
mainTopicsDiscussed { topic candidateResponseQuality keyPoints }
questionsAsked
candidateQuestions
topicsNotCovered
}
redFlagsAndConcerns {
inconsistencies { description severity context }
experienceGaps
behavioralConcerns
followUpQuestionsRecommended
overallRiskLevel
}
overallRecommendation {
hireRecommendation
confidenceLevel
summary
strengthsSummary
concernsSummary
nextSteps
}
transcriptLength
processingTimeMs
llmModel
llmTokensUsed
generatedAt
}
}
... on MeetingInsightFailure {
id
status
error { code message timestamp recoveryHint }
}
}
}

Input

{
"input": {
"meetingId": "aiqlick-general-123456789",
"interviewId": null
}
}
FieldTypeRequiredDescription
meetingIdStringYesJitsi meeting room name (e.g., aiqlick-general-123456789)
interviewIdStringNoInterview UUID for additional job/candidate context in the analysis
warning

Meeting IDs are Jitsi room names, not UUIDs. The system resolves them internally to the database Meeting record.

Progress Updates

ProgressMessage
0%Initializing insight generation...
10%Fetching transcript...
20%Fetching job/candidate context...
30-90%Generating AI insights...
95%Saving results...
100%Success (full insight data returned)

Typical processing time: 30-60 seconds.

Insight Content Structure

SkillsAssessment

Evaluates how well the candidate's demonstrated skills match job requirements.

FieldTypeDescription
overallSkillMatchPercentageInt (0-100)Overall match score
skillSummaryStringNarrative summary
matchedSkills[MatchedSkill]Skills demonstrated with evidence
missingSkills[String]Required skills not demonstrated
additionalSkillsDemonstrated[String]Extra skills shown

Each MatchedSkill includes skillName, demonstratedLevel (BASIC/INTERMEDIATE/ADVANCED/EXPERT), evidence (quote or description), and meetsRequirement (boolean).

CommunicationAnalysis

Scores communication quality on three 0-10 scales.

FieldTypeDescription
clarityScoreInt (0-10)How clearly the candidate communicated
confidenceScoreInt (0-10)Confidence level displayed
professionalismScoreInt (0-10)Professional demeanor
communicationStyleStringOverall style description
strengths[String]Communication strengths
areasForImprovement[String]Improvement suggestions
notableQuotes[String]Notable direct quotes from the transcript

KeyTopicsSummary

Summarizes what was discussed and what was missed.

FieldTypeDescription
mainTopicsDiscussed[TopicDiscussed]Topics with response quality rating
questionsAsked[String]Questions the interviewer asked
candidateQuestions[String]Questions the candidate asked
topicsNotCovered[String]Important topics that were not discussed

Each TopicDiscussed rates candidateResponseQuality as POOR, FAIR, GOOD, or EXCELLENT.

RedFlagsAndConcerns

Identifies potential issues for follow-up.

FieldTypeDescription
inconsistencies[Inconsistency]Contradictions with severity (LOW/MEDIUM/HIGH)
experienceGaps[String]Gaps in experience timeline
behavioralConcerns[String]Behavioral red flags
followUpQuestionsRecommended[String]Suggested follow-up questions
overallRiskLevelEnumLOW, MEDIUM, HIGH

OverallRecommendation

The final hiring recommendation.

FieldTypeDescription
hireRecommendationEnumSTRONG_YES, YES, MAYBE, NO, STRONG_NO
confidenceLevelEnumLOW, MEDIUM, HIGH
summaryStringOverall assessment narrative
strengthsSummary[String]Key strengths
concernsSummary[String]Key concerns
nextSteps[String]Recommended next steps

Full Report

The fullReport field contains a complete markdown-formatted report combining all sections. Render it with a markdown component:

import ReactMarkdown from 'react-markdown';

function InsightReport({ insight }) {
return <ReactMarkdown>{insight.fullReport}</ReactMarkdown>;
}

Versioning

Multiple insight versions can exist for the same meeting. This supports regeneration after more transcript data becomes available (e.g., during a live meeting).

StatusDescription
GENERATINGCurrently being generated
COMPLETEDSuccessfully generated (immutable)
FAILEDGeneration failed
STALESuperseded by a newer version

When a new insight is generated, previous COMPLETED versions are marked STALE. The latest COMPLETED version is always the most relevant.

REST API Endpoints

MethodEndpointDescription
POST/api/meeting-insights/generateGenerate new insight (blocks 30-60s)
GET/api/meeting-insights/{meeting_id}Get latest completed insight
GET/api/meeting-insights/{meeting_id}/history?limit=10Get all versions
GET/api/meeting-insights/by-id/{insight_id}Get by insight ID
DELETE/api/meeting-insights/{meeting_id}Delete all insights for a meeting
GET/api/meeting-insights/health/checkService health check

Generate via REST

curl -X POST https://ai.aiqlick.com/api/meeting-insights/generate \
-H "Content-Type: application/json" \
-d '{"meeting_id": "aiqlick-general-123456789"}'
warning

The REST generate endpoint blocks for 30-60 seconds. Use the GraphQL subscription for a better user experience with real-time progress updates.

Get Latest Insight

curl https://ai.aiqlick.com/api/meeting-insights/aiqlick-general-123456789

Returns the most recent COMPLETED insight or null if none exists.

LLM Configuration

SettingValue
ModelClaude Haiku 4.5 (eu.anthropic.claude-haiku-4-5-20251001-v1:0)
Max tokens8192 (2x default)
Temperature0.3 (low for consistent analysis)
ProviderAWS Bedrock (eu-north-1)
Minimum transcript1 character

The LLM receives a system prompt positioning it as an "expert interview analyst" along with the full transcript, job context, and candidate context.

Error Handling

Common Errors

ErrorCauseSolution
"No transcript found"Meeting has no transcription dataEnsure transcription was enabled during the meeting
"Transcript too short"Empty transcript (0 characters)Check Jigasi logs; verify JVB forwarded audio to transcriber
"Meeting not found"No Meeting record AND auto-create could not resolve an organizerThe service auto-creates the Meeting from the matching InterviewBooking — but only when an InterviewBookingAttendee with role = INTERVIEWER and userId IS NOT NULL exists (this becomes the Meeting's organizerId, matching how the backend sets it when the meeting is first booked). If no such attendee is resolvable, no Meeting is created and this error is returned. Verify the booking has a mapped interviewer user.
"Authentication required"No JWT token in the Jitsi session (moderator panel only)Sign in through AIQLick before joining the meeting
"LLM API error"Bedrock service issueRetry after a few minutes
Auto-create contract (since SUP-00058, 2026-04-17)

Auto-create only runs when the Meeting row is missing for a known room. It is a defensive fallback for orphaned interview bookings; most new bookings have their Meeting created up-front by createMeetingForBooking at booking confirmation time.

The earlier implementation queried a non-existent Interview.scheduledById column and silently returned None when asyncpg raised UndefinedColumnError, reproducing the same "Meeting not found" the fallback was supposed to prevent. The fix resolves organizerId via InterviewBookingAttendee (role=INTERVIEWER, userId not null, oldest first). A secondary transcription-based fallback was removed because the transcriptions table has no user_id column, so no organizer can be resolved from transcripts alone.

GraphQL Error Format

Errors come as MeetingInsightFailure with error.code, error.message, error.timestamp, and error.recoveryHint.

Frontend Integration

import { useSubscription, gql } from '@apollo/client';

const GENERATE_INSIGHT = gql`
subscription GenerateMeetingInsight($input: MeetingInsightInput!) {
generateMeetingInsight(input: $input) {
... on MeetingInsightProcessing { id status message progress }
... on MeetingInsightSuccess {
id status
insight {
id fullReport
skillsAssessment { overallSkillMatchPercentage matchedSkills { skillName demonstratedLevel } }
communicationAnalysis { clarityScore confidenceScore professionalismScore }
redFlagsAndConcerns { overallRiskLevel }
overallRecommendation { hireRecommendation summary }
}
}
... on MeetingInsightFailure { id error { message recoveryHint } }
}
}
`;

function InsightGenerator({ meetingId }) {
const { data, loading } = useSubscription(GENERATE_INSIGHT, {
variables: { input: { meetingId } },
});

const update = data?.generateMeetingInsight;
if (!update) return loading ? <p>Connecting...</p> : null;

if (update.__typename === 'MeetingInsightProcessing') {
return <progress value={update.progress} max={100} />;
}
if (update.__typename === 'MeetingInsightSuccess') {
const { insight } = update;
return (
<div>
<p>Skill Match: {insight.skillsAssessment?.overallSkillMatchPercentage}%</p>
<p>Risk: {insight.redFlagsAndConcerns?.overallRiskLevel}</p>
<p>Recommendation: {insight.overallRecommendation?.hireRecommendation}</p>
</div>
);
}
return <p>Error: {update.error.message}</p>;
}

Best Practices

  1. Use the GraphQL subscription for generation. The REST endpoint blocks for 30-60 seconds without progress feedback.
  2. Use REST for retrieval. Once generated, fetch insights via GET. Completed insights are immutable.
  3. Cache by ID and version. Completed insights never change -- cache aggressively.
  4. Display summary first, details on demand. Show the overall match percentage, recommendation, and risk level as a summary card. Let users drill into full sections.
  5. Handle version management. Multiple insights can exist for a meeting. Always show the latest COMPLETED version.
  6. Check for existing insights first. Call GET /api/meeting-insights/{meeting_id} before triggering manual generation to avoid redundant processing.