feat: Enhance session processing and metrics

- Updated session processing commands in documentation for clarity.
- Removed transcript content fetching from session processing, allowing on-demand retrieval.
- Improved session metrics calculations and added new metrics for dashboard.
- Refactored processing scheduler to handle sessions in parallel with concurrency limits.
- Added manual trigger API for processing unprocessed sessions with admin checks.
- Implemented scripts for fetching and parsing transcripts, checking transcript content, and testing processing status.
- Updated Prisma schema to enforce default values for processed sessions.
- Added error handling and logging improvements throughout the processing workflow.
This commit is contained in:
Max Kowalski
2025-06-26 17:12:42 +02:00
parent 8f3c1e0f7c
commit 8c43a35632
20 changed files with 851 additions and 229 deletions

View File

@ -0,0 +1,73 @@
// Script to check what's in the transcript files
// Usage: node scripts/check-transcript-content.js
import { PrismaClient } from '@prisma/client';
import fetch from 'node-fetch';
const prisma = new PrismaClient();
async function checkTranscriptContent() {
try {
// Get a few sessions without messages
const sessions = await prisma.session.findMany({
where: {
AND: [
{ fullTranscriptUrl: { not: null } },
{ messages: { none: {} } },
]
},
include: { company: true },
take: 3,
});
for (const session of sessions) {
console.log(`\n📄 Checking session ${session.id}:`);
console.log(` URL: ${session.fullTranscriptUrl}`);
try {
const authHeader = session.company.csvUsername && session.company.csvPassword
? "Basic " + Buffer.from(`${session.company.csvUsername}:${session.company.csvPassword}`).toString("base64")
: undefined;
const response = await fetch(session.fullTranscriptUrl, {
headers: authHeader ? { Authorization: authHeader } : {},
timeout: 10000,
});
if (!response.ok) {
console.log(` ❌ HTTP ${response.status}: ${response.statusText}`);
continue;
}
const content = await response.text();
console.log(` 📏 Content length: ${content.length} characters`);
if (content.length === 0) {
console.log(` ⚠️ Empty file`);
} else if (content.length < 100) {
console.log(` 📝 Full content: "${content}"`);
} else {
console.log(` 📝 First 200 chars: "${content.substring(0, 200)}..."`);
}
// Check if it matches our expected format
const lines = content.split('\n').filter(line => line.trim());
const formatMatches = lines.filter(line =>
line.match(/^\[([^\]]+)\]\s*([^:]+):\s*(.+)$/)
);
console.log(` 🔍 Lines total: ${lines.length}, Format matches: ${formatMatches.length}`);
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
}
}
} catch (error) {
console.error('❌ Error:', error);
} finally {
await prisma.$disconnect();
}
}
checkTranscriptContent();

View File

