feat: update session metrics and processing to use enums for sentiment and streamline status tracking

This commit is contained in:
Max Kowalski
2025-06-27 23:23:09 +02:00
parent 8ffd5a7a2c
commit 9238c9a6af
9 changed files with 38 additions and 79 deletions

View File

@ -453,41 +453,25 @@ export function sessionMetrics(
if (session.escalated) escalatedCount++; if (session.escalated) escalatedCount++;
if (session.forwardedHr) forwardedHrCount++; if (session.forwardedHr) forwardedHrCount++;
// Sentiment // Sentiment (now using enum values)
if (session.sentiment !== undefined && session.sentiment !== null) { if (session.sentiment !== undefined && session.sentiment !== null) {
// Example thresholds, adjust as needed if (session.sentiment === "POSITIVE") sentimentPositiveCount++;
if (session.sentiment > 0.3) sentimentPositiveCount++; else if (session.sentiment === "NEGATIVE") sentimentNegativeCount++;
else if (session.sentiment < -0.3) sentimentNegativeCount++; else if (session.sentiment === "NEUTRAL") sentimentNeutralCount++;
else sentimentNeutralCount++;
} }
// Sentiment Alert Check // Sentiment Alert Check (simplified for enum)
if ( if (
companyConfig.sentimentAlert !== undefined && companyConfig.sentimentAlert !== undefined &&
session.sentiment !== undefined && session.sentiment === "NEGATIVE"
session.sentiment !== null &&
session.sentiment < companyConfig.sentimentAlert
) { ) {
alerts++; alerts++;
} }
// Tokens
if (session.tokens !== undefined && session.tokens !== null) {
totalTokens += session.tokens;
}
if (session.tokensEur !== undefined && session.tokensEur !== null) {
totalTokensEur += session.tokensEur;
}
// Daily metrics // Daily metrics
const day = new Date(session.startTime).toISOString().split("T")[0]; const day = new Date(session.startTime).toISOString().split("T")[0];
byDay[day] = (byDay[day] || 0) + 1; // Sessions per day byDay[day] = (byDay[day] || 0) + 1; // Sessions per day
if (session.tokens !== undefined && session.tokens !== null) { // Note: tokens and tokensEur are not available in the new schema
tokensByDay[day] = (tokensByDay[day] || 0) + session.tokens;
}
if (session.tokensEur !== undefined && session.tokensEur !== null) {
tokensCostByDay[day] = (tokensCostByDay[day] || 0) + session.tokensEur;
}
// Category metrics // Category metrics
if (session.category) { if (session.category) {
@ -506,24 +490,7 @@ export function sessionMetrics(
// Extract questions from session // Extract questions from session
const extractQuestions = () => { const extractQuestions = () => {
// 1. Extract from questions JSON field // 1. Extract questions from user messages (if available)
if (session.questions) {
try {
const questionsArray = JSON.parse(session.questions);
if (Array.isArray(questionsArray)) {
questionsArray.forEach((question: string) => {
if (question && question.trim().length > 0) {
const cleanQuestion = question.trim();
questionCounts[cleanQuestion] = (questionCounts[cleanQuestion] || 0) + 1;
}
});
}
} catch (error) {
console.warn(`[metrics] Failed to parse questions JSON for session ${session.id}: ${error}`);
}
}
// 2. Extract questions from user messages (if available)
if (session.messages) { if (session.messages) {
session.messages session.messages
.filter(msg => msg.role === 'User') .filter(msg => msg.role === 'User')

View File

@ -52,7 +52,7 @@ export function startCsvImportScheduler() {
tokensEur: rawSession.tokensEur, tokensEur: rawSession.tokensEur,
category: rawSession.category, category: rawSession.category,
initialMessage: rawSession.initialMessage, initialMessage: rawSession.initialMessage,
status: "QUEUED", // Reset status for reprocessing if needed // Status tracking now handled by ProcessingStatusManager
}, },
create: { create: {
companyId: company.id, companyId: company.id,
@ -72,7 +72,7 @@ export function startCsvImportScheduler() {
tokensEur: rawSession.tokensEur, tokensEur: rawSession.tokensEur,
category: rawSession.category, category: rawSession.category,
initialMessage: rawSession.initialMessage, initialMessage: rawSession.initialMessage,
status: "QUEUED", // Status tracking now handled by ProcessingStatusManager
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -1,15 +1,15 @@
// Combined scheduler initialization // Combined scheduler initialization
import { startScheduler } from "./scheduler"; import { startCsvImportScheduler } from "./scheduler";
import { startProcessingScheduler } from "./processingScheduler"; import { startProcessingScheduler } from "./processingScheduler";
/** /**
* Initialize all schedulers * Initialize all schedulers
* - Session refresh scheduler (runs every 15 minutes) * - CSV import scheduler (runs every 15 minutes)
* - Session processing scheduler (runs every hour) * - Session processing scheduler (runs every hour)
*/ */
export function initializeSchedulers() { export function initializeSchedulers() {
// Start the session refresh scheduler // Start the CSV import scheduler
startScheduler(); startCsvImportScheduler();
// Start the session processing scheduler // Start the session processing scheduler
startProcessingScheduler(); startProcessingScheduler();

View File

@ -54,8 +54,7 @@ export interface ChatSession {
language?: string | null; language?: string | null;
country?: string | null; country?: string | null;
ipAddress?: string | null; ipAddress?: string | null;
sentiment?: number | null; sentiment?: string | null; // Now a SentimentCategory enum: "POSITIVE", "NEUTRAL", "NEGATIVE"
sentimentCategory?: string | null; // "positive", "neutral", "negative" from OpenAPI
messagesSent?: number; messagesSent?: number;
startTime: Date; startTime: Date;
endTime?: Date | null; endTime?: Date | null;
@ -66,14 +65,11 @@ export interface ChatSession {
avgResponseTime?: number | null; avgResponseTime?: number | null;
escalated?: boolean; escalated?: boolean;
forwardedHr?: boolean; forwardedHr?: boolean;
tokens?: number;
tokensEur?: number;
initialMsg?: string; initialMsg?: string;
fullTranscriptUrl?: string | null; fullTranscriptUrl?: string | null;
processed?: boolean | null; // Flag for post-processing status
questions?: string | null; // JSON array of questions asked by user
summary?: string | null; // Brief summary of the conversation summary?: string | null; // Brief summary of the conversation
messages?: Message[]; // Parsed messages from transcript messages?: Message[]; // Parsed messages from transcript
transcriptContent?: string | null; // Full transcript content
} }
export interface SessionQuery { export interface SessionQuery {

View File

@ -82,7 +82,7 @@ export default async function handler(
tokensEur: rawSession.tokensEur, tokensEur: rawSession.tokensEur,
category: rawSession.category, category: rawSession.category,
initialMessage: rawSession.initialMessage, initialMessage: rawSession.initialMessage,
status: "QUEUED", // Reset status for reprocessing if needed // Status tracking now handled by ProcessingStatusManager
}, },
create: { create: {
companyId: company.id, companyId: company.id,
@ -102,7 +102,7 @@ export default async function handler(
tokensEur: rawSession.tokensEur, tokensEur: rawSession.tokensEur,
category: rawSession.category, category: rawSession.category,
initialMessage: rawSession.initialMessage, initialMessage: rawSession.initialMessage,
status: "QUEUED", // Status tracking now handled by ProcessingStatusManager
}, },
}); });
importedCount++; importedCount++;

View File

@ -3,6 +3,8 @@ import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]"; import { authOptions } from "../auth/[...nextauth]";
import { prisma } from "../../../lib/prisma"; import { prisma } from "../../../lib/prisma";
import { processUnprocessedSessions } from "../../../lib/processingScheduler"; import { processUnprocessedSessions } from "../../../lib/processingScheduler";
import { ProcessingStatusManager } from "../../../lib/processingStatusManager";
import { ProcessingStage } from "@prisma/client";
interface SessionUser { interface SessionUser {
email: string; email: string;
@ -53,19 +55,23 @@ export default async function handler(
const validatedBatchSize = batchSize && batchSize > 0 ? parseInt(batchSize) : null; const validatedBatchSize = batchSize && batchSize > 0 ? parseInt(batchSize) : null;
const validatedMaxConcurrency = maxConcurrency && maxConcurrency > 0 ? parseInt(maxConcurrency) : 5; const validatedMaxConcurrency = maxConcurrency && maxConcurrency > 0 ? parseInt(maxConcurrency) : 5;
// Check how many unprocessed sessions exist // Check how many sessions need AI processing using the new status system
const unprocessedCount = await prisma.session.count({ const sessionsNeedingAI = await ProcessingStatusManager.getSessionsNeedingProcessing(
where: { ProcessingStage.AI_ANALYSIS,
companyId: user.companyId, 1000 // Get count only
processed: false, );
messages: { some: {} }, // Must have messages
}, // Filter to sessions for this company
}); const companySessionsNeedingAI = sessionsNeedingAI.filter(
statusRecord => statusRecord.session.companyId === user.companyId
);
const unprocessedCount = companySessionsNeedingAI.length;
if (unprocessedCount === 0) { if (unprocessedCount === 0) {
return res.json({ return res.json({
success: true, success: true,
message: "No unprocessed sessions found", message: "No sessions requiring AI processing found",
unprocessedCount: 0, unprocessedCount: 0,
processedCount: 0, processedCount: 0,
}); });

View File

@ -36,10 +36,9 @@ export default async function handler(
// Get date range from query parameters // Get date range from query parameters
const { startDate, endDate } = req.query; const { startDate, endDate } = req.query;
// Build where clause with optional date filtering and only processed sessions // Build where clause with optional date filtering
const whereClause: any = { const whereClause: any = {
companyId: user.companyId, companyId: user.companyId,
processed: true, // Only show processed sessions in dashboard
}; };
if (startDate && endDate) { if (startDate && endDate) {
@ -74,13 +73,10 @@ export default async function handler(
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent, // Handle null messagesSent messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent, // Handle null messagesSent
avgResponseTime: avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime, ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
tokens: ps.tokens === null ? undefined : ps.tokens,
tokensEur: ps.tokensEur === null ? undefined : ps.tokensEur,
escalated: ps.escalated || false, escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false, forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined, initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined, fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
questions: ps.questions || undefined, // Include questions field
summary: ps.summary || undefined, // Include summary field summary: ps.summary || undefined, // Include summary field
messages: ps.messages || [], // Include messages for question extraction messages: ps.messages || [], // Include messages for question extraction
// userId is missing in Prisma Session model, assuming it's not strictly needed for metrics or can be null // userId is missing in Prisma Session model, assuming it's not strictly needed for metrics or can be null

View File

@ -51,18 +51,14 @@ export default async function handler(
country: prismaSession.country ?? null, country: prismaSession.country ?? null,
ipAddress: prismaSession.ipAddress ?? null, ipAddress: prismaSession.ipAddress ?? null,
sentiment: prismaSession.sentiment ?? null, sentiment: prismaSession.sentiment ?? null,
sentimentCategory: prismaSession.sentimentCategory ?? null, // New field
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
avgResponseTime: prismaSession.avgResponseTime ?? null, avgResponseTime: prismaSession.avgResponseTime ?? null,
escalated: prismaSession.escalated ?? undefined, escalated: prismaSession.escalated ?? undefined,
forwardedHr: prismaSession.forwardedHr ?? undefined, forwardedHr: prismaSession.forwardedHr ?? undefined,
tokens: prismaSession.tokens ?? undefined,
tokensEur: prismaSession.tokensEur ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined, initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null, fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
processed: prismaSession.processed ?? null, // New field
questions: prismaSession.questions ?? null, // New field
summary: prismaSession.summary ?? null, // New field summary: prismaSession.summary ?? null, // New field
transcriptContent: null, // Not available in Session model
messages: messages:
prismaSession.messages?.map((msg) => ({ prismaSession.messages?.map((msg) => ({
id: msg.id, id: msg.id,

View File

@ -50,16 +50,16 @@ export default async function handler(
) { ) {
const searchConditions = [ const searchConditions = [
{ id: { contains: searchTerm } }, { id: { contains: searchTerm } },
{ category: { contains: searchTerm } },
{ initialMsg: { contains: searchTerm } }, { initialMsg: { contains: searchTerm } },
{ transcriptContent: { contains: searchTerm } }, { summary: { contains: searchTerm } },
]; ];
whereClause.OR = searchConditions; whereClause.OR = searchConditions;
} }
// Category Filter // Category Filter
if (category && typeof category === "string" && category.trim() !== "") { if (category && typeof category === "string" && category.trim() !== "") {
whereClause.category = category; // Cast to SessionCategory enum if it's a valid value
whereClause.category = category as any;
} }
// Language Filter // Language Filter
@ -146,8 +146,6 @@ export default async function handler(
avgResponseTime: ps.avgResponseTime ?? null, avgResponseTime: ps.avgResponseTime ?? null,
escalated: ps.escalated ?? undefined, escalated: ps.escalated ?? undefined,
forwardedHr: ps.forwardedHr ?? undefined, forwardedHr: ps.forwardedHr ?? undefined,
tokens: ps.tokens ?? undefined,
tokensEur: ps.tokensEur ?? undefined,
initialMsg: ps.initialMsg ?? undefined, initialMsg: ps.initialMsg ?? undefined,
fullTranscriptUrl: ps.fullTranscriptUrl ?? null, fullTranscriptUrl: ps.fullTranscriptUrl ?? null,
transcriptContent: null, // Transcript content is now fetched from fullTranscriptUrl when needed transcriptContent: null, // Transcript content is now fetched from fullTranscriptUrl when needed