mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:52:08 +01:00
feat: update session metrics and processing to use enums for sentiment and streamline status tracking
This commit is contained in:
@ -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')
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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++;
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user