From 2327ed053c49244cf15d4bc052c0d68359ec630e Mon Sep 17 00:00:00 2001 From: Godwin-Ekoh Date: Tue, 29 Jul 2025 13:24:09 +0100 Subject: [PATCH 1/2] feat: complete centralized error handling implementation across all modules --- ai-invoice-extractor/src/cli.ts | 21 +- .../src/extractors/extractor.ts | 65 +++-- .../packages/core/src/analyzeDocument.ts | 235 ++++++++++-------- ai-receipt-generator/src/core/api/router.py | 34 ++- .../src/core/errors/__init__.py | 118 +++++++++ .../src/core/services/receipt_service.py | 48 ++-- docs/ERROR_HANDLING.md | 84 +++++++ shared/errors/base.ts | 117 +++++++++ shared/errors/types.ts | 64 +++++ 9 files changed, 625 insertions(+), 161 deletions(-) create mode 100644 ai-receipt-generator/src/core/errors/__init__.py create mode 100644 docs/ERROR_HANDLING.md create mode 100644 shared/errors/base.ts create mode 100644 shared/errors/types.ts diff --git a/ai-invoice-extractor/src/cli.ts b/ai-invoice-extractor/src/cli.ts index ffe0690..85148c8 100644 --- a/ai-invoice-extractor/src/cli.ts +++ b/ai-invoice-extractor/src/cli.ts @@ -11,6 +11,8 @@ import { invoiceOutputSchema } from "./prompts/extract-invoice.prompt.js"; import type { AiConfig } from "./types.js"; import { ConfigUtils } from "./utils/config.js"; import { StringUtils } from "./utils/string.js"; +import { BaseError } from "../../shared/errors/base.js"; +import { ErrorCode } from "../../shared/errors/types.js"; export type CliOptions = z.infer; export const CliOptions = z.object({ @@ -59,7 +61,7 @@ export async function main(argv: string[]) { const messages = parsedCliOptions.error.issues.map((issue) => issue.path.length ? `${issue.path.join(".")}: ${issue.message}` - : issue.message, + : issue.message ); stderr.push(messages.join("\n")); exitCode = 1; @@ -99,6 +101,8 @@ export async function main(argv: string[]) { // --------------------------- const extractor = Extractor.create(mergedAiConfig); + const requestId = Math.random().toString(36).substring(7); + try { const data = await extractor.analyseFile({ path: filePath, @@ -112,8 +116,19 @@ export async function main(argv: string[]) { stdout.push(`\n${JSON.stringify(data)}\n\n`); } } catch (error) { - stderr.push(`${(error as Error).message}\n`); - exitCode = 1; + if (error instanceof BaseError) { + stderr.push(`Error ${error.code}: ${error.userMessage}`); + if (error.recovery.type === "retry") { + stderr.push(`Recovery: ${error.recovery.description}`); + } + if (error.technicalDetails && process.env.DEBUG) { + stderr.push(`Technical details: ${error.technicalDetails}`); + } + exitCode = Math.floor(error.statusCode / 100) === 4 ? 1 : 2; + } else { + stderr.push(`Unexpected error: ${(error as Error).message}`); + exitCode = 2; + } } }); diff --git a/ai-invoice-extractor/src/extractors/extractor.ts b/ai-invoice-extractor/src/extractors/extractor.ts index f20a90d..74da6ef 100644 --- a/ai-invoice-extractor/src/extractors/extractor.ts +++ b/ai-invoice-extractor/src/extractors/extractor.ts @@ -10,6 +10,8 @@ import { createAnthropic } from "@ai-sdk/anthropic" import { createOllama } from 'ollama-ai-provider'; import { APICallError, type LanguageModelV1, NoObjectGeneratedError, generateObject } from "ai" import type { z } from "zod" +import { BaseError, AIServiceError, ValidationError, ProcessingError } from '../../../shared/errors/base.js'; +import { ErrorCode, ErrorContext } from '../../../shared/errors/types.js'; // ============================== // Types @@ -40,19 +42,30 @@ export abstract class BaseExtractor implements IExtractor { // ============================== async analyseFile(input: AnalyseFilePathInput): Promise { - const { path, prompt = "EXTRACT_INVOICE", output } = input + const { path, prompt = "EXTRACT_INVOICE", output } = input; + + const context: ErrorContext = { + operation: 'analyseFile', + module: 'extractor', + timestamp: new Date().toISOString(), + metadata: { path, prompt, model: `${this.model.provider}:${this.model.modelId}` } + }; - // Get file data - // --------------------------- - const file = FileUtils.getMetadata(path) - const buffer = await FileUtils.readFile(path) - const fileUrl = new URL(`data:${file.mimeType};base64,${buffer.toString("base64")}`) - - if (file.fileType !== "image" && file.fileType !== "text" && file.fileType !== "file") { - throw new Error("Only image, text and PDF files are supported.") - } + try { + // Get file data + const file = FileUtils.getMetadata(path); + const buffer = await FileUtils.readFile(path); + const fileUrl = new URL(`data:${file.mimeType};base64,${buffer.toString("base64")}`); + + if (file.fileType !== "image" && file.fileType !== "text" && file.fileType !== "file") { + throw new ValidationError( + `Unsupported file type: ${file.fileType}. Only image, text and PDF files are supported.`, + context, + `File: ${path}, Detected type: ${file.fileType}, MIME: ${file.mimeType}` + ); + } - logger.info(`Analyzing ${file.filename}…`) + logger.info(`Analyzing ${file.filename}…`); // Call LLM // --------------------------- @@ -112,19 +125,35 @@ export abstract class BaseExtractor implements IExtractor { } } catch (error) { + if (error instanceof BaseError) { + throw error; // Re-throw our custom errors + } + if (APICallError.isInstance(error)) { - throw new Error( - `AI_API_CALL_ERROR: ${this.model.provider}:${this.model.modelId} returned: ${error.message.slice(0, 300)}` - ) + throw new AIServiceError( + this.model.provider, + this.model.modelId, + error.message, + context + ); } if (NoObjectGeneratedError.isInstance(error)) { - logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} returned: \n${error.text}`) - logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} cause: \n${error.cause}`) - throw new Error(`${this.model.provider}:${this.model.modelId} returned: ${error.message}`) + logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} returned: \n${error.text}`); + logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} cause: \n${error.cause}`); + + throw new ProcessingError( + 'object generation', + context, + `LLM Response: ${error.text}, Cause: ${error.cause}` + ); } - throw new Error(`${this.model.provider}:${this.model.modelId} returned: ${(error as Error).message}`) + throw new ProcessingError( + 'file analysis', + context, + (error as Error).message + ); } } } diff --git a/ai-invoice-receipt-fraud-detector/packages/core/src/analyzeDocument.ts b/ai-invoice-receipt-fraud-detector/packages/core/src/analyzeDocument.ts index 662307a..7b5a2ba 100644 --- a/ai-invoice-receipt-fraud-detector/packages/core/src/analyzeDocument.ts +++ b/ai-invoice-receipt-fraud-detector/packages/core/src/analyzeDocument.ts @@ -3,123 +3,150 @@ import { parseDocument } from "./parseDocument.js"; import { getFraudDetectionPrompt } from "./llm/prompts"; import { getCurrentLLM } from "./llm/models"; import type { DocumentInput, DetectionResult } from "./types"; +import { + BaseError, + ValidationError, + AIServiceError, + ProcessingError, +} from "../../../shared/errors/base.js"; +import { ErrorCode, ErrorContext } from "../../../shared/errors/types.js"; export async function analyzeDocument( input: DocumentInput ): Promise { - const parsedData = await parseDocument(input); - const extractedText = - parsedData.extracted_text || JSON.stringify(parsedData, null, 2); - const metadata = parsedData.metadata || {}; - let is_fake = false; - const indicators: DetectionResult["indicators"] = []; - - // 🔎 0. Rejet si document image/pdf sans signes de facture - let fileExt = ""; - if ( - input.type === "file" && - "path" in input && - typeof input.path === "string" - ) { - fileExt = path.extname(input.path).toLowerCase(); - } - - const isImage = [".png", ".jpg", ".jpeg"].includes(fileExt); - const isPDF = fileExt === ".pdf"; - const textContent = typeof extractedText === "string" ? extractedText : ""; - const keywords = [ - "invoice", - "facture", - "total", - "payment", - "receipt", - "numéro", - "commande", - "prix", - "amount", - ]; - const containsInvoiceTerms = keywords.some((word) => - textContent.toLowerCase().includes(word) - ); + const context: ErrorContext = { + operation: "analyzeDocument", + module: "fraud-detector", + timestamp: new Date().toISOString(), + metadata: { inputType: input.type }, + }; - if ( - (isImage || isPDF) && - (textContent.length < 200 || !containsInvoiceTerms) - ) { - return { - is_fake: false, - confidence: 0, - indicators: [ - { - category: "textual", - value: "Unrecognized document type", - description: - "The document does not appear to contain a valid invoice or receipt.", - }, - ], - }; - } + try { + const parsedData = await parseDocument(input); + const extractedText = + parsedData.extracted_text || JSON.stringify(parsedData, null, 2); + const metadata = parsedData.metadata || {}; + let is_fake = false; + const indicators: DetectionResult["indicators"] = []; + + // 🔎 0. Rejet si document image/pdf sans signes de facture + let fileExt = ""; + if ( + input.type === "file" && + "path" in input && + typeof input.path === "string" + ) { + fileExt = path.extname(input.path).toLowerCase(); + } - // 🔍 1. Heuristic check: Total vs item sum - if (parsedData.total && Array.isArray(parsedData.items)) { - const sum = parsedData.items.reduce( - (acc, item) => acc + parseFloat(item.price || "0"), - 0 + const isImage = [".png", ".jpg", ".jpeg"].includes(fileExt); + const isPDF = fileExt === ".pdf"; + const textContent = typeof extractedText === "string" ? extractedText : ""; + const keywords = [ + "invoice", + "facture", + "total", + "payment", + "receipt", + "numéro", + "commande", + "prix", + "amount", + ]; + const containsInvoiceTerms = keywords.some((word) => + textContent.toLowerCase().includes(word) ); - const declared = parseFloat(parsedData.total); - if (Math.abs(sum - declared) > 0.01) { - indicators.push({ - category: "textual", - value: "invoice total does not match item sum", - description: `Declared total is ${declared}, but sum of items is ${sum.toFixed( - 2 - )}`, - }); - is_fake = true; + if ( + (isImage || isPDF) && + (textContent.length < 200 || !containsInvoiceTerms) + ) { + return { + is_fake: false, + confidence: 0, + indicators: [ + { + category: "textual", + value: "Unrecognized document type", + description: + "The document does not appear to contain a valid invoice or receipt.", + }, + ], + }; } - } - // 🔍 2. Metadata check (heuristic) - if (metadata.producer) { - const suspiciousPatterns = ["ai", "generator", "fake", "synth"]; - const lowerProducer = metadata.producer.toLowerCase(); + // 🔍 1. Heuristic check: Total vs item sum + if (parsedData.total && Array.isArray(parsedData.items)) { + const sum = parsedData.items.reduce( + (acc, item) => acc + parseFloat(item.price || "0"), + 0 + ); + const declared = parseFloat(parsedData.total); - if (suspiciousPatterns.some((pattern) => lowerProducer.includes(pattern))) { - indicators.push({ - category: "metadata", - value: metadata.producer, - description: "Suspicious PDF producer suggests AI-generated document", - }); - is_fake = true; + if (Math.abs(sum - declared) > 0.01) { + indicators.push({ + category: "textual", + value: "invoice total does not match item sum", + description: `Declared total is ${declared}, but sum of items is ${sum.toFixed( + 2 + )}`, + }); + is_fake = true; + } } - } - // 🧠 3. Prompt-based LLM analysis - const metadataBlock = Object.entries(metadata) - .map(([k, v]) => `- ${k}: ${v}`) - .join("\n"); - - const promptTemplate = await getFraudDetectionPrompt(extractedText); - const finalPrompt = `${promptTemplate} - -===== DOCUMENT TEXT ===== -${extractedText} - -===== METADATA ===== -${metadataBlock || "None"} -`; - - console.log("🧠 Prompt envoyé au modèle :\n", finalPrompt); - - const llmResult = await getCurrentLLM().generate(finalPrompt); + // 🔍 2. Metadata check (heuristic) + if (metadata.producer) { + const suspiciousPatterns = ["ai", "generator", "fake", "synth"]; + const lowerProducer = metadata.producer.toLowerCase(); + + if ( + suspiciousPatterns.some((pattern) => lowerProducer.includes(pattern)) + ) { + indicators.push({ + category: "metadata", + value: metadata.producer, + description: "Suspicious PDF producer suggests AI-generated document", + }); + is_fake = true; + } + } - // 🧮 4. Combine results (heuristics + LLM) - const combined: DetectionResult = { - is_fake: is_fake || llmResult.is_fake, - confidence: Math.max(llmResult.confidence, is_fake ? 60 : 0), - indicators: [...indicators, ...llmResult.indicators], - }; + // 🧠 3. Prompt-based LLM analysis + const metadataBlock = Object.entries(metadata) + .map(([k, v]) => `- ${k}: ${v}`) + .join("\n"); + + const promptTemplate = await getFraudDetectionPrompt(extractedText); + const finalPrompt = `${promptTemplate} + + ===== DOCUMENT TEXT ===== + ${extractedText} + + ===== METADATA ===== + ${metadataBlock || "None"} + `; + + console.log("🧠 Prompt envoyé au modèle :\n", finalPrompt); + + const llmResult = await getCurrentLLM().generate(finalPrompt); + + // 🧮 4. Combine results (heuristics + LLM) + const combined: DetectionResult = { + is_fake: is_fake || llmResult.is_fake, + confidence: Math.max(llmResult.confidence, is_fake ? 60 : 0), + indicators: [...indicators, ...llmResult.indicators], + }; - return combined; + return combined; + } catch (error) { + if (error instanceof BaseError) { + throw error; + } + throw new ProcessingError( + "document analysis", + context, + (error as Error).message + ); + } } diff --git a/ai-receipt-generator/src/core/api/router.py b/ai-receipt-generator/src/core/api/router.py index b90c52b..3d83e82 100644 --- a/ai-receipt-generator/src/core/api/router.py +++ b/ai-receipt-generator/src/core/api/router.py @@ -30,6 +30,7 @@ GenerationRequest ) from ..services.receipt_service import ReceiptService +from ..errors import ReceiptGeneratorError, ErrorCode, RecoveryStrategy router = APIRouter() @@ -79,16 +80,17 @@ async def get_status(): # Receipt Generation Endpoints # ============================== +@router.exception_handler(ReceiptGeneratorError) +async def receipt_generator_exception_handler(request: Request, exc: ReceiptGeneratorError): + """Handle custom receipt generator errors""" + return JSONResponse( + status_code=exc.status_code, + content=exc.to_dict() + ) + +# Remove lines 121-132 (duplicate exception handlers) @router.post("/generate", response_model=GenerationResult, tags=["Generation"]) async def generate_receipt(request: ReceiptGenerationRequest): - """ - Generate a complete receipt with optional image - - - **input_fields**: Optional overrides for receipt generation - - **style**: Visual style for receipt generation - - **include_image**: Whether to generate image - - **image_config**: Optional image generation configuration - """ try: # Generate receipt data receipt_data = receipt_service.generate_receipt_data( @@ -114,7 +116,21 @@ async def generate_receipt(request: ReceiptGenerationRequest): "style_used": request.style } ) - except ValueError as e: + except ReceiptGeneratorError: + raise # Let the global exception handler deal with it + except Exception as e: + error = ReceiptGeneratorError( + code=ErrorCode.INTERNAL_ERROR, + message="Unexpected error during receipt generation", + status_code=500, + operation="generate_receipt", + recovery=RecoveryStrategy("retry", "Please try again"), + user_message="An unexpected error occurred. Please try again.", + technical_details=str(e) + ) + raise error.to_http_exception() + # Remove the duplicate except blocks below + except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid request: {str(e)}" diff --git a/ai-receipt-generator/src/core/errors/__init__.py b/ai-receipt-generator/src/core/errors/__init__.py new file mode 100644 index 0000000..ead0fde --- /dev/null +++ b/ai-receipt-generator/src/core/errors/__init__.py @@ -0,0 +1,118 @@ +"""Centralized error handling for receipt generator""" +from enum import Enum +from typing import Dict, Any, Optional, List +from datetime import datetime +from fastapi import HTTPException +import json + +class ErrorCode(str, Enum): + # Validation Errors + INVALID_STYLE = "INVALID_STYLE" + MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD" + INVALID_CONFIGURATION = "INVALID_CONFIGURATION" + + # Resource Errors + STYLE_NOT_FOUND = "STYLE_NOT_FOUND" + + # Processing Errors + GENERATION_FAILED = "GENERATION_FAILED" + PARSING_FAILED = "PARSING_FAILED" + VALIDATION_FAILED = "VALIDATION_FAILED" + + # External Service Errors + AI_SERVICE_ERROR = "AI_SERVICE_ERROR" + + # System Errors + INTERNAL_ERROR = "INTERNAL_ERROR" + +class RecoveryStrategy: + def __init__(self, + strategy_type: str, + description: str, + auto_retry: bool = False, + max_retries: int = 0, + fallback_action: Optional[str] = None): + self.type = strategy_type + self.description = description + self.auto_retry = auto_retry + self.max_retries = max_retries + self.fallback_action = fallback_action + +class ReceiptGeneratorError(Exception): + def __init__(self, + code: ErrorCode, + message: str, + status_code: int, + operation: str, + recovery: RecoveryStrategy, + user_message: str, + technical_details: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None): + super().__init__(message) + self.code = code + self.status_code = status_code + self.operation = operation + self.recovery = recovery + self.user_message = user_message + self.technical_details = technical_details + self.metadata = metadata or {} + self.timestamp = datetime.now().isoformat() + + def to_dict(self) -> Dict[str, Any]: + return { + "error": { + "code": self.code.value, + "message": self.user_message, + "status_code": self.status_code, + "timestamp": self.timestamp, + "operation": self.operation, + "recovery": { + "type": self.recovery.type, + "description": self.recovery.description, + "auto_retry": self.recovery.auto_retry, + "max_retries": self.recovery.max_retries, + "fallback_action": self.recovery.fallback_action + }, + "technical_details": self.technical_details, + "metadata": self.metadata + } + } + + def to_http_exception(self) -> HTTPException: + return HTTPException( + status_code=self.status_code, + detail=self.to_dict()["error"] + ) + +# Specific error classes +class StyleNotFoundError(ReceiptGeneratorError): + def __init__(self, style_name: str, available_styles: List[str]): + super().__init__( + code=ErrorCode.STYLE_NOT_FOUND, + message=f"Style '{style_name}' not found", + status_code=404, + operation="style_lookup", + recovery=RecoveryStrategy( + "manual", + f"Use one of the available styles: {', '.join(available_styles)}" + ), + user_message=f"The requested style '{style_name}' is not available.", + metadata={"requested_style": style_name, "available_styles": available_styles} + ) + +class GenerationFailedError(ReceiptGeneratorError): + def __init__(self, operation: str, original_error: str): + super().__init__( + code=ErrorCode.GENERATION_FAILED, + message=f"Generation failed during {operation}", + status_code=500, + operation=operation, + recovery=RecoveryStrategy( + "retry", + "Retry the generation or contact support if the issue persists", + auto_retry=True, + max_retries=2 + ), + user_message=f"Failed to generate receipt. Please try again.", + technical_details=original_error + ) \ No newline at end of file diff --git a/ai-receipt-generator/src/core/services/receipt_service.py b/ai-receipt-generator/src/core/services/receipt_service.py index 7bbd64b..dbcd97b 100644 --- a/ai-receipt-generator/src/core/services/receipt_service.py +++ b/ai-receipt-generator/src/core/services/receipt_service.py @@ -15,6 +15,7 @@ from ..generators.base import BaseGenerator from ..generators.openai_generator import OpenAIGenerator from ..generators.anthropic_generator import AnthropicGenerator +from ..errors import StyleNotFoundError, GenerationFailedError, ErrorCode, RecoveryStrategy, ReceiptGeneratorError faker = Faker("fr_FR") @@ -44,32 +45,23 @@ def generate_receipt_image( style: str = "table_noire", image_config: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: - """ - Generate receipt image from data and style - - Args: - receipt_data: Receipt data dictionary - style: Style name for visual generation - image_config: Optional image generation configuration - - Returns: - Dictionary containing image data and metadata - """ - # Load style - style_path = self.style_dir / f"{style}.json" - if not style_path.exists(): - raise ValueError(f"Style '{style}' not found") - - style_data = json.loads(style_path.read_text(encoding="utf-8")) - - # Generate prompt - prompt = generate_image_prompt(receipt_data, style_data) - - # Get image generator - generator = self._get_image_generator(image_config) - - # Generate image + """Generate receipt image from data and style""" try: + # Load style + style_path = self.style_dir / f"{style}.json" + if not style_path.exists(): + available_styles = self.get_available_styles() + raise StyleNotFoundError(style, available_styles) + + style_data = json.loads(style_path.read_text(encoding="utf-8")) + + # Generate prompt + prompt = generate_image_prompt(receipt_data, style_data) + + # Get image generator + generator = self._get_image_generator(image_config) + + # Generate image image_data = generator.generate(prompt) return { "image_data": image_data, @@ -81,8 +73,10 @@ def generate_receipt_image( "data_hash": hash(str(receipt_data)) } } + except StyleNotFoundError: + raise # Re-raise our custom errors except Exception as e: - raise RuntimeError(f"Image generation failed: {str(e)}") + raise GenerationFailedError("image_generation", str(e)) def parse_receipt_data(self, receipt_text: str) -> Dict[str, Any]: """ @@ -312,4 +306,4 @@ def _extract_date(self, text: str) -> str: if match: return match.group() - return datetime.now().strftime("%Y-%m-%d") \ No newline at end of file + return datetime.now().strftime("%Y-%m-%d") \ No newline at end of file diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md new file mode 100644 index 0000000..6cdd1f9 --- /dev/null +++ b/docs/ERROR_HANDLING.md @@ -0,0 +1,84 @@ +# Error Handling and Recovery Guide + +## Common Error Scenarios + +### 1. File Format Errors + +**Error Code**: `INVALID_FILE_FORMAT` +**Common Causes**: + +- Unsupported file types +- Corrupted files +- Files exceeding size limits + +**Recovery Strategies**: + +- Convert file to supported format (PDF, PNG, JPG) +- Check file integrity +- Reduce file size if needed + +### 2. AI Service Errors + +**Error Code**: `AI_SERVICE_ERROR` +**Common Causes**: + +- API rate limits +- Network timeouts +- Invalid API keys +- Service outages + +**Recovery Strategies**: + +- Automatic retry with exponential backoff +- Switch to alternative AI provider +- Check API key validity +- Monitor service status + +### 3. Processing Failures + +**Error Code**: `PROCESSING_FAILED` +**Common Causes**: + +- Invalid input data +- Memory limitations +- Timeout errors + +**Recovery Strategies**: + +- Validate input data +- Break large operations into smaller chunks +- Increase timeout limits +- Retry with different parameters + +## Implementation Guidelines + +### For Contributors + +1. **Always use centralized error classes** +2. **Provide meaningful error messages** +3. **Include recovery strategies** +4. **Log technical details for debugging** +5. **Test error scenarios thoroughly** + +### Error Response Format + +All errors follow this consistent format: + +```json +{ + "error": { + "code": "ERROR_CODE", + "message": "User-friendly message", + "status_code": 400, + "timestamp": "2024-01-15T10:30:00Z", + "operation": "operation_name", + "recovery": { + "type": "retry", + "description": "Retry the request with valid parameters", + "auto_retry": false, + "max_retries": 3 + }, + "technical_details": "Detailed error information for debugging" + } +} +``` diff --git a/shared/errors/base.ts b/shared/errors/base.ts new file mode 100644 index 0000000..fe860a9 --- /dev/null +++ b/shared/errors/base.ts @@ -0,0 +1,117 @@ +import { + ErrorCode, + ErrorDetails, + ErrorContext, + RecoveryStrategy, +} from "./types.js"; + +export abstract class BaseError extends Error { + public readonly code: ErrorCode; + public readonly statusCode: number; + public readonly context: ErrorContext; + public readonly recovery: RecoveryStrategy; + public readonly userMessage: string; + public readonly technicalDetails?: string; + public readonly timestamp: string; + + constructor(details: ErrorDetails) { + super(details.message); + this.name = this.constructor.name; + this.code = details.code; + this.statusCode = details.statusCode; + this.context = details.context; + this.recovery = details.recovery; + this.userMessage = details.userMessage; + this.technicalDetails = details.technicalDetails; + this.timestamp = new Date().toISOString(); + + // Ensure proper prototype chain + Object.setPrototypeOf(this, new.target.prototype); + } + + toJSON() { + return { + error: { + code: this.code, + message: this.userMessage, + statusCode: this.statusCode, + timestamp: this.timestamp, + context: this.context, + recovery: this.recovery, + technicalDetails: this.technicalDetails, + }, + }; + } +} + +// Specific error classes +export class ValidationError extends BaseError { + constructor( + message: string, + context: ErrorContext, + technicalDetails?: string + ) { + super({ + code: ErrorCode.INVALID_CONFIGURATION, + message, + statusCode: 400, + context, + recovery: { + type: "manual", + description: "Please check your input parameters and try again", + }, + userMessage: message, + technicalDetails, + }); + } +} + +export class AIServiceError extends BaseError { + constructor( + provider: string, + model: string, + originalError: string, + context: ErrorContext + ) { + const message = `AI service error from ${provider}:${model}`; + super({ + code: ErrorCode.AI_SERVICE_ERROR, + message, + statusCode: 502, + context, + recovery: { + type: "retry", + description: "Retry the request or try a different AI provider", + autoRetry: true, + maxRetries: 3, + fallbackAction: "Switch to alternative AI provider", + }, + userMessage: "AI service is temporarily unavailable. Please try again.", + technicalDetails: originalError, + }); + } +} + +export class ProcessingError extends BaseError { + constructor( + operation: string, + context: ErrorContext, + technicalDetails?: string + ) { + super({ + code: ErrorCode.PROCESSING_FAILED, + message: `Processing failed during ${operation}`, + statusCode: 500, + context, + recovery: { + type: "retry", + description: + "Retry the operation or contact support if the issue persists", + autoRetry: false, + maxRetries: 1, + }, + userMessage: `Failed to process ${operation}. Please try again.`, + technicalDetails, + }); + } +} diff --git a/shared/errors/types.ts b/shared/errors/types.ts new file mode 100644 index 0000000..d33ddde --- /dev/null +++ b/shared/errors/types.ts @@ -0,0 +1,64 @@ +// Centralized error types for all modules +export enum ErrorCode { + // Validation Errors (400-499) + INVALID_FILE_FORMAT = "INVALID_FILE_FORMAT", + MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD", + INVALID_CONFIGURATION = "INVALID_CONFIGURATION", + UNSUPPORTED_OPERATION = "UNSUPPORTED_OPERATION", + + // Authentication/Authorization (401-403) + INVALID_API_KEY = "INVALID_API_KEY", + INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", + + // Resource Errors (404-409) + RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND", + STYLE_NOT_FOUND = "STYLE_NOT_FOUND", + MODEL_NOT_FOUND = "MODEL_NOT_FOUND", + + // Rate Limiting (429) + RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", + + // External Service Errors (500-503) + AI_SERVICE_ERROR = "AI_SERVICE_ERROR", + NETWORK_TIMEOUT = "NETWORK_TIMEOUT", + SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE", + + // Processing Errors (500) + PROCESSING_FAILED = "PROCESSING_FAILED", + GENERATION_FAILED = "GENERATION_FAILED", + PARSING_FAILED = "PARSING_FAILED", + VALIDATION_FAILED = "VALIDATION_FAILED", + + // System Errors (500) + INTERNAL_ERROR = "INTERNAL_ERROR", + CONFIGURATION_ERROR = "CONFIGURATION_ERROR", + DEPENDENCY_ERROR = "DEPENDENCY_ERROR", +} + +export interface ErrorContext { + operation: string; + module: "extractor" | "generator" | "fraud-detector"; + timestamp: string; + requestId?: string; + userId?: string; + metadata?: Record; +} + +export interface RecoveryStrategy { + type: "retry" | "fallback" | "manual" | "ignore"; + description: string; + autoRetry?: boolean; + maxRetries?: number; + fallbackAction?: string; +} + +export interface ErrorDetails { + code: ErrorCode; + message: string; + statusCode: number; + context: ErrorContext; + recovery: RecoveryStrategy; + userMessage: string; + technicalDetails?: string; + relatedErrors?: ErrorCode[]; +} From 6aa89ea8ac1315a1b26b9a7cb6c6f4f5d4e79758 Mon Sep 17 00:00:00 2001 From: Godwin-Ekoh Date: Thu, 31 Jul 2025 12:17:01 +0100 Subject: [PATCH 2/2] fix build issues --- ai-invoice-extractor/bun.lock | 221 +++++++++++- .../src/extractors/extractor.ts | 317 +++++++++++------- ai-invoice-extractor/src/types.ts | 70 ++-- ai-invoice-extractor/tsconfig.json | 44 +-- ai-receipt-generator/src/core/api/app.py | 12 + ai-receipt-generator/src/core/api/router.py | 14 +- 6 files changed, 492 insertions(+), 186 deletions(-) diff --git a/ai-invoice-extractor/bun.lock b/ai-invoice-extractor/bun.lock index 9c1122b..793a560 100644 --- a/ai-invoice-extractor/bun.lock +++ b/ai-invoice-extractor/bun.lock @@ -22,9 +22,14 @@ }, "devDependencies": { "@biomejs/biome": "^1", + "@rollup/rollup-win32-x64-msvc": "^4.45.1", "@types/figlet": "^1.7.0", - "@types/node": "^22.15.21", + "@types/node": "^22.16.5", + "@vitest/ui": "^3.2.4", + "bun-types": "^1.2.19", + "tsx": "^4.20.3", "typescript": "^5", + "vitest": "^3.2.4", }, }, }, @@ -63,26 +68,152 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.8", "", { "os": "android", "cpu": "arm" }, "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.8", "", { "os": "android", "cpu": "arm64" }, "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.8", "", { "os": "android", "cpu": "x64" }, "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.8", "", { "os": "linux", "cpu": "arm" }, "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.8", "", { "os": "linux", "cpu": "ia32" }, "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.8", "", { "os": "linux", "cpu": "x64" }, "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.8", "", { "os": "none", "cpu": "x64" }, "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.8", "", { "os": "sunos", "cpu": "x64" }, "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.2", "", { "os": "android", "cpu": "arm64" }, "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.2", "", { "os": "win32", "cpu": "x64" }, "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg=="], + + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="], "@types/dotenv": ["@types/dotenv@6.1.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/figlet": ["@types/figlet@1.7.0", "", {}, "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q=="], - "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + "@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/ui": ["@vitest/ui@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.1", "tinyglobby": "^0.2.14", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.2.4" } }, "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" } }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], @@ -91,6 +222,10 @@ "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], @@ -99,24 +234,52 @@ "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "figlet": ["figlet@1.8.1", "", { "bin": "bin/index.js" }, "sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg=="], + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": "bin/jsondiffpatch.js" }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "loupe": ["loupe@3.2.0", "", {}, "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "ollama-ai-provider": ["ollama-ai-provider@1.2.0", "", { "dependencies": { "@ai-sdk/provider": "^1.0.0", "@ai-sdk/provider-utils": "^2.0.0", "partial-json": "0.1.7" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww=="], @@ -127,6 +290,14 @@ "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], @@ -135,6 +306,8 @@ "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], @@ -145,32 +318,76 @@ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], + "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "zod": ["zod@3.25.30", "", {}, "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + + "@types/dotenv/@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + + "bun-types/@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], } } diff --git a/ai-invoice-extractor/src/extractors/extractor.ts b/ai-invoice-extractor/src/extractors/extractor.ts index 74da6ef..a2cf3eb 100644 --- a/ai-invoice-extractor/src/extractors/extractor.ts +++ b/ai-invoice-extractor/src/extractors/extractor.ts @@ -1,34 +1,52 @@ -import { PROMPTS } from "@/constants" -import { logger } from "@/libs/logger" -import type { AiConfig, AnyString, MistralModelId, OpenAIModelId, GoogleModelId, AnthropicModelId, PromptId } from "@/types" -import { FileUtils } from "@/utils/file" -import { StringUtils } from "@/utils/string" -import { createMistral } from "@ai-sdk/mistral" -import { createOpenAI } from "@ai-sdk/openai" -import { createGoogleGenerativeAI } from "@ai-sdk/google" -import { createAnthropic } from "@ai-sdk/anthropic" -import { createOllama } from 'ollama-ai-provider'; -import { APICallError, type LanguageModelV1, NoObjectGeneratedError, generateObject } from "ai" -import type { z } from "zod" -import { BaseError, AIServiceError, ValidationError, ProcessingError } from '../../../shared/errors/base.js'; -import { ErrorCode, ErrorContext } from '../../../shared/errors/types.js'; +import { PROMPTS } from "../constants.js"; +import { logger } from "../libs/logger.js"; +import type { + AiConfig, + AnyString, + MistralModelId, + OpenAIModelId, + GoogleModelId, + AnthropicModelId, + PromptId, +} from "../types.js"; +import { FileUtils } from "../utils/file.js"; +import { StringUtils } from "../utils/string.js"; +import { createMistral } from "@ai-sdk/mistral"; +import { createOpenAI } from "@ai-sdk/openai"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { createAnthropic } from "@ai-sdk/anthropic"; +import { createOllama } from "ollama-ai-provider"; +import { + APICallError, + type LanguageModelV1, + NoObjectGeneratedError, + generateObject, +} from "ai"; +import type { z } from "zod"; +import { + BaseError, + AIServiceError, + ValidationError, + ProcessingError, +} from "../../../shared/errors/base.js"; +import { ErrorCode, ErrorContext } from "../../../shared/errors/types.js"; // ============================== // Types // ============================== type AnalyseFilePathInput = { - path: string - prompt: PromptId | AnyString - output: z.ZodType -} + path: string; + prompt: PromptId | AnyString; + output: z.ZodType; +}; // ============================== // Interfaces // ============================== export interface IExtractor { - analyseFile(props: AnalyseFilePathInput): Promise + analyseFile(props: AnalyseFilePathInput): Promise; } // ============================== @@ -36,28 +54,38 @@ export interface IExtractor { // ============================== export abstract class BaseExtractor implements IExtractor { - public constructor(protected readonly model: LanguageModelV1) { } + public constructor(protected readonly model: LanguageModelV1) {} // Public methods // ============================== async analyseFile(input: AnalyseFilePathInput): Promise { const { path, prompt = "EXTRACT_INVOICE", output } = input; - + const context: ErrorContext = { - operation: 'analyseFile', - module: 'extractor', + operation: "analyseFile", + module: "extractor", timestamp: new Date().toISOString(), - metadata: { path, prompt, model: `${this.model.provider}:${this.model.modelId}` } + metadata: { + path, + prompt, + model: `${this.model.provider}:${this.model.modelId}`, + }, }; try { // Get file data const file = FileUtils.getMetadata(path); const buffer = await FileUtils.readFile(path); - const fileUrl = new URL(`data:${file.mimeType};base64,${buffer.toString("base64")}`); + const fileUrl = new globalThis.URL( + `data:${file.mimeType};base64,${buffer.toString("base64")}` + ); - if (file.fileType !== "image" && file.fileType !== "text" && file.fileType !== "file") { + if ( + file.fileType !== "image" && + file.fileType !== "text" && + file.fileType !== "file" + ) { throw new ValidationError( `Unsupported file type: ${file.fileType}. Only image, text and PDF files are supported.`, context, @@ -67,92 +95,115 @@ export abstract class BaseExtractor implements IExtractor { logger.info(`Analyzing ${file.filename}…`); - // Call LLM - // --------------------------- - try { - const instructions = PROMPTS[prompt as PromptId] || prompt - - logger.debug(`Using instructions: \n${instructions}`) - - const content = [ - file.fileType === "image" - ? { - type: "image" as const, - image: fileUrl, - mimeType: file.mimeType - } - : file.fileType === "text" + // Call LLM + // --------------------------- + try { + const instructions = PROMPTS[prompt as PromptId] || prompt; + + logger.debug(`Using instructions: \n${instructions}`); + + const content = [ + file.fileType === "image" + ? { + type: "image" as const, + image: fileUrl, + mimeType: file.mimeType, + } + : file.fileType === "text" ? { - type: "text" as const - } + type: "text" as const, + text: buffer.toString(), + } : { - type: "file" as const, - data: fileUrl, - mimeType: file.mimeType, - filename: file.filename - } - ] - - logger.debug( - `Using content: \n${JSON.stringify( - content.map(c => ({ - type: c.type, - mimeType: c.mimeType, - filename: "filename" in c ? c.filename : undefined, - image: "image" in c ? StringUtils.mask(c.image?.toString() ?? "", 50) : undefined, - data: "data" in c ? StringUtils.mask(c.prompt?.toString() ?? "", 50) : undefined, - prompt: buffer.toString() ? buffer.toString() : undefined - })) - )}\n` - ) - - if (file.fileType !== "text") { - const { object } = await generateObject({ - model: this.model, - schema: output, - system: instructions, - messages: [{ role: "user", content }] - }) - return object - } else { - const { object } = await generateObject({ - model: this.model, - schema: output, - system: instructions, - prompt: buffer.toString() - }) - return object - } + type: "file" as const, + data: fileUrl, + mimeType: file.mimeType, + filename: file.filename, + }, + ]; - } catch (error) { - if (error instanceof BaseError) { - throw error; // Re-throw our custom errors - } - - if (APICallError.isInstance(error)) { - throw new AIServiceError( - this.model.provider, - this.model.modelId, - error.message, - context + logger.debug( + `Using content: \n${JSON.stringify( + content.map((c) => ({ + type: c.type, + mimeType: "mimeType" in c ? c.mimeType : undefined, + filename: "filename" in c ? c.filename : undefined, + image: + "image" in c + ? StringUtils.mask(c.image?.toString() ?? "", 50) + : undefined, + data: + "data" in c + ? StringUtils.mask(c.data?.toString() ?? "", 50) + : undefined, + text: + "text" in c ? StringUtils.mask(c.text ?? "", 50) : undefined, + })) + )}\n` ); - } - if (NoObjectGeneratedError.isInstance(error)) { - logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} returned: \n${error.text}`); - logger.debug(`[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} cause: \n${error.cause}`); - + if (file.fileType !== "text") { + const { object } = await generateObject({ + model: this.model, + schema: output, + system: instructions, + messages: [{ role: "user", content }], + }); + return object; + } else { + const { object } = await generateObject({ + model: this.model, + schema: output, + system: instructions, + prompt: buffer.toString(), + }); + return object; + } + } catch (error: unknown) { + if (error instanceof BaseError) { + throw error; // Re-throw our custom errors + } + + if (APICallError.isInstance(error)) { + throw new AIServiceError( + this.model.provider, + this.model.modelId, + error.message, + context + ); + } + + if (NoObjectGeneratedError.isInstance(error)) { + logger.debug( + `[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} returned: \n${error.text}` + ); + logger.debug( + `[NoObjectGeneratedError] ${this.model.provider}:${this.model.modelId} cause: \n${error.cause}` + ); + + throw new ProcessingError( + "object generation", + context, + `LLM Response: ${error.text}, Cause: ${error.cause}` + ); + } + throw new ProcessingError( - 'object generation', + "file analysis", context, - `LLM Response: ${error.text}, Cause: ${error.cause}` + error instanceof Error ? error.message : "Unknown error" ); } + } catch (error: unknown) { + // Handle errors from file processing (outer try block) + if (error instanceof BaseError) { + throw error; // Re-throw our custom errors + } throw new ProcessingError( - 'file analysis', + "file processing", context, - (error as Error).message + error instanceof Error ? error.message : "Unknown error" ); } } @@ -164,41 +215,63 @@ export abstract class BaseExtractor implements IExtractor { export class MistralExtractor extends BaseExtractor { constructor(model: MistralModelId | AnyString, apiKey: string) { - logger.debug(`Creating extractor mistral:${model} with apiKey: ${StringUtils.mask(apiKey)}`) - const mistral = createMistral({ apiKey }) - super(mistral(model)) + logger.debug( + `Creating extractor mistral:${model} with apiKey: ${StringUtils.mask( + apiKey + )}` + ); + const mistral = createMistral({ apiKey }); + super(mistral(model)); } } export class AnthropicExtractor extends BaseExtractor { constructor(model: AnthropicModelId | AnyString, apiKey: string) { - logger.debug(`Creating extractor anthropic:${model} with apiKey: ${StringUtils.mask(apiKey)}`) - const anthropic = createAnthropic({ apiKey }) - super(anthropic(model)) + logger.debug( + `Creating extractor anthropic:${model} with apiKey: ${StringUtils.mask( + apiKey + )}` + ); + const anthropic = createAnthropic({ apiKey }); + super(anthropic(model)); } } export class OpenAIExtractor extends BaseExtractor { constructor(model: OpenAIModelId | AnyString, apiKey: string) { - logger.debug(`Creating extractor openai:${model} with apiKey: ${StringUtils.mask(apiKey)}`) - const openai = createOpenAI({ apiKey }) - super(openai(model)) + logger.debug( + `Creating extractor openai:${model} with apiKey: ${StringUtils.mask( + apiKey + )}` + ); + const openai = createOpenAI({ apiKey }); + super(openai(model)); } } export class GoogleExtractor extends BaseExtractor { constructor(model: GoogleModelId | AnyString, apiKey: string) { - logger.debug(`Creating extractor google:${model} with apiKey: ${StringUtils.mask(apiKey)}`) - const google = createGoogleGenerativeAI({ apiKey }) - super(google(model)) + logger.debug( + `Creating extractor google:${model} with apiKey: ${StringUtils.mask( + apiKey + )}` + ); + const google = createGoogleGenerativeAI({ apiKey }); + super(google(model)); } } export class OllamaExtractor extends BaseExtractor { - constructor(model: AnyString, apiKey: string) { - logger.debug(`Creating extractor ollama:${model} with apiKey: ${StringUtils.mask(apiKey)}`) - const ollama = createOllama({ apiKey }) - super(ollama(model)) + constructor(model: AnyString, baseURL?: string) { + logger.debug( + `Creating extractor ollama:${model} with baseURL: ${ + baseURL || "http://localhost:11434" + }` + ); + const ollama = createOllama({ + baseURL: baseURL || "http://localhost:11434", + }); + super(ollama(model)); } } @@ -210,17 +283,17 @@ export class Extractor { static create(config: AiConfig): IExtractor { switch (config.vendor) { case "openai": - return new OpenAIExtractor(config.model, config.apiKey) + return new OpenAIExtractor(config.model, config.apiKey); case "mistral": - return new MistralExtractor(config.model, config.apiKey) + return new MistralExtractor(config.model, config.apiKey); case "google": - return new GoogleExtractor(config.model, config.apiKey) + return new GoogleExtractor(config.model, config.apiKey); case "anthropic": - return new AnthropicExtractor(config.model, config.apiKey) + return new AnthropicExtractor(config.model, config.apiKey); case "ollama": - return new OllamaExtractor(config.model, config.apiKey) + return new OllamaExtractor(config.model, config.baseURL); default: - throw new Error(`Unsupported vendor: ${(config as AiConfig).vendor}`) + throw new Error(`Unsupported vendor: ${(config as AiConfig).vendor}`); } } } diff --git a/ai-invoice-extractor/src/types.ts b/ai-invoice-extractor/src/types.ts index 5ff7462..d73c6d2 100644 --- a/ai-invoice-extractor/src/types.ts +++ b/ai-invoice-extractor/src/types.ts @@ -1,67 +1,81 @@ -import z from "zod/v4" -import { MISTRAL_MODEL_ID, OPENAI_MODEL_ID, GOOGLE_MODEL_ID, ANTHROPIC_MODEL_ID, PROMPT_ID } from "./constants" +import z from "zod/v4"; +import { + MISTRAL_MODEL_ID, + OPENAI_MODEL_ID, + GOOGLE_MODEL_ID, + ANTHROPIC_MODEL_ID, + PROMPT_ID, +} from "./constants.js"; // ============================== // Global types // ============================== -export type PromptId = z.infer -export const PromptId = z.enum(PROMPT_ID) +export type PromptId = z.infer; +export const PromptId = z.enum(PROMPT_ID); -export type MistralModelId = z.infer -export const MistralModelId = z.enum(MISTRAL_MODEL_ID) +export type MistralModelId = z.infer; +export const MistralModelId = z.enum(MISTRAL_MODEL_ID); -export type OpenAIModelId = z.infer -export const OpenAiModelId = z.enum(OPENAI_MODEL_ID) +export type OpenAIModelId = z.infer; +export const OpenAiModelId = z.enum(OPENAI_MODEL_ID); -export type AnthropicModelId = z.infer -export const AnthropicModelId = z.enum(ANTHROPIC_MODEL_ID) +export type AnthropicModelId = z.infer; +export const AnthropicModelId = z.enum(ANTHROPIC_MODEL_ID); -export type GoogleModelId = z.infer -export const GoogleModelId = z.enum(GOOGLE_MODEL_ID) +export type GoogleModelId = z.infer; +export const GoogleModelId = z.enum(GOOGLE_MODEL_ID); -export type ModelId = MistralModelId | OpenAIModelId | GoogleModelId +export type ModelId = MistralModelId | OpenAIModelId | GoogleModelId; -export const MistralVendor = z.literal("mistral") -export const OpenAiVendor = z.literal("openai") -export const GoogleVendor = z.literal("google") +export const MistralVendor = z.literal("mistral"); +export const OpenAiVendor = z.literal("openai"); +export const GoogleVendor = z.literal("google"); +export const AnthropicVendor = z.literal("anthropic"); +export const OllamaVendor = z.literal("ollama"); -export type AiVendor = z.infer -export const AiVendor = z.union([MistralVendor, OpenAiVendor, GoogleVendor]) +export type AiVendor = z.infer; +export const AiVendor = z.union([ + MistralVendor, + OpenAiVendor, + GoogleVendor, + AnthropicVendor, + OllamaVendor, +]); -export type AiConfig = z.infer +export type AiConfig = z.infer; export const AiConfig = z.discriminatedUnion("vendor", [ z.object({ vendor: z.literal("openai"), model: OpenAiModelId, - apiKey: z.string().min(1) + apiKey: z.string().min(1), }), z.object({ vendor: z.literal("mistral"), model: MistralModelId, - apiKey: z.string().min(1) + apiKey: z.string().min(1), }), z.object({ vendor: z.literal("google"), model: GoogleModelId, - apiKey: z.string().min(1) + apiKey: z.string().min(1), }), z.object({ vendor: z.literal("anthropic"), model: AnthropicModelId, - apiKey: z.string().min(1) + apiKey: z.string().min(1), }), z.object({ vendor: z.literal("ollama"), model: z.string().min(1), - apiKey: z.string().min(1) - }) -]) + baseURL: z.string().url().optional().default("http://localhost:11434"), + }), +]); // ============================== // Utils types // ============================== -export type AnyString = string & {} +export type AnyString = string & {}; -export type AsyncIterableStream = AsyncIterable & ReadableStream +export type AsyncIterableStream = AsyncIterable & ReadableStream; diff --git a/ai-invoice-extractor/tsconfig.json b/ai-invoice-extractor/tsconfig.json index c8de3f6..e98bc7e 100644 --- a/ai-invoice-extractor/tsconfig.json +++ b/ai-invoice-extractor/tsconfig.json @@ -1,24 +1,24 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ESNext"], - "rootDir": "./src", - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "sourceMap": true, - "baseUrl": ".", - "types": ["bun-types", "@types/node"], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*", "tests/**/*"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ESNext", "DOM"], + "rootDir": ".", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true, + "baseUrl": ".", + "useUnknownInCatchVariables": false, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/ai-receipt-generator/src/core/api/app.py b/ai-receipt-generator/src/core/api/app.py index bee2600..0e8fbed 100644 --- a/ai-receipt-generator/src/core/api/app.py +++ b/ai-receipt-generator/src/core/api/app.py @@ -11,6 +11,7 @@ from datetime import datetime from .router import router +from .errors import ReceiptGeneratorError # ============================== # Application Configuration @@ -260,3 +261,14 @@ async def debug_info(): "debug": DEBUG, "timestamp": datetime.now().isoformat() } + +# Add this to app.py +from .errors import ReceiptGeneratorError + +@app.exception_handler(ReceiptGeneratorError) +async def receipt_generator_exception_handler(request: Request, exc: ReceiptGeneratorError): + """Handle custom receipt generator errors""" + return JSONResponse( + status_code=exc.status_code, + content=exc.to_dict()["error"] + ) diff --git a/ai-receipt-generator/src/core/api/router.py b/ai-receipt-generator/src/core/api/router.py index 3d83e82..5d48073 100644 --- a/ai-receipt-generator/src/core/api/router.py +++ b/ai-receipt-generator/src/core/api/router.py @@ -88,7 +88,6 @@ async def receipt_generator_exception_handler(request: Request, exc: ReceiptGene content=exc.to_dict() ) -# Remove lines 121-132 (duplicate exception handlers) @router.post("/generate", response_model=GenerationResult, tags=["Generation"]) async def generate_receipt(request: ReceiptGenerationRequest): try: @@ -117,19 +116,10 @@ async def generate_receipt(request: ReceiptGenerationRequest): } ) except ReceiptGeneratorError: - raise # Let the global exception handler deal with it + raise # Let the exception handler deal with it except Exception as e: - error = ReceiptGeneratorError( - code=ErrorCode.INTERNAL_ERROR, - message="Unexpected error during receipt generation", - status_code=500, - operation="generate_receipt", - recovery=RecoveryStrategy("retry", "Please try again"), - user_message="An unexpected error occurred. Please try again.", - technical_details=str(e) - ) + error = GenerationFailedError("generate_receipt", str(e)) raise error.to_http_exception() - # Remove the duplicate except blocks below except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST,