Skip to content

Commit

Permalink
feat: add chat
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminshafii committed Feb 15, 2025
1 parent f72f637 commit c768428
Show file tree
Hide file tree
Showing 10 changed files with 10,441 additions and 19,375 deletions.
6 changes: 3 additions & 3 deletions packages/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Toaster } from 'sonner';

import { ThemeProvider } from '@/components/theme-provider';
import { Providers } from './providers';
import '@/lib/ocr/pipeline';

import './globals.css';
// call pipeline

export const metadata: Metadata = {
metadataBase: new URL('https://chat.vercel.ai'),
Expand Down Expand Up @@ -70,9 +72,7 @@ export default async function RootLayout({
disableTransitionOnChange
>
<Toaster position="top-center" />
<Providers>
{children}
</Providers>
<Providers>{children}</Providers>
</ThemeProvider>
</body>
</html>
Expand Down
83 changes: 39 additions & 44 deletions packages/web/lib/db/queries/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '../index';
import { invoice, adminObligation } from '../schema';
import type { Invoice, AdminObligation } from '../schema';
import type { NewInvoice, NewAdminObligation } from '../schema';
import { and, eq, gte, desc } from 'drizzle-orm';
import { eq, and, gte, desc } from 'drizzle-orm';

export async function storeInvoices(data: Array<{
invoiceNumber: string;
Expand All @@ -13,18 +13,19 @@ export async function storeInvoices(data: Array<{
userId: string;
}>) {
try {
for (const inv of data) {
await db.insert(invoice).values({
id: crypto.randomUUID(),
invoiceNumber: inv.invoiceNumber,
vendor: inv.vendor,
amount: inv.amount,
invoiceDate: new Date(inv.invoiceDate),
dueDate: new Date(inv.dueDate),
source: `ocr_batch_${new Date().toISOString()}`,
userId: inv.userId,
});
}
const invoiceInputs: NewInvoice[] = data.map(inv => ({
id: crypto.randomUUID(),
invoiceNumber: inv.invoiceNumber,
vendor: inv.vendor,
amount: inv.amount,
invoiceDate: inv.invoiceDate,
dueDate: inv.dueDate,
userId: inv.userId,
createdAt: new Date(),
source: `ocr_batch_${new Date().toISOString()}`
}));

await db.insert(invoice).values(invoiceInputs);
console.log('Stored', data.length, 'invoice entries');
} catch (error) {
console.error('Failed to store invoices:', error);
Expand All @@ -39,16 +40,17 @@ export async function storeAdminObligations(data: Array<{
userId: string;
}>) {
try {
for (const admin of data) {
await db.insert(adminObligation).values({
id: crypto.randomUUID(),
obligation: admin.obligation,
dueDate: new Date(admin.dueDate),
notes: admin.notes || null,
source: `ocr_batch_${new Date().toISOString()}`,
userId: admin.userId,
});
}
const adminInputs: NewAdminObligation[] = data.map(admin => ({
id: crypto.randomUUID(),
obligation: admin.obligation,
dueDate: admin.dueDate,
notes: admin.notes || null,
userId: admin.userId,
createdAt: new Date(),
source: `ocr_batch_${new Date().toISOString()}`
}));

await db.insert(adminObligation).values(adminInputs);
console.log('Stored', data.length, 'admin obligation entries');
} catch (error) {
console.error('Failed to store admin obligations:', error);
Expand All @@ -73,10 +75,10 @@ export async function getRecentInvoices({
.where(
and(
eq(invoice.userId, userId),
gte(invoice.ocrTimestamp, startTime)
gte(invoice.createdAt, startTime)
)
)
.orderBy(desc(invoice.ocrTimestamp))
.orderBy(desc(invoice.createdAt))
.limit(limit);
} catch (error) {
console.error('Failed to get recent invoices:', error);
Expand All @@ -86,28 +88,21 @@ export async function getRecentInvoices({

export async function getRecentAdminObligations({
userId,
minutes = 15,
limit = 5,
minutes = 60 * 24, // Last 24 hours by default
limit = 100
}: {
userId: string;
minutes?: number;
limit?: number;
}) {
try {
const startTime = new Date(Date.now() - minutes * 60 * 1000);
return await db
.select()
.from(adminObligation)
.where(
and(
eq(adminObligation.userId, userId),
gte(adminObligation.ocrTimestamp, startTime)
)
)
.orderBy(desc(adminObligation.ocrTimestamp))
.limit(limit);
} catch (error) {
console.error('Failed to get recent admin obligations:', error);
throw error;
}
const startTime = new Date(Date.now() - minutes * 60 * 1000);

return await db.select()
.from(adminObligation)
.where(and(
eq(adminObligation.userId, userId),
gte(adminObligation.createdAt, startTime)
))
.limit(limit)
.orderBy(desc(adminObligation.createdAt));
}
47 changes: 34 additions & 13 deletions packages/web/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
primaryKey,
foreignKey,
boolean,
jsonb,
numeric,
date,
} from 'drizzle-orm/pg-core';

export const user = pgTable('User', {
Expand Down Expand Up @@ -113,34 +115,53 @@ export const suggestion = pgTable(
}),
);

export type Suggestion = InferSelectModel<typeof suggestion>;

export const structuredData = pgTable('structured_data', {
id: text('id').primaryKey(),
chatId: text('chat_id').notNull(),
type: text('type').notNull(), // e.g., 'payment', 'invoice', etc.
data: jsonb('data').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});

export const ocrData = pgTable('ocr_data', {
id: text('id').primaryKey(),
deviceId: text('device_id').notNull(),
text: text('text').notNull(),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at').defaultNow(),
});

export const invoice = pgTable('Invoice', {
id: uuid('id').primaryKey().notNull().defaultRandom(),
invoiceNumber: text('invoiceNumber').notNull(),
invoiceNumber: text('invoice_number').notNull(),
vendor: text('vendor').notNull(),
amount: text('amount').notNull(),
invoiceDate: timestamp('invoiceDate').notNull(),
dueDate: timestamp('dueDate').notNull(),
ocrTimestamp: timestamp('ocrTimestamp').notNull().defaultNow(),
source: text('source'),
userId: uuid('userId')
invoiceDate: timestamp('invoice_date').notNull(),
dueDate: timestamp('due_date').notNull(),
userId: uuid('user_id')
.notNull()
.references(() => user.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
source: text('source'),
});

export type Invoice = InferSelectModel<typeof invoice>;
export type NewInvoice = InferInsertModel<typeof invoice>;

export const adminObligation = pgTable('AdminObligation', {
id: uuid('id').primaryKey().notNull().defaultRandom(),
obligation: text('obligation').notNull(),
dueDate: timestamp('dueDate').notNull(),
dueDate: timestamp('due_date').notNull(),
notes: text('notes'),
ocrTimestamp: timestamp('ocrTimestamp').notNull().defaultNow(),
source: text('source'),
userId: uuid('userId')
userId: uuid('user_id')
.notNull()
.references(() => user.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
source: text('source'),
});

export type Invoice = InferSelectModel<typeof invoice>;
export type NewInvoice = InferInsertModel<typeof invoice>;
export type AdminObligation = InferSelectModel<typeof adminObligation>;
export type NewAdminObligation = InferInsertModel<typeof adminObligation>;
export type Suggestion = InferSelectModel<typeof suggestion>;
131 changes: 82 additions & 49 deletions packages/web/lib/ocr/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,120 @@
import { screenPipe } from '../screenpipe/client';
import { pipe } from '@screenpipe/js';
import { extractInvoicesAndAdmin } from '../ai/extractors';
import { auth } from '@/app/(auth)/auth';
import { storeInvoices, storeAdminObligations } from '../db/queries/invoices';
import { InvoicesAndAdminSchema } from '../schemas/invoicesAdminSchema';
import { InvoicesAndAdminSchema, type InvoicesAndAdmin } from '../schemas/invoicesAdminSchema';
import { type NewInvoice, type NewAdminObligation } from '../db/schema';
import { z } from 'zod';

type Session = {
user: {
id: string;
name?: string | null;
email?: string | null;
interface VisionEvent {
data: {
text: string;
app_name: string;
window_name: string;
timestamp: string;
};
};
}

import { type NewInvoice, type NewAdminObligation } from '../db/schema';
let isProcessing = false;
let textBuffer = '';
const PROCESS_INTERVAL = 60000; // Process every 60 seconds
let lastProcessTime = Date.now();

export async function processOCRForInvoicesAndAdmin() {
try {
// TODO: Remove test user ID once auth is set up
const userId = 'test-user-123';
export async function startOCRProcessing() {
if (isProcessing) {
console.log('OCR processing already running');
return;
}

// Query new OCR data from the last minute
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60000);
isProcessing = true;
console.log('Starting OCR processing stream...');

// TODO: Restore screenpipe query once service is available
// Hardcoded test data until screenpipe is available
const newOCRRecords = [{
text: `INVOICE
Number: INV-2024-001
Vendor: Tech Solutions Inc.
Amount: $1,500.00
Invoice Date: 2024-02-14
Due Date: 2024-03-14
try {
const stream = await pipe.streamVision(false);
for await (const event of stream) {
if (event.data.text) {
textBuffer += event.data.text + '\n';
console.log('New OCR text:', event.data.text);
console.log('From app:', event.data.app_name);
console.log('Window:', event.data.window_name);
}

REMINDER: Tax filing deadline on March 15, 2024
Note: Don't forget to include Q1 projections
const now = Date.now();
if (now - lastProcessTime >= PROCESS_INTERVAL && textBuffer.trim()) {
await processBuffer();
lastProcessTime = now;
}
}
} catch (err) {
console.error('Error in OCR stream:', {
error: err,
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined
});
} finally {
isProcessing = false;
}
}

Payment reminder: Insurance premium due on February 28, 2024
Notes: Coverage period March-May`
}];
async function processBuffer() {
if (!textBuffer.trim()) return;

const aggregatedText = (newOCRRecords as any[]).map(record => record.text).join('\n');
if (!aggregatedText.trim()) {
console.log('No new OCR text found');
try {
console.log('Processing OCR buffer...');
const result = await extractInvoicesAndAdmin(textBuffer);

if (!result) {
console.log('No data extracted from OCR text');
return;
}

// Extract structured invoice and admin obligations using AI
const extracted = await extractInvoicesAndAdmin(aggregatedText);
console.log('Extracted data:', extracted);
const parsed = InvoicesAndAdminSchema.safeParse(result);
if (!parsed.success) {
console.error('Invalid data format:', parsed.error);
return;
}

// Store extracted invoices if any
const data = extracted.object as z.infer<typeof InvoicesAndAdminSchema>;
const data: InvoicesAndAdmin = parsed.data;
const userId = 'test-user-123'; // TODO: Remove test user ID once auth is set up
const source = `ocr_stream_${new Date().toISOString()}`;

// Store invoices
if (data.invoices && data.invoices.length > 0) {
const invoiceInputs = data.invoices.map(inv => ({
const invoiceInputs: NewInvoice[] = data.invoices.map(inv => ({
id: crypto.randomUUID(),
invoiceNumber: inv.invoiceNumber,
vendor: inv.vendor,
amount: inv.amount.toString(),
invoiceDate: new Date(inv.invoiceDate),
dueDate: new Date(inv.dueDate),
userId
userId,
createdAt: new Date(),
source
}));
await storeInvoices(invoiceInputs);
console.log(`Stored ${invoiceInputs.length} invoices`);
}

// Store extracted admin obligations if any
// Store admin obligations
if (data.adminObligations && data.adminObligations.length > 0) {
const adminInputs = data.adminObligations.map(admin => ({
const adminInputs: NewAdminObligation[] = data.adminObligations.map(admin => ({
id: crypto.randomUUID(),
obligation: admin.obligation,
dueDate: new Date(admin.dueDate),
notes: admin.notes || null,
userId
userId,
createdAt: new Date(),
source
}));
await storeAdminObligations(adminInputs);
console.log(`Stored ${adminInputs.length} admin obligations`);
}
} catch (err) {
console.error('Error processing OCR for invoices and admin obligations:', err);
console.error('Error processing OCR buffer:', err);
} finally {
// Clear the buffer after processing
textBuffer = '';
}
}

// Run this process every 60 seconds
setInterval(() => {
processOCRForInvoicesAndAdmin();
}, 60000);
// Start the OCR processing when this module is imported
startOCRProcessing();
4 changes: 4 additions & 0 deletions packages/web/lib/schemas/invoicesAdminSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export const InvoicesAndAdminSchema = z.object({
invoices: z.array(InvoiceSchema),
adminObligations: z.array(AdminObligationSchema),
});

export type Invoice = z.infer<typeof InvoiceSchema>;
export type AdminObligation = z.infer<typeof AdminObligationSchema>;
export type InvoicesAndAdmin = z.infer<typeof InvoicesAndAdminSchema>;
Loading

0 comments on commit c768428

Please sign in to comment.