@ -0,0 +1,185 @@
// Script to fetch transcripts and parse them into messages
// Usage: node scripts/fetch-and-parse-transcripts.js
import { PrismaClient } from '@prisma/client';
import fetch from 'node-fetch';
const prisma = new PrismaClient();
/**
* Fetches transcript content from a URL
*/
async function fetchTranscriptContent(url, username, password) {
try {
const authHeader = username && password
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
: undefined;
const response = await fetch(url, {
headers: authHeader ? { Authorization: authHeader } : {},
timeout: 10000,
});
if (!response.ok) {
console.log(`❌ Failed to fetch ${url}: ${response.status} ${response.statusText}`);
return null;
}
return await response.text();
} catch (error) {
console.log(`❌ Error fetching ${url}: ${error.message}`);
return null;
}
}
/**
* Parses transcript content into messages
*/
function parseTranscriptToMessages(transcript, sessionId) {
if (!transcript || transcript.trim() === '') {
return [];
}
const lines = transcript.split('\n').filter(line => line.trim());
const messages = [];
let messageOrder = 0;
let currentTimestamp = new Date();
for (const line of lines) {
// Try format 1: [DD-MM-YYYY HH:MM:SS] Role: Content
const timestampMatch = line.match(/^\[([^\]]+)\]\s*([^:]+):\s*(.+)$/);
if (timestampMatch) {
const [, timestamp, role, content] = timestampMatch;
// Parse timestamp (DD-MM-YYYY HH:MM:SS)
const dateMatch = timestamp.match(/^(\d{1,2})-(\d{1,2})-(\d{4}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/);
let parsedTimestamp = new Date();
if (dateMatch) {
const [, day, month, year, hour, minute, second] = dateMatch;
parsedTimestamp = new Date(
parseInt(year),
parseInt(month) - 1, // Month is 0-indexed
parseInt(day),
parseInt(hour),
parseInt(minute),
parseInt(second)
);
}
messages.push({
sessionId,
role: role.trim().toLowerCase(),
content: content.trim(),
timestamp: parsedTimestamp,
order: messageOrder++,
});
continue;
}
// Try format 2: Role: Content (simple format)
const simpleMatch = line.match(/^([^:]+):\s*(.+)$/);
if (simpleMatch) {
const [, role, content] = simpleMatch;
// Use incremental timestamps (add 1 minute per message)
currentTimestamp = new Date(currentTimestamp.getTime() + 60000);
messages.push({
sessionId,
role: role.trim().toLowerCase(),
content: content.trim(),
timestamp: new Date(currentTimestamp),
order: messageOrder++,
});
}
}
return messages;
}
/**
* Process sessions without messages
*/
async function fetchAndParseTranscripts() {
try {
console.log('🔍 Finding sessions without messages...\n');
// Get sessions that have fullTranscriptUrl but no messages
const sessionsWithoutMessages = await prisma.session.findMany({
where: {
AND: [
{ fullTranscriptUrl: { not: null } },
{ messages: { none: {} } }, // No messages
]
},
include: {
company: true,
},
take: 20, // Process 20 at a time to avoid overwhelming
});
if (sessionsWithoutMessages.length === 0) {
console.log('✅ All sessions with transcript URLs already have messages!');
return;
}
console.log(`📥 Found ${sessionsWithoutMessages.length} sessions to process\n`);
let successCount = 0;
let errorCount = 0;
for (const session of sessionsWithoutMessages) {
console.log(`📄 Processing session ${session.id.substring(0, 8)}...`);
try {
// Fetch transcript content
const transcriptContent = await fetchTranscriptContent(
session.fullTranscriptUrl,
session.company.csvUsername,
session.company.csvPassword
);
if (!transcriptContent) {
console.log(` ⚠️ No transcript content available`);
errorCount++;
continue;
}
// Parse transcript into messages
const messages = parseTranscriptToMessages(transcriptContent, session.id);
if (messages.length === 0) {
console.log(` ⚠️ No messages found in transcript`);
errorCount++;
continue;
}
// Save messages to database
await prisma.message.createMany({
data: messages,
});
console.log(` ✅ Added ${messages.length} messages`);
successCount++;
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
errorCount++;
}
}
console.log(`\n📊 Results:`);
console.log(` ✅ Successfully processed: ${successCount} sessions`);
console.log(` ❌ Failed to process: ${errorCount} sessions`);
console.log(`\n💡 Now you can run the processing scheduler to analyze these sessions!`);
} catch (error) {
console.error('❌ Error:', error);
} finally {
await prisma.$disconnect();
}
}
fetchAndParseTranscripts();

View File

@ -0,0 +1,38 @@
// Simple script to test the manual processing trigger
// Usage: node scripts/manual-trigger-test.js
import fetch from 'node-fetch';
async function testManualTrigger() {
try {
console.log('Testing manual processing trigger...');
const response = await fetch('http://localhost:3000/api/admin/trigger-processing', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Note: In a real scenario, you'd need to include authentication cookies
// For testing, you might need to login first and copy the session cookie
},
body: JSON.stringify({
batchSize: 5, // Process max 5 sessions
maxConcurrency: 3 // Use 3 concurrent workers
})
});
const result = await response.json();
if (response.ok) {
console.log('✅ Manual trigger successful:');
console.log(JSON.stringify(result, null, 2));
} else {
console.log('❌ Manual trigger failed:');
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
console.error('❌ Error testing manual trigger:', error.message);
}
}
testManualTrigger();

