@accounter/server
Version:
117 lines (105 loc) • 3.67 kB
text/typescript
import { generateObject } from 'ai';
import { Injectable, Scope } from 'graphql-modules';
import stripIndent from 'strip-indent';
import { z } from 'zod';
import { anthropic } from '@ai-sdk/anthropic';
import { Currency, DocumentType } from '@shared/enums';
const documentDataSchema = z.object({
type: z.nativeEnum(DocumentType).nullable().describe('The type of financial document'),
issuer: z.string().nullable().describe('Legal name of the organization that issued the document'),
recipient: z
.string()
.nullable()
.describe('Legal name and details of the entity to whom the document is addressed'),
fullAmount: z
.number()
.nullable()
.describe('Total monetary amount including taxes and all charges'),
currency: z.nativeEnum(Currency).nullable().describe('ISO 4217 currency code'),
vatAmount: z
.number()
.nullable()
.describe('Value Added Tax amount if separately specified on the document'),
date: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.nullable()
.describe('Document issue date in ISO 8601 format (YYYY-MM-DD)'),
referenceCode: z
.string()
.nullable()
.describe('Complete document identifier including any separators (e.g., dashes, slashes)'),
});
type DocumentData = z.infer<typeof documentDataSchema>;
const SUPPORTED_FILE_TYPES = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
] as const;
type SupportedFileType = (typeof SUPPORTED_FILE_TYPES)[number];
function isSupportedFileType(value: string): value is SupportedFileType {
return SUPPORTED_FILE_TYPES.includes(value as SupportedFileType);
}
@Injectable({
scope: Scope.Singleton,
global: true,
})
export class AnthropicProvider {
/**
* Convert File or Blob to Base64
* @param fileOrBlob File or Blob to convert
* @returns Base64 encoded string
*/
private async fileToBase64(fileOrBlob: File | Blob): Promise<string> {
const buffer = await fileOrBlob.arrayBuffer();
const base64string = Buffer.from(buffer).toString('base64');
return base64string;
}
/**
* Extract invoice details using Anthropic API
* @param fileOrBlob File or Blob to process
* @returns Parsed invoice data
*/
async extractInvoiceDetails(fileOrBlob: File | Blob): Promise<DocumentData> {
try {
const fileType = fileOrBlob.type.toLowerCase();
if (!isSupportedFileType(fileType)) {
throw new Error('Unsupported file type. Please provide an image or PDF.');
}
const fileData = await this.fileToBase64(fileOrBlob);
const { object, usage } = await generateObject({
model: anthropic('claude-3-5-sonnet-20241022'),
schema: documentDataSchema,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: stripIndent(`Please analyze the provided document and extract:
- Document type
- Issuer and recipient details
- Monetary amounts (total and VAT)
- Date and reference numbers
Return only a JSON object without any explanation. Use NULL for missing values.`),
},
{
type: 'file',
data: fileData,
mimeType: fileType,
},
],
},
],
});
// Shows token usage to calculate the cost of the request
console.log('Usage:', usage);
return object;
} catch (error) {
console.error('Error in document extraction:', error);
throw error;
}
}
}