Files
MOPC-Portal/src/server/services/ai-errors.ts
Matt 928b1c65dc Optimize AI system with batching, token tracking, and GDPR compliance
- Add AIUsageLog model for persistent token/cost tracking
- Implement batched processing for all AI services:
  - Assignment: 15 projects/batch
  - Filtering: 20 projects/batch
  - Award eligibility: 20 projects/batch
  - Mentor matching: 15 projects/batch
- Create unified error classification (ai-errors.ts)
- Enhance anonymization with comprehensive project data
- Add AI usage dashboard to Settings page
- Add usage stats endpoints to settings router
- Create AI system documentation (5 files)
- Create GDPR compliance documentation (2 files)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:58:12 +01:00

319 lines
9.4 KiB
TypeScript

/**
* AI Error Classification Service
*
* Provides unified error handling and classification for all AI services.
* Converts technical API errors into user-friendly messages.
*/
// ─── Error Types ─────────────────────────────────────────────────────────────
export type AIErrorType =
| 'rate_limit'
| 'quota_exceeded'
| 'model_not_found'
| 'invalid_api_key'
| 'context_length'
| 'parse_error'
| 'timeout'
| 'network_error'
| 'content_filter'
| 'server_error'
| 'unknown'
export interface ClassifiedError {
type: AIErrorType
message: string
originalMessage: string
retryable: boolean
suggestedAction?: string
}
// ─── Error Patterns ──────────────────────────────────────────────────────────
interface ErrorPattern {
type: AIErrorType
patterns: Array<string | RegExp>
retryable: boolean
userMessage: string
suggestedAction?: string
}
const ERROR_PATTERNS: ErrorPattern[] = [
{
type: 'rate_limit',
patterns: [
'rate_limit',
'rate limit',
'too many requests',
'429',
'quota exceeded',
'Rate limit reached',
],
retryable: true,
userMessage: 'Rate limit exceeded. Please wait a few minutes and try again.',
suggestedAction: 'Wait 1-2 minutes before retrying, or reduce batch size.',
},
{
type: 'quota_exceeded',
patterns: [
'insufficient_quota',
'billing',
'exceeded your current quota',
'payment required',
'account deactivated',
],
retryable: false,
userMessage: 'API quota exceeded. Please check your OpenAI billing settings.',
suggestedAction: 'Add payment method or increase spending limit in OpenAI dashboard.',
},
{
type: 'model_not_found',
patterns: [
'model_not_found',
'does not exist',
'The model',
'invalid model',
'model not available',
],
retryable: false,
userMessage: 'The selected AI model is not available. Please check your settings.',
suggestedAction: 'Go to Settings → AI and select a different model.',
},
{
type: 'invalid_api_key',
patterns: [
'invalid_api_key',
'Incorrect API key',
'authentication',
'unauthorized',
'401',
'invalid api key',
],
retryable: false,
userMessage: 'Invalid API key. Please check your OpenAI API key in settings.',
suggestedAction: 'Go to Settings → AI and enter a valid API key.',
},
{
type: 'context_length',
patterns: [
'context_length',
'maximum context length',
'tokens',
'too long',
'reduce the length',
'max_tokens',
],
retryable: true,
userMessage: 'Request too large. Try processing fewer items at once.',
suggestedAction: 'Process items in smaller batches.',
},
{
type: 'content_filter',
patterns: [
'content_filter',
'content policy',
'flagged',
'inappropriate',
'safety system',
],
retryable: false,
userMessage: 'Content was flagged by the AI safety system. Please review the input data.',
suggestedAction: 'Check project descriptions for potentially sensitive content.',
},
{
type: 'timeout',
patterns: [
'timeout',
'timed out',
'ETIMEDOUT',
'ECONNABORTED',
'deadline exceeded',
],
retryable: true,
userMessage: 'Request timed out. Please try again.',
suggestedAction: 'Try again or process fewer items at once.',
},
{
type: 'network_error',
patterns: [
'ENOTFOUND',
'ECONNREFUSED',
'network',
'connection',
'DNS',
'getaddrinfo',
],
retryable: true,
userMessage: 'Network error. Please check your connection and try again.',
suggestedAction: 'Check network connectivity and firewall settings.',
},
{
type: 'server_error',
patterns: [
'500',
'502',
'503',
'504',
'internal error',
'server error',
'service unavailable',
],
retryable: true,
userMessage: 'OpenAI service temporarily unavailable. Please try again later.',
suggestedAction: 'Wait a few minutes and retry. Check status.openai.com for outages.',
},
]
// ─── Error Classification ────────────────────────────────────────────────────
/**
* Classify an error from the OpenAI API
*/
export function classifyAIError(error: Error | unknown): ClassifiedError {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorString = errorMessage.toLowerCase()
// Check against known patterns
for (const pattern of ERROR_PATTERNS) {
for (const matcher of pattern.patterns) {
const matches =
typeof matcher === 'string'
? errorString.includes(matcher.toLowerCase())
: matcher.test(errorString)
if (matches) {
return {
type: pattern.type,
message: pattern.userMessage,
originalMessage: errorMessage,
retryable: pattern.retryable,
suggestedAction: pattern.suggestedAction,
}
}
}
}
// Unknown error
return {
type: 'unknown',
message: 'An unexpected error occurred. Please try again.',
originalMessage: errorMessage,
retryable: true,
suggestedAction: 'If the problem persists, check the AI settings or contact support.',
}
}
/**
* Check if an error is a JSON parse error
*/
export function isParseError(error: Error | unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return (
message.includes('JSON') ||
message.includes('parse') ||
message.includes('Unexpected token') ||
message.includes('SyntaxError')
)
}
/**
* Create a classified parse error
*/
export function createParseError(originalMessage: string): ClassifiedError {
return {
type: 'parse_error',
message: 'AI returned an invalid response. Items flagged for manual review.',
originalMessage,
retryable: true,
suggestedAction: 'Review flagged items manually. Consider using a different model.',
}
}
// ─── User-Friendly Messages ──────────────────────────────────────────────────
const USER_FRIENDLY_MESSAGES: Record<AIErrorType, string> = {
rate_limit: 'Rate limit exceeded. Please wait a few minutes and try again.',
quota_exceeded: 'API quota exceeded. Please check your OpenAI billing settings.',
model_not_found: 'Selected AI model is not available. Please check your settings.',
invalid_api_key: 'Invalid API key. Please verify your OpenAI API key.',
context_length: 'Request too large. Please try with fewer items.',
parse_error: 'AI response could not be processed. Items flagged for review.',
timeout: 'Request timed out. Please try again.',
network_error: 'Network connection error. Please check your connection.',
content_filter: 'Content flagged by AI safety system. Please review input data.',
server_error: 'AI service temporarily unavailable. Please try again later.',
unknown: 'An unexpected error occurred. Please try again.',
}
/**
* Get a user-friendly message for an error type
*/
export function getUserFriendlyMessage(errorType: AIErrorType): string {
return USER_FRIENDLY_MESSAGES[errorType]
}
// ─── Error Handling Helpers ──────────────────────────────────────────────────
/**
* Wrap an async function with standardized AI error handling
*/
export async function withAIErrorHandling<T>(
fn: () => Promise<T>,
fallback: T
): Promise<{ result: T; error?: ClassifiedError }> {
try {
const result = await fn()
return { result }
} catch (error) {
const classified = classifyAIError(error)
console.error(`[AI Error] ${classified.type}:`, classified.originalMessage)
return { result: fallback, error: classified }
}
}
/**
* Log an AI error with context
*/
export function logAIError(
service: string,
operation: string,
error: ClassifiedError,
context?: Record<string, unknown>
): void {
console.error(
`[AI ${service}] ${operation} failed:`,
JSON.stringify({
type: error.type,
message: error.message,
originalMessage: error.originalMessage,
retryable: error.retryable,
...context,
})
)
}
// ─── Retry Logic ─────────────────────────────────────────────────────────────
/**
* Determine if an operation should be retried based on error type
*/
export function shouldRetry(error: ClassifiedError, attempt: number, maxAttempts: number = 3): boolean {
if (!error.retryable) return false
if (attempt >= maxAttempts) return false
// Rate limits need longer delays
if (error.type === 'rate_limit') {
return attempt < 2 // Only retry once for rate limits
}
return true
}
/**
* Calculate delay before retry (exponential backoff)
*/
export function getRetryDelay(error: ClassifiedError, attempt: number): number {
const baseDelay = error.type === 'rate_limit' ? 30000 : 1000 // 30s for rate limit, 1s otherwise
return baseDelay * Math.pow(2, attempt)
}