mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Implement AI-powered negotiation strategy and letter generation for debt negotiation
This commit is contained in:
@@ -25,6 +25,56 @@ const debtSchema = z.object({
|
|||||||
.describe("Whether the debt information was successfully parsed"),
|
.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
|
// Function to parse debt information using AI
|
||||||
async function parseDebtWithAI(emailText: string, fromEmail: string) {
|
async function parseDebtWithAI(emailText: string, fromEmail: string) {
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +165,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for opt-out keywords
|
// Check for opt-out keywords
|
||||||
const optOutKeywords = ["STOP", "UNSUBSCRIBE", "OPT-OUT", "REMOVE"];
|
|
||||||
const textBody = data.TextBody || data.HtmlBody || "";
|
const textBody = data.TextBody || data.HtmlBody || "";
|
||||||
const fromEmail = data.FromFull?.Email || data.From || "unknown";
|
const fromEmail = data.FromFull?.Email || data.From || "unknown";
|
||||||
const toEmail = data.ToFull?.[0]?.Email || data.To || "";
|
const toEmail = data.ToFull?.[0]?.Email || data.To || "";
|
||||||
@@ -130,9 +179,21 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
// Increment email processing usage
|
// Increment email processing usage
|
||||||
await incrementEmailUsage(userId, supabaseAdmin);
|
await incrementEmailUsage(userId, supabaseAdmin);
|
||||||
|
|
||||||
const hasOptOut = optOutKeywords.some((keyword) =>
|
// Check for opt-out using AI
|
||||||
textBody.toUpperCase().includes(keyword)
|
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) {
|
if (hasOptOut) {
|
||||||
// Log opt-out and don't process further
|
// Log opt-out and don't process further
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
# Debt Negotiation AI Edge Function
|
# Debt Negotiation AI Edge Function
|
||||||
|
|
||||||
This function generates FDCPA-compliant negotiation responses based on debt amounts:
|
This function generates FDCPA-compliant negotiation responses using AI analysis:
|
||||||
- < $500: 30-day extension request
|
- Analyzes debt details and vendor information
|
||||||
- $500-$2000: 3-month installment plan
|
- Generates personalized negotiation strategies
|
||||||
- >= $2000: 60% lump-sum settlement offer
|
- Creates contextually appropriate response letters
|
||||||
|
- Ensures FDCPA compliance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createClient } from 'npm:@supabase/supabase-js@2';
|
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 = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -15,11 +19,172 @@ const corsHeaders = {
|
|||||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
'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 {
|
interface DebtRecord {
|
||||||
id: string;
|
id: string;
|
||||||
vendor: string;
|
vendor: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
raw_email: string;
|
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) => {
|
Deno.serve(async (req) => {
|
||||||
@@ -34,31 +199,31 @@ Deno.serve(async (req) => {
|
|||||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
|
|
||||||
// Generate negotiation strategy based on amount
|
// Generate AI-powered negotiation strategy
|
||||||
let strategy = '';
|
const strategy = await generateNegotiationStrategy(record);
|
||||||
let projectedSavings = 0;
|
|
||||||
|
// Generate AI-powered negotiation letter
|
||||||
|
const letterResult = await generateNegotiationLetter(record, strategy);
|
||||||
|
|
||||||
if (record.amount < 500) {
|
// Update debt record with AI-generated content
|
||||||
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
|
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('debts')
|
.from('debts')
|
||||||
.update({
|
.update({
|
||||||
negotiated_plan: negotiatedPlan,
|
negotiated_plan: letterResult.letter,
|
||||||
projected_savings: projectedSavings,
|
projected_savings: strategy.projectedSavings,
|
||||||
status: 'negotiating'
|
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);
|
.eq('id', record.id);
|
||||||
|
|
||||||
@@ -73,14 +238,22 @@ Deno.serve(async (req) => {
|
|||||||
debt_id: record.id,
|
debt_id: record.id,
|
||||||
action: 'negotiation_generated',
|
action: 'negotiation_generated',
|
||||||
details: {
|
details: {
|
||||||
strategy,
|
strategy: strategy.strategy,
|
||||||
amount: record.amount,
|
amount: record.amount,
|
||||||
projected_savings: projectedSavings
|
projected_savings: strategy.projectedSavings,
|
||||||
|
ai_confidence: strategy.confidenceLevel,
|
||||||
|
reasoning: strategy.reasoning
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response(
|
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' } }
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,7 +270,7 @@ Deno.serve(async (req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function generateNegotiationResponse(record: DebtRecord, strategy: string): string {
|
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 companyName = vendorDomain.split('.')[0].toUpperCase();
|
||||||
|
|
||||||
const baseResponse = `Dear ${companyName} Collections Department,
|
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.`;
|
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;
|
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 = `
|
const closingResponse = `
|
||||||
|
|||||||
Reference in New Issue
Block a user