Optimize all AI functions for efficiency and speed

- AI Tagging: batch 10 projects per API call with 3 concurrent batches (~10x faster)
  - New `tagProjectsBatch()` with `getAISuggestionsBatch()` for multi-project prompts
  - Single DB query for all projects, single anonymization pass
  - Compact JSON in prompts (no pretty-print) saves tokens
- AI Shortlist: run STARTUP and BUSINESS_CONCEPT categories in parallel (2x faster)
- AI Filtering: increase default parallel batches from 1 to 3 (3x faster)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-16 14:02:38 +01:00
parent 989db4dc14
commit 65a22e6f19
4 changed files with 377 additions and 49 deletions

View File

@@ -5,6 +5,7 @@ import { prisma } from '@/lib/prisma'
import { logAudit } from '../utils/audit'
import {
tagProject,
tagProjectsBatch,
getTagSuggestions,
addProjectTag,
removeProjectTag,
@@ -17,7 +18,7 @@ import {
NotificationTypes,
} from '../services/in-app-notification'
// Background job runner for tagging
// Background job runner for tagging — uses batched API calls for efficiency
async function runTaggingJob(jobId: string, userId: string) {
const job = await prisma.taggingJob.findUnique({
where: { id: jobId },
@@ -28,7 +29,7 @@ async function runTaggingJob(jobId: string, userId: string) {
return
}
console.log(`[AI Tagging Job] Starting job ${jobId}...`)
console.log(`[AI Tagging Job] Starting job ${jobId} (batched mode)...`)
// Mark as running
await prisma.taggingJob.update({
@@ -56,7 +57,7 @@ async function runTaggingJob(jobId: string, userId: string) {
const allProjects = await prisma.project.findMany({
where: whereClause,
select: { id: true, title: true, tags: true },
select: { id: true, title: true, tags: true, projectTags: { select: { tagId: true } } },
})
const untaggedProjects = allProjects.filter(p => p.tags.length === 0)
@@ -83,48 +84,33 @@ async function runTaggingJob(jobId: string, userId: string) {
return
}
let taggedCount = 0
let failedCount = 0
const errors: string[] = []
const startTime = Date.now()
for (let i = 0; i < untaggedProjects.length; i++) {
const project = untaggedProjects[i]
console.log(`[AI Tagging Job] Processing ${i + 1}/${untaggedProjects.length}: "${project.title.substring(0, 40)}..."`)
// Use batched tagging — processes 10 projects per API call, 3 concurrent calls
const { results, totalTokens } = await tagProjectsBatch(
untaggedProjects,
userId,
async (processed, total) => {
// Update job progress on each batch completion
const taggedSoFar = results?.length ?? processed
await prisma.taggingJob.update({
where: { id: jobId },
data: {
processedCount: processed,
taggedCount: taggedSoFar,
},
})
try {
const result = await tagProject(project.id, userId)
taggedCount++
console.log(`[AI Tagging Job] ✓ Tagged with ${result.applied.length} tags`)
} catch (error) {
failedCount++
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
errors.push(`${project.title}: ${errorMsg}`)
console.error(`[AI Tagging Job] ✗ Failed: ${errorMsg}`)
}
// Update progress
await prisma.taggingJob.update({
where: { id: jobId },
data: {
processedCount: i + 1,
taggedCount,
failedCount,
errorsJson: errors.length > 0 ? errors.slice(0, 20) : undefined, // Keep last 20 errors
},
})
// Log progress every 10 projects
if ((i + 1) % 10 === 0) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0)
const avgTime = (Date.now() - startTime) / (i + 1) / 1000
const remaining = avgTime * (untaggedProjects.length - i - 1)
console.log(`[AI Tagging Job] Progress: ${i + 1}/${untaggedProjects.length} (${elapsed}s elapsed, ~${remaining.toFixed(0)}s remaining)`)
console.log(`[AI Tagging Job] Progress: ${processed}/${total} (${elapsed}s elapsed)`)
}
}
)
const taggedCount = results.filter(r => r.applied.length > 0).length
const failedCount = untaggedProjects.length - results.length
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1)
console.log(`[AI Tagging Job] Complete: ${taggedCount} tagged, ${failedCount} failed in ${totalTime}s`)
console.log(`[AI Tagging Job] Complete: ${taggedCount} tagged, ${failedCount} failed in ${totalTime}s (${totalTokens} tokens)`)
// Mark as completed
await prisma.taggingJob.update({
@@ -132,7 +118,9 @@ async function runTaggingJob(jobId: string, userId: string) {
data: {
status: 'COMPLETED',
completedAt: new Date(),
errorsJson: errors.length > 0 ? errors : undefined,
processedCount: results.length,
taggedCount,
failedCount,
},
})
@@ -144,7 +132,7 @@ async function runTaggingJob(jobId: string, userId: string) {
linkUrl: '/admin/projects',
linkLabel: 'View Projects',
priority: 'normal',
metadata: { jobId, taggedCount, failedCount, skippedCount },
metadata: { jobId, taggedCount, failedCount, skippedCount, totalTokens },
})
} catch (error) {