Files
acmcc/supabase/functions/summarize-document/index.ts
2026-06-01 20:19:26 -04:00

180 lines
6.9 KiB
TypeScript

import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.99.1";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const { documentId, fileUrl, title } = await req.json();
if (!documentId || !fileUrl) {
return new Response(JSON.stringify({ error: "documentId and fileUrl are required" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
if (!LOVABLE_API_KEY) {
return new Response(JSON.stringify({ error: "LOVABLE_API_KEY not configured" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Fetch the file content
let fileContent = "";
try {
const fileResp = await fetch(fileUrl);
if (!fileResp.ok) throw new Error(`Failed to fetch file: ${fileResp.status}`);
const contentType = fileResp.headers.get("content-type") || "";
if (contentType.includes("application/pdf")) {
// For PDFs, extract text using base64 and Gemini's multimodal capability
const arrayBuffer = await fileResp.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
const aiResp = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${LOVABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content: `You are a document summarizer for a property management company. Generate a concise 2-4 sentence executive summary of the uploaded document. Focus on key findings, financial figures, action items, and important dates. Be professional and factual.`,
},
{
role: "user",
content: [
{
type: "text",
text: `Please summarize this management report document titled "${title || 'Untitled'}".`,
},
{
type: "image_url",
image_url: {
url: `data:application/pdf;base64,${base64}`,
},
},
],
},
],
}),
});
if (!aiResp.ok) {
if (aiResp.status === 429) {
return new Response(JSON.stringify({ error: "Rate limit exceeded, please try again later." }), {
status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (aiResp.status === 402) {
return new Response(JSON.stringify({ error: "AI credits exhausted. Please add funds." }), {
status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const errText = await aiResp.text();
console.error("AI error:", aiResp.status, errText);
throw new Error(`AI gateway error: ${aiResp.status}`);
}
const aiData = await aiResp.json();
fileContent = aiData.choices?.[0]?.message?.content || "";
} else {
// For text-based files, read as text
const textContent = await fileResp.text();
const truncated = textContent.substring(0, 15000);
const aiResp = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${LOVABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content: `You are a document summarizer for a property management company. Generate a concise 2-4 sentence executive summary. Focus on key findings, financial figures, action items, and important dates. Be professional and factual.`,
},
{
role: "user",
content: `Please summarize this management report titled "${title || 'Untitled'}":\n\n${truncated}`,
},
],
}),
});
if (!aiResp.ok) {
if (aiResp.status === 429) {
return new Response(JSON.stringify({ error: "Rate limit exceeded." }), {
status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (aiResp.status === 402) {
return new Response(JSON.stringify({ error: "AI credits exhausted." }), {
status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
throw new Error(`AI gateway error: ${aiResp.status}`);
}
const aiData = await aiResp.json();
fileContent = aiData.choices?.[0]?.message?.content || "";
}
} catch (fetchErr) {
console.error("File fetch/AI error:", fetchErr);
return new Response(JSON.stringify({ error: "Failed to process document for summary" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Save summary to database
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseKey);
const { error: updateError } = await supabase
.from("documents")
.update({ ai_summary: fileContent })
.eq("id", documentId);
if (updateError) {
console.error("DB update error:", updateError);
return new Response(JSON.stringify({ error: "Failed to save summary" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ summary: fileContent }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (e) {
console.error("summarize-document error:", e);
return new Response(JSON.stringify({ error: e instanceof Error ? e.message : "Unknown error" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});