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:
-
Automatic (recommended) -- When a Jitsi meeting ends, the Prosody Event Sync plugin sends a
muc-room-destroyedwebhook toPOST /api/events/room/destroyedon background-tasks. IfAUTO_GENERATE_MEETING_INSIGHTS=true(default), the service automatically generates an insight when the transcript has more than 100 characters. -
On-demand -- Users can trigger generation via the
generateMeetingInsightGraphQL subscription or thePOST /api/meeting-insights/generateREST endpoint.
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
}
}
| Field | Type | Required | Description |
|---|---|---|---|
meetingId | String | Yes | Jitsi meeting room name (e.g., aiqlick-general-123456789) |
interviewId | String | No | Interview UUID for additional job/candidate context in the analysis |
Meeting IDs are Jitsi room names, not UUIDs. The system resolves them internally to the database Meeting record.
Progress Updates
| Progress | Message |
|---|---|
| 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.
| Field | Type | Description |
|---|---|---|
overallSkillMatchPercentage | Int (0-100) | Overall match score |
skillSummary | String | Narrative 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.
| Field | Type | Description |
|---|---|---|
clarityScore | Int (0-10) | How clearly the candidate communicated |
confidenceScore | Int (0-10) | Confidence level displayed |
professionalismScore | Int (0-10) | Professional demeanor |
communicationStyle | String | Overall 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.
| Field | Type | Description |
|---|---|---|
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.
| Field | Type | Description |
|---|---|---|
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 |
overallRiskLevel | Enum | LOW, MEDIUM, HIGH |
OverallRecommendation
The final hiring recommendation.
| Field | Type | Description |
|---|---|---|
hireRecommendation | Enum | STRONG_YES, YES, MAYBE, NO, STRONG_NO |
confidenceLevel | Enum | LOW, MEDIUM, HIGH |
summary | String | Overall 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).
| Status | Description |
|---|---|
GENERATING | Currently being generated |
COMPLETED | Successfully generated (immutable) |
FAILED | Generation failed |
STALE | Superseded 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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/meeting-insights/generate | Generate new insight (blocks 30-60s) |
| GET | /api/meeting-insights/{meeting_id} | Get latest completed insight |
| GET | /api/meeting-insights/{meeting_id}/history?limit=10 | Get 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/check | Service 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"}'
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
| Setting | Value |
|---|---|
| Model | Claude Haiku 4.5 (eu.anthropic.claude-haiku-4-5-20251001-v1:0) |
| Max tokens | 8192 (2x default) |
| Temperature | 0.3 (low for consistent analysis) |
| Provider | AWS Bedrock (eu-north-1) |
| Minimum transcript | 1 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
| Error | Cause | Solution |
|---|---|---|
| "No transcript found" | Meeting has no transcription data | Ensure 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 organizer | The 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 issue | Retry after a few minutes |
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
- Use the GraphQL subscription for generation. The REST endpoint blocks for 30-60 seconds without progress feedback.
- Use REST for retrieval. Once generated, fetch insights via GET. Completed insights are immutable.
- Cache by ID and version. Completed insights never change -- cache aggressively.
- 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.
- Handle version management. Multiple insights can exist for a meeting. Always show the latest
COMPLETEDversion. - Check for existing insights first. Call
GET /api/meeting-insights/{meeting_id}before triggering manual generation to avoid redundant processing.