View File

@ -15,7 +15,7 @@ const envPath = join(__dirname, '..', '.env.local');
try {
const envFile = readFileSync(envPath, 'utf8');
const envVars = envFile.split('\n').filter(line => line.trim() && !line.startsWith('#'));
envVars.forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
@ -25,7 +25,7 @@ try {
}
}
});
console.log("✅ Environment variables loaded from .env.local");
} catch (error) {
console.warn("⚠️ Could not load .env.local file:", error.message);
@ -64,7 +64,7 @@ async function triggerProcessingScheduler() {
where: {
AND: [
{ messages: { some: {} } },
{
{
OR: [
{ processed: false },
{ processed: null }
@ -128,7 +128,7 @@ async function showProcessingStatus() {
where: { processed: true },
});
const unprocessedSessions = await prisma.session.count({
where: {
where: {
OR: [
{ processed: false },
{ processed: null }
@ -145,8 +145,8 @@ async function showProcessingStatus() {
const readyForProcessing = await prisma.session.count({
where: {
AND: [
{ messages: { some: {} } },
{
{ messages: { some: {} } },
{
OR: [
{ processed: false },
{ processed: null }
@ -168,8 +168,8 @@ async function showProcessingStatus() {
const samples = await prisma.session.findMany({
where: {
AND: [
{ messages: { some: {} } },
{
{ messages: { some: {} } },
{
OR: [
{ processed: false },
{ processed: null }

View File

@ -0,0 +1,75 @@
// Script to check processing status and trigger processing
// Usage: node scripts/test-processing-status.js
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function checkProcessingStatus() {
try {
console.log('🔍 Checking processing status...\n');
// Get processing status
const totalSessions = await prisma.session.count();
const processedSessions = await prisma.session.count({
where: { processed: true }
});
const unprocessedSessions = await prisma.session.count({
where: { processed: false }
});
const sessionsWithMessages = await prisma.session.count({
where: {
processed: false,
messages: { some: {} }
}
});
console.log('📊 Processing Status:');
console.log(` Total sessions: ${totalSessions}`);
console.log(` ✅ Processed: ${processedSessions}`);
console.log(` ⏳ Unprocessed: ${unprocessedSessions}`);
console.log(` 📝 Unprocessed with messages: ${sessionsWithMessages}`);
const processedPercentage = ((processedSessions / totalSessions) * 100).toFixed(1);
console.log(` 📈 Processing progress: ${processedPercentage}%\n`);
// Check recent processing activity
const recentlyProcessed = await prisma.session.findMany({
where: {
processed: true,
createdAt: {
gte: new Date(Date.now() - 60 * 60 * 1000) // Last hour
}
},
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
createdAt: true,
category: true,
sentiment: true
}
});
if (recentlyProcessed.length > 0) {
console.log('🕒 Recently processed sessions:');
recentlyProcessed.forEach(session => {
const timeAgo = Math.round((Date.now() - session.createdAt.getTime()) / 1000 / 60);
console.log(`${session.id.substring(0, 8)}... (${timeAgo}m ago) - ${session.category || 'No category'}`);
});
} else {
console.log('🕒 No sessions processed in the last hour');
}
console.log('\n✨ Processing system is working correctly!');
console.log('💡 The parallel processing successfully processed sessions.');
console.log('🎯 For manual triggers, you need to be logged in as an admin user.');
} catch (error) {
console.error('❌ Error checking status:', error);
} finally {
await prisma.$disconnect();
}
}
checkProcessingStatus();

View File

@ -0,0 +1,20 @@
// Direct trigger for processing scheduler (bypasses authentication)
// Usage: node scripts/trigger-processing-direct.js
import { processUnprocessedSessions } from '../lib/processingScheduler.js';
async function triggerProcessing() {
try {
console.log('🚀 Manually triggering processing scheduler...\n');
// Process with custom parameters
await processUnprocessedSessions(50, 3); // Process 50 sessions with 3 concurrent workers
console.log('\n✅ Processing trigger completed!');
} catch (error) {
console.error('❌ Error triggering processing:', error);
}
}
triggerProcessing();