Always reference these two python files to fill in the blanks:
AND
Nothing else is required.
types.ts:
export interface ProcessData {workflow_id: string;client_id: string;client_name: string;document_id: string;invoiceID: string;pdf_path: string;pdf_prefix?: string;image_urls?: string[];image_prefixes?: string[];di_json_url?: string;di_json_prefix?: string;ai_extracted_content?: any;client_config?: Record<string, any>;workflow_status?: string;workflow_substatus?: string;}export interface ProcessedDocumentLineItem {product_code: string;description: string;quantity: number;unit_price: number;extended_amount: number;discount?: number;}export interface ProcessedDocument {total_pages: number;invoice_number: string;invoice_date: string;invoice_total: number;line_items: ProcessedDocumentLineItem[];}
azureBlob Service:
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";const accountName = process.env.AZURE_STORAGE_ACCOUNT!;const accountKey = process.env.AZURE_STORAGE_KEY!;const containerName = process.env.AZURE_CONTAINER_NAME!;const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, sharedKeyCredential);const containerClient = blobServiceClient.getContainerClient(containerName);export async function uploadBlob(blobName: string, content: Buffer, contentType: string) {const blockBlobClient = containerClient.getBlockBlobClient(blobName);await blockBlobClient.upload(content, content.length, { blobHTTPHeaders: { blobContentType: contentType } });}export async function getBlobContent(blobName: string): Promise<Buffer> {const blockBlobClient = containerClient.getBlockBlobClient(blobName);const downloadResponse = await blockBlobClient.download();return Buffer.concat(await streamToBuffer(downloadResponse.readableStreamBody!));}export async function getSasUrl(blobName: string): Promise<string> {const blockBlobClient = containerClient.getBlockBlobClient(blobName);return blockBlobClient.url; // Add SAS token generation if needed}export async function blobExists(blobName: string): Promise<boolean> {const blockBlobClient = containerClient.getBlockBlobClient(blobName);return await blockBlobClient.exists();}async function streamToBuffer(readableStream: NodeJS.ReadableStream): Promise<Buffer[]> {const chunks: Buffer[] = [];for await (const chunk of readableStream) {chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));}return chunks;}
formRecoznizer:
import { DocumentAnalysisClient, AzureKeyCredential } from "@azure/ai-form-recognizer";const client = new DocumentAnalysisClient(process.env.FORM_RECOGNIZER_ENDPOINT!,new AzureKeyCredential(process.env.FORM_RECOGNIZER_KEY!));export async function analyzePdf(pdfPath: string): Promise<string> {const poller = await client.beginAnalyzeDocument("prebuilt-invoice", pdfPath);const result = await poller.pollUntilDone();return JSON.stringify(result);}
openAiExtractor:
import { OpenAIClient } from "@azure/openai";import { AzureKeyCredential } from "@azure/core-auth";import { ProcessedDocument } from "./types";import { PDFDocument } from "pdf-lib";export class OpenAIExtractor {private client: OpenAIClient;private MAX_PAGE_LIMIT = 20;constructor(private config: { endpoint: string; apiKey: string }) {this.client = new OpenAIClient(config.endpoint, new AzureKeyCredential(config.apiKey));}async extractFromBytes(content: Buffer, pageRotations: number[], filename: string): Promise<ProcessedDocument> {const base64Images = await this.convertPdfToImages(content);const batches = this.chunkArray(base64Images, 3);const responses: any[] = [];for (const batch of batches) {const response = await this.client.getChatCompletions("gpt-4", [{role: "user",content: [{ type: "text", text: "Extract invoice details from these images" },...batch.map(img => ({ type: "image_url", image_url: { url: `data:image/png;base64,${img}` } }))]}]);responses.push(JSON.parse(response.choices[0].message.content));}const combined = responses.length > 1 ? await this.combineResponses(responses) : responses[0];return combined as ProcessedDocument;}private async convertPdfToImages(content: Buffer): Promise<string[]> {const pdfDoc = await PDFDocument.load(content);const images: string[] = [];for (const page of pdfDoc.getPages()) {const pngImage = await page.renderToBuffer({ dpi: 300 }); // Use pdf-lib or pdf2picimages.push(pngImage.toString("base64"));}return images;}private chunkArray<T>(arr: T[], size: number): T[][] {return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));}private async combineResponses(responses: any[]): Promise<any> {const combinedPrompt = `Combine these invoice page extractions into one: ${JSON.stringify(responses)}`;const response = await this.client.getChatCompletions("gpt-4", [{ role: "user", content: combinedPrompt }]);return JSON.parse(response.choices[0].message.content);}}
Activities:
import { ProcessData } from "./types";import { getBlobContent, uploadBlob, getSasUrl, blobExists } from "./azureBlob";import { OpenAIExtractor } from "./openaiExtractor";import { analyzePdf } from "./formRecognizer";export async function generateImages(data: ProcessData): Promise<ProcessData> {const pdfContent = await getBlobContent(data.pdf_path);const images = []; // Implement PDF to image conversionconst sasUrls: string[] = [];const prefixes: string[] = [];for (const image of images) {const blobName = `jpg/${data.invoiceID}/page${image.pageNumber}/${data.pdf_prefix}-page-${image.pageNumber}.jpg`;await uploadBlob(blobName, image.buffer, "image/jpg");sasUrls.push(await getSasUrl(blobName));prefixes.push(blobName);}data.image_urls = sasUrls;data.image_prefixes = prefixes;return data;}export async function getBoundingRegions(data: ProcessData): Promise<ProcessData> {const blobName = `json/${data.client_id}/${data.invoiceID}/${data.pdf_prefix}.json`;if (!(await blobExists(blobName))) {const diResult = await analyzePdf(data.pdf_path);await uploadBlob(blobName, Buffer.from(diResult), "application/json");}data.di_json_url = await getSasUrl(blobName);data.di_json_prefix = blobName;return data;}export async function extractInvoice(data: ProcessData): Promise<ProcessData> {const pdfContent = await getBlobContent(data.pdf_path);const diData = JSON.parse((await getBlobContent(data.di_json_prefix)).toString());const pageRotations = diData.pages.map((p: any) => Math.round(p.angle));const extractor = new OpenAIExtractor({ endpoint: process.env.OPENAI_ENDPOINT!, apiKey: process.env.OPENAI_KEY! });data.ai_extracted_content = await extractor.extractFromBytes(pdfContent, pageRotations, data.pdf_path);return data;}export async function mapInvoice(data: ProcessData): Promise<ProcessData> {// Map AI extracted content to internal schemareturn data;}export async function transformInvoice(data: ProcessData): Promise<ProcessData> {// Combine DI and AI extracted contentreturn data;}
Invoice Service:
import { ProcessData } from "./types";import { generateImages, getBoundingRegions, extractInvoice, mapInvoice, transformInvoice } from "./activities";export async function processInvoice(data: ProcessData): Promise<ProcessData> {data = await generateImages(data);data = await getBoundingRegions(data);data = await extractInvoice(data);data = await mapInvoice(data);data = await transformInvoice(data);return