WFDASH-WEBAPP/scripts/smoke/one-shot-minimax.ts
SJCoetz 63048e51c9 feat: add AI quote processing pipeline and admin settings UI
Add complete end-to-end AI-powered quote extraction workflow:
- Disable PB SDK auto-cancellation to support parallel batch requests
- Add required runtime dependencies for AI processing and document parsing
- Implement AI settings backend, operator UI, and REST API routes
- Add quote ingestion, insight population, and manual re-run AI endpoints
- Build service layers for quote items, AI settings, and structured item insights
- Fix attachment upload handling and quote item history blob parsing
- Add supporting migrations, smoke tests, and shared utility functions
- Export previously private helper functions for cross-service reuse
2026-08-09 16:17:29 +02:00

94 lines
3.8 KiB
TypeScript

/**
* One-shot fake-email processor test.
*
* Runs the actual AI extraction against MiniMax using the real key
* the operator just configured in /settings/ai. Bypasses the Next.js
* HTTP layer (which is broken in dev due to a pdfjs-dist/webpack
* bundling issue — see https://github.com/mozilla/pdfjs-dist/issues/)
* so we can prove the wiring works end-to-end without touching the
* dev server. Same logic the route handler runs.
*/
import "./_env";
import { ingestQuoteFromEmail } from "../../src/lib/server/quotes-from-email-service";
import { processOneQuote } from "../../src/lib/server/quotes-ai-processor";
import { getAdminPocketBase } from "../../src/lib/server/pocketbase-admin";
import { getAiSettings } from "../../src/lib/server/ai-settings-service";
async function main() {
const settings = await getAiSettings({ includeSecret: true });
console.log("=== AI settings (with secret) ===");
console.log(` provider: ${settings.provider}`);
console.log(` model: ${settings.model_id}`);
console.log(` base_url: ${settings.base_url || "(SDK default)"}`);
console.log(` max_tokens: ${settings.max_tokens}`);
console.log(` enabled: ${settings.enabled}`);
console.log(` api_key_set: ${settings.api_key_set}\n`);
if (!settings.enabled) {
throw new Error("AI is not enabled in settings — flip the toggle first");
}
console.log("=== Ingesting smoke email ===");
const ingest = await ingestQuoteFromEmail({
from_email: "smoke-minimax-real@example.invalid",
from_name: "Smoke MiniMax Real",
subject: "[SMOKE] URGENT QUOTE — M16 fasteners, JHB",
body_plain:
"Please quote:\n\n" +
"- 10 x M16 x 50mm hex bolts grade 8.8 zinc plated\n" +
"- 20 x M16 wing nuts zinc plated\n" +
"- 4 x M16 flat washers zinc plated\n\n" +
"Need delivery to Johannesburg by Friday. Please confirm availability and lead time.\n\n" +
"Thanks,\nSmoke Tester",
message_id: `<smoke-minimax-real-direct-${Date.now()}@wfdash.local>`,
received_at: new Date().toISOString(),
company_hint: "Smoke MiniMax Direct Co",
});
console.log(` quote: ${ingest.quote.quote_number} (${ingest.quote.id})`);
console.log(` status: ai_status=${ingest.quote.ai_status}\n`);
console.log("=== Running processOneQuote() ===");
const t0 = Date.now();
const result = await processOneQuote({ quoteId: ingest.quote.id });
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(` outcome: ${result.outcome}`);
console.log(` itemsWritten: ${result.itemsWritten}`);
console.log(` error: ${result.error.slice(0, 200)}`);
console.log(` elapsed: ${elapsed}s\n`);
const pb = await getAdminPocketBase();
const after = await pb.collection("quotes").getOne(ingest.quote.id);
console.log("=== Quote row after processing ===");
console.log(` ai_status: ${after.ai_status}`);
console.log(` ai_error: ${(after.ai_error || "").slice(0, 200)}`);
console.log(` ai_processed_at: ${after.ai_processed_at}`);
if (result.outcome === "processed" && result.itemsWritten > 0) {
const items = await pb.collection("quote_items").getFullList({
filter: `quote = "${ingest.quote.id}"`,
});
console.log(`\n=== ${items.length} items written ===`);
for (const it of items) {
console.log(
` ${it.description} (qty=${it.quantity} ${it.unit || "ea"})`,
);
}
}
// Cleanup
console.log("\n=== Cleanup ===");
await pb.collection("quotes").delete(ingest.quote.id);
try { await pb.collection("customers").delete(ingest.customer.id); } catch { /* cascade */ }
if (ingest.company) {
try { await pb.collection("companies").delete(ingest.company.id); } catch { /* cascade */ }
}
console.log(" done");
console.log("\n=== DONE ===");
}
main().catch((e) => {
console.error("FAILED:", e.message);
console.error(e.stack);
process.exit(1);
});