Skip to navigation

Always reference these two python files to fill in the blanks:

classes.py - Repos

AND

ingest_activities.py - Repos

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 pdf2pic
images.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 conversion
const 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 schema
return data;
}
export async function transformInvoice(data: ProcessData): Promise<ProcessData> {
// Combine DI and AI extracted content
return 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