From aa287e424d94d972c44ae9f7ed0e016322d089a5 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Sat, 7 Jun 2025 12:27:38 -0300 Subject: [PATCH] Implement AI-powered negotiation strategy and letter generation for debt negotiation --- src/pages/api/postmark.ts | 69 +++++++- supabase/functions/negotiate/index.ts | 243 +++++++++++++++++++++++--- 2 files changed, 279 insertions(+), 33 deletions(-) diff --git a/src/pages/api/postmark.ts b/src/pages/api/postmark.ts index 416d9d7..a99459c 100644 --- a/src/pages/api/postmark.ts +++ b/src/pages/api/postmark.ts @@ -25,6 +25,56 @@ const debtSchema = z.object({ .describe("Whether the debt information was successfully parsed"), }); +// Schema for opt-out detection +const optOutSchema = z.object({ + isOptOut: z.boolean().describe("Whether this email contains an opt-out request"), + confidence: z + .number() + .min(0) + .max(1) + .describe("Confidence level of the opt-out detection"), + reason: z + .string() + .describe("Explanation of why this was classified as opt-out or not"), +}); + +// Function to detect opt-out requests using AI +async function detectOptOutWithAI(emailText: string, fromEmail: string) { + try { + const googleApiKey = + process.env.GOOGLE_GENERATIVE_AI_API_KEY || + import.meta.env.GOOGLE_GENERATIVE_AI_API_KEY; + if (!googleApiKey) { + console.warn("Google API key not configured, falling back to keyword detection"); + return null; + } + + const result = await generateObject({ + model: createGoogleGenerativeAI({ + apiKey: googleApiKey, + })("gemini-2.5-flash-preview-04-17"), + system: `You are an expert at analyzing email content to detect opt-out requests. + Analyze the email to determine if the sender is requesting to opt-out, unsubscribe, + or stop receiving communications. Consider: + - Explicit opt-out keywords (STOP, UNSUBSCRIBE, REMOVE, etc.) + - Implicit requests to stop communication + - Context and tone indicating unwillingness to continue correspondence + - Legal language requesting cessation of contact + Be conservative - only flag as opt-out if you're confident it's a genuine request.`, + prompt: `Analyze this email for opt-out intent: + + From: ${fromEmail} + Content: ${emailText}`, + schema: optOutSchema, + }); + + return result.object; + } catch (error) { + console.error("AI opt-out detection error:", error); + return null; + } +} + // Function to parse debt information using AI async function parseDebtWithAI(emailText: string, fromEmail: string) { try { @@ -115,7 +165,6 @@ export const POST: APIRoute = async ({ request }) => { } // Check for opt-out keywords - const optOutKeywords = ["STOP", "UNSUBSCRIBE", "OPT-OUT", "REMOVE"]; const textBody = data.TextBody || data.HtmlBody || ""; const fromEmail = data.FromFull?.Email || data.From || "unknown"; const toEmail = data.ToFull?.[0]?.Email || data.To || ""; @@ -130,9 +179,21 @@ export const POST: APIRoute = async ({ request }) => { // Increment email processing usage await incrementEmailUsage(userId, supabaseAdmin); - const hasOptOut = optOutKeywords.some((keyword) => - textBody.toUpperCase().includes(keyword) - ); + // Check for opt-out using AI + const optOutDetection = await detectOptOutWithAI(textBody, fromEmail); + let hasOptOut = false; + + if (optOutDetection) { + hasOptOut = optOutDetection.isOptOut && optOutDetection.confidence > 0.7; + console.log(`AI opt-out detection: ${hasOptOut} (confidence: ${optOutDetection.confidence})`); + } else { + // Fallback to keyword matching if AI is unavailable + const optOutKeywords = ["STOP", "UNSUBSCRIBE", "OPT-OUT", "REMOVE"]; + hasOptOut = optOutKeywords.some((keyword) => + textBody.toUpperCase().includes(keyword) + ); + console.log("Using fallback keyword opt-out detection"); + } if (hasOptOut) { // Log opt-out and don't process further diff --git a/supabase/functions/negotiate/index.ts b/supabase/functions/negotiate/index.ts index 0ab2cca..b6b7604 100644 --- a/supabase/functions/negotiate/index.ts +++ b/supabase/functions/negotiate/index.ts @@ -1,13 +1,17 @@ /* # Debt Negotiation AI Edge Function - This function generates FDCPA-compliant negotiation responses based on debt amounts: - - < $500: 30-day extension request - - $500-$2000: 3-month installment plan - - >= $2000: 60% lump-sum settlement offer + This function generates FDCPA-compliant negotiation responses using AI analysis: + - Analyzes debt details and vendor information + - Generates personalized negotiation strategies + - Creates contextually appropriate response letters + - Ensures FDCPA compliance */ import { createClient } from 'npm:@supabase/supabase-js@2'; +import { generateObject } from 'npm:ai@4.3.16'; +import { createGoogleGenerativeAI } from 'npm:@ai-sdk/google@1.2.19'; +import { z } from 'npm:zod@3.23.8'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -15,11 +19,172 @@ const corsHeaders = { 'Access-Control-Allow-Methods': 'POST, OPTIONS', }; +// Schema for AI negotiation strategy +const negotiationStrategySchema = z.object({ + strategy: z.enum(['extension', 'installment', 'settlement', 'dispute']).describe('The recommended negotiation strategy'), + confidenceLevel: z.number().min(0).max(1).describe('Confidence in the strategy recommendation'), + projectedSavings: z.number().min(0).describe('Estimated savings from this strategy'), + reasoning: z.string().describe('Explanation of why this strategy was chosen'), + customTerms: z.object({ + extensionDays: z.number().optional().describe('Days for extension if applicable'), + installmentMonths: z.number().optional().describe('Number of months for installment plan'), + settlementPercentage: z.number().optional().describe('Settlement percentage (0-1) if applicable'), + monthlyPayment: z.number().optional().describe('Monthly payment amount for installments'), + }).describe('Custom terms based on the strategy'), +}); + +// Schema for AI-generated negotiation letter +const negotiationLetterSchema = z.object({ + letter: z.string().describe('The complete negotiation letter text'), + tone: z.enum(['formal', 'respectful', 'assertive', 'conciliatory']).describe('The tone used in the letter'), + keyPoints: z.array(z.string()).describe('Key negotiation points included in the letter'), + fdcpaCompliant: z.boolean().describe('Whether the letter meets FDCPA compliance requirements'), +}); + interface DebtRecord { id: string; vendor: string; amount: number; raw_email: string; + description?: string; + due_date?: string; + metadata?: { + isDebtCollection?: boolean; + subject?: string; + fromEmail?: string; + }; +} + +// AI-powered negotiation strategy generator +async function generateNegotiationStrategy(record: DebtRecord) { + try { + const googleApiKey = Deno.env.get('GOOGLE_GENERATIVE_AI_API_KEY'); + if (!googleApiKey) { + console.warn('Google API key not configured, falling back to rule-based strategy'); + return generateFallbackStrategy(record); + } + + const result = await generateObject({ + model: createGoogleGenerativeAI({ + apiKey: googleApiKey, + })('gemini-2.5-flash-preview-04-17'), + system: `You are an expert debt negotiation advisor specializing in FDCPA-compliant strategies. + Analyze the debt details and recommend the best negotiation approach. Consider: + - Debt amount and type + - Vendor/creditor information + - Legal compliance requirements + - Realistic settlement possibilities + - Consumer protection laws + + Strategy guidelines: + - Extension: For temporary hardship, usually < $500 + - Installment: For manageable monthly payments, $500-$2000 + - Settlement: For significant savings, typically $2000+ + - Dispute: If debt validity is questionable + + Always ensure FDCPA compliance and realistic expectations.`, + prompt: `Analyze this debt for negotiation strategy: + + Amount: $${record.amount} + Vendor: ${record.vendor} + Description: ${record.description || 'Not specified'} + Due Date: ${record.due_date || 'Not specified'} + Email Content Preview: ${record.raw_email.substring(0, 500)}... + + Recommend the best negotiation strategy with specific terms.`, + schema: negotiationStrategySchema, + }); + + return result.object; + } catch (error) { + console.error('AI strategy generation error:', error); + return generateFallbackStrategy(record); + } +} + +// Fallback rule-based strategy when AI is unavailable +function generateFallbackStrategy(record: DebtRecord) { + let strategy: 'extension' | 'installment' | 'settlement' | 'dispute' = 'extension'; + let projectedSavings = 0; + let customTerms = {}; + + if (record.amount < 500) { + strategy = 'extension'; + projectedSavings = 0; + customTerms = { extensionDays: 30 }; + } else if (record.amount >= 500 && record.amount < 2000) { + strategy = 'installment'; + projectedSavings = record.amount * 0.1; + customTerms = { + installmentMonths: 3, + monthlyPayment: Math.round(record.amount / 3 * 100) / 100 + }; + } else { + strategy = 'settlement'; + projectedSavings = record.amount * 0.4; + customTerms = { settlementPercentage: 0.6 }; + } + + return { + strategy, + confidenceLevel: 0.7, + projectedSavings, + reasoning: 'Generated using rule-based fallback logic', + customTerms, + }; +} + +// AI-powered negotiation letter generator +async function generateNegotiationLetter(record: DebtRecord, strategy: any) { + try { + const googleApiKey = Deno.env.get('GOOGLE_GENERATIVE_AI_API_KEY'); + if (!googleApiKey) { + console.warn('Google API key not configured, falling back to template'); + return generateFallbackLetter(record, strategy); + } + + const result = await generateObject({ + model: createGoogleGenerativeAI({ + apiKey: googleApiKey, + })('gemini-2.5-flash-preview-04-17'), + system: `You are an expert at writing FDCPA-compliant debt negotiation letters. + Create professional, respectful letters that: + - Follow Fair Debt Collection Practices Act requirements + - Are appropriately formal but human + - Include specific negotiation terms + - Maintain consumer rights protections + - Are personalized to the specific situation + + Always include FDCPA compliance language and validation requests. + Keep tone professional but not overly legal or intimidating.`, + prompt: `Generate a negotiation letter for this debt: + + Debt Amount: $${record.amount} + Vendor: ${record.vendor} + Strategy: ${strategy.strategy} + Custom Terms: ${JSON.stringify(strategy.customTerms)} + Reasoning: ${strategy.reasoning} + + Create a complete, ready-to-send negotiation letter.`, + schema: negotiationLetterSchema, + }); + + return result.object; + } catch (error) { + console.error('AI letter generation error:', error); + return generateFallbackLetter(record, strategy); + } +} + +// Fallback letter generation +function generateFallbackLetter(record: DebtRecord, strategy: any) { + const letter = generateNegotiationResponse(record, strategy.strategy); + return { + letter, + tone: 'formal' as const, + keyPoints: ['Payment arrangement', 'FDCPA compliance', 'Good faith negotiation'], + fdcpaCompliant: true, + }; } Deno.serve(async (req) => { @@ -34,31 +199,31 @@ Deno.serve(async (req) => { const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseKey); - // Generate negotiation strategy based on amount - let strategy = ''; - let projectedSavings = 0; + // Generate AI-powered negotiation strategy + const strategy = await generateNegotiationStrategy(record); + + // Generate AI-powered negotiation letter + const letterResult = await generateNegotiationLetter(record, strategy); - if (record.amount < 500) { - strategy = 'extension'; - projectedSavings = 0; // No savings, just time - } else if (record.amount >= 500 && record.amount < 2000) { - strategy = 'installment'; - projectedSavings = record.amount * 0.1; // 10% savings from avoiding late fees - } else { - strategy = 'settlement'; - projectedSavings = record.amount * 0.4; // 40% savings from 60% settlement - } - - // Generate FDCPA-compliant response - const negotiatedPlan = generateNegotiationResponse(record, strategy); - - // Update debt record + // Update debt record with AI-generated content const { error: updateError } = await supabase .from('debts') .update({ - negotiated_plan: negotiatedPlan, - projected_savings: projectedSavings, - status: 'negotiating' + negotiated_plan: letterResult.letter, + projected_savings: strategy.projectedSavings, + status: 'negotiating', + metadata: { + ...record.metadata, + aiStrategy: { + strategy: strategy.strategy, + confidence: strategy.confidenceLevel, + reasoning: strategy.reasoning, + customTerms: strategy.customTerms, + letterTone: letterResult.tone, + keyPoints: letterResult.keyPoints, + fdcpaCompliant: letterResult.fdcpaCompliant, + } + } }) .eq('id', record.id); @@ -73,14 +238,22 @@ Deno.serve(async (req) => { debt_id: record.id, action: 'negotiation_generated', details: { - strategy, + strategy: strategy.strategy, amount: record.amount, - projected_savings: projectedSavings + projected_savings: strategy.projectedSavings, + ai_confidence: strategy.confidenceLevel, + reasoning: strategy.reasoning } }); return new Response( - JSON.stringify({ success: true, strategy, projected_savings: projectedSavings }), + JSON.stringify({ + success: true, + strategy: strategy.strategy, + projected_savings: strategy.projectedSavings, + confidence: strategy.confidenceLevel, + reasoning: strategy.reasoning + }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); @@ -97,7 +270,7 @@ Deno.serve(async (req) => { }); function generateNegotiationResponse(record: DebtRecord, strategy: string): string { - const vendorDomain = record.vendor.split('@')[1] || 'your company'; + const vendorDomain = record.vendor.includes('@') ? record.vendor.split('@')[1] : record.vendor; const companyName = vendorDomain.split('.')[0].toUpperCase(); const baseResponse = `Dear ${companyName} Collections Department, @@ -132,6 +305,18 @@ During this extension period, I request that no additional fees or interest be a This settlement would be paid within 10 business days of written acceptance of this offer. Upon payment, I request written confirmation that this account will be considered paid in full and closed.`; break; + + case 'dispute': + proposal = `I am formally disputing this debt and requesting validation under Section 809(b) of the Fair Debt Collection Practices Act. + +Please provide: +- Verification of the debt amount +- Name and address of the original creditor +- Copy of any judgment (if applicable) +- Verification of your authority to collect this debt + +Until proper validation is provided, I request that all collection activities cease.`; + break; } const closingResponse = `