|
@@ -11,62 +11,6 @@ const corsHeaders = {
|
|
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface VAPIMessage {
|
|
|
|
|
- role: string
|
|
|
|
|
- message?: string
|
|
|
|
|
- content?: string
|
|
|
|
|
- time: number
|
|
|
|
|
- endTime?: number
|
|
|
|
|
- secondsFromStart: number
|
|
|
|
|
- duration?: number
|
|
|
|
|
- source?: string
|
|
|
|
|
- metadata?: any
|
|
|
|
|
- toolCalls?: any[]
|
|
|
|
|
- tool_calls?: any[]
|
|
|
|
|
- toolCallId?: string
|
|
|
|
|
- tool_call_id?: string
|
|
|
|
|
- name?: string
|
|
|
|
|
- result?: string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface VAPIAnalysis {
|
|
|
|
|
- summary?: string
|
|
|
|
|
- successEvaluation?: string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface VAPIArtifact {
|
|
|
|
|
- messages: VAPIMessage[]
|
|
|
|
|
- messagesOpenAIFormatted?: VAPIMessage[]
|
|
|
|
|
- transcript?: string
|
|
|
|
|
- recordingUrl?: string
|
|
|
|
|
- stereoRecordingUrl?: string
|
|
|
|
|
- pcapUrl?: string
|
|
|
|
|
- logUrl?: string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface VAPIWebhookPayload {
|
|
|
|
|
- message: {
|
|
|
|
|
- timestamp: number
|
|
|
|
|
- type: string
|
|
|
|
|
- analysis?: VAPIAnalysis
|
|
|
|
|
- artifact?: VAPIArtifact
|
|
|
|
|
- call?: {
|
|
|
|
|
- id?: string
|
|
|
|
|
- phoneNumberId?: string
|
|
|
|
|
- customerId?: string
|
|
|
|
|
- assistantId?: string
|
|
|
|
|
- cost?: number
|
|
|
|
|
- costBreakdown?: {
|
|
|
|
|
- stt?: number
|
|
|
|
|
- llm?: number
|
|
|
|
|
- tts?: number
|
|
|
|
|
- twilio?: number
|
|
|
|
|
- total?: number
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
serve(async (req) => {
|
|
serve(async (req) => {
|
|
|
// Handle CORS preflight
|
|
// Handle CORS preflight
|
|
|
if (req.method === 'OPTIONS') {
|
|
if (req.method === 'OPTIONS') {
|
|
@@ -142,10 +86,10 @@ serve(async (req) => {
|
|
|
console.log(`Processing VAPI webhook for webshop: ${store.store_name} (${webshopId})`)
|
|
console.log(`Processing VAPI webhook for webshop: ${store.store_name} (${webshopId})`)
|
|
|
|
|
|
|
|
// Parse the webhook payload
|
|
// Parse the webhook payload
|
|
|
- const payload: VAPIWebhookPayload = await req.json()
|
|
|
|
|
|
|
+ const payload = await req.json()
|
|
|
console.log('Received VAPI webhook:', JSON.stringify(payload, null, 2))
|
|
console.log('Received VAPI webhook:', JSON.stringify(payload, null, 2))
|
|
|
|
|
|
|
|
- // Validate payload structure
|
|
|
|
|
|
|
+ // Validate payload structure - only process end-of-call-report
|
|
|
if (!payload.message || payload.message.type !== 'end-of-call-report') {
|
|
if (!payload.message || payload.message.type !== 'end-of-call-report') {
|
|
|
console.log('Ignoring non-end-of-call-report webhook:', payload.message?.type)
|
|
console.log('Ignoring non-end-of-call-report webhook:', payload.message?.type)
|
|
|
return new Response(
|
|
return new Response(
|
|
@@ -154,91 +98,14 @@ serve(async (req) => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const { message } = payload
|
|
|
|
|
- const { timestamp, analysis, artifact, call } = message
|
|
|
|
|
-
|
|
|
|
|
- // Extract transcript from messages
|
|
|
|
|
- let transcript = artifact?.transcript || ''
|
|
|
|
|
- if (!transcript && artifact?.messages) {
|
|
|
|
|
- // Build transcript from messages
|
|
|
|
|
- const transcriptParts = artifact.messages
|
|
|
|
|
- .filter(msg => msg.role === 'user' || msg.role === 'bot' || msg.role === 'assistant')
|
|
|
|
|
- .map(msg => {
|
|
|
|
|
- const speaker = msg.role === 'user' ? 'Customer' : 'Assistant'
|
|
|
|
|
- const text = msg.message || msg.content || ''
|
|
|
|
|
- return `[${speaker}]: ${text}`
|
|
|
|
|
- })
|
|
|
|
|
- transcript = transcriptParts.join('\n\n')
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Calculate call duration from messages
|
|
|
|
|
- let durationSeconds = 0
|
|
|
|
|
- if (artifact?.messages && artifact.messages.length > 0) {
|
|
|
|
|
- const lastMessage = artifact.messages[artifact.messages.length - 1]
|
|
|
|
|
- durationSeconds = Math.round(lastMessage.secondsFromStart)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Determine call outcome based on analysis
|
|
|
|
|
- let callOutcome = 'pending'
|
|
|
|
|
- if (analysis?.successEvaluation === 'true' || analysis?.successEvaluation === true) {
|
|
|
|
|
- callOutcome = 'resolved'
|
|
|
|
|
- } else if (analysis?.successEvaluation === 'false' || analysis?.successEvaluation === false) {
|
|
|
|
|
- callOutcome = 'not_interested'
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Prepare call log data
|
|
|
|
|
|
|
+ // Prepare simplified call log data
|
|
|
const callLogData = {
|
|
const callLogData = {
|
|
|
- // Primary key - generate UUID for the call log
|
|
|
|
|
id: crypto.randomUUID(),
|
|
id: crypto.randomUUID(),
|
|
|
-
|
|
|
|
|
- // Associate with webshop
|
|
|
|
|
store_id: webshopId,
|
|
store_id: webshopId,
|
|
|
-
|
|
|
|
|
- // VAPI-specific fields
|
|
|
|
|
- vapi_call_id: call?.id || crypto.randomUUID(),
|
|
|
|
|
- vapi_timestamp: timestamp,
|
|
|
|
|
- messages: artifact?.messages || [],
|
|
|
|
|
- analysis: analysis || {},
|
|
|
|
|
-
|
|
|
|
|
- // Recording URLs
|
|
|
|
|
- recording_url: artifact?.recordingUrl,
|
|
|
|
|
- stereo_recording_url: artifact?.stereoRecordingUrl,
|
|
|
|
|
- pcap_url: artifact?.pcapUrl,
|
|
|
|
|
- log_url: artifact?.logUrl,
|
|
|
|
|
-
|
|
|
|
|
- // Call metadata
|
|
|
|
|
- assistant_id: call?.assistantId,
|
|
|
|
|
- phone_number_id: call?.phoneNumberId,
|
|
|
|
|
- customer_number: call?.customerId,
|
|
|
|
|
-
|
|
|
|
|
- // Call status and outcome
|
|
|
|
|
- status: 'completed',
|
|
|
|
|
- call_outcome: callOutcome,
|
|
|
|
|
- call_type: 'inbound', // VAPI calls are typically inbound
|
|
|
|
|
-
|
|
|
|
|
- // Transcript and summary
|
|
|
|
|
- transcript: transcript,
|
|
|
|
|
- summary: analysis?.summary || '',
|
|
|
|
|
-
|
|
|
|
|
- // Timing
|
|
|
|
|
- started_at: new Date(timestamp).toISOString(),
|
|
|
|
|
- ended_at: new Date(timestamp).toISOString(),
|
|
|
|
|
- duration_seconds: durationSeconds,
|
|
|
|
|
-
|
|
|
|
|
- // Costs
|
|
|
|
|
- cost_stt: call?.costBreakdown?.stt || 0,
|
|
|
|
|
- cost_llm: call?.costBreakdown?.llm || 0,
|
|
|
|
|
- cost_tts: call?.costBreakdown?.tts || 0,
|
|
|
|
|
- cost_twilio: call?.costBreakdown?.twilio || 0,
|
|
|
|
|
- cost_total: call?.costBreakdown?.total || call?.cost || 0,
|
|
|
|
|
- cost_totals: call?.costBreakdown?.total || call?.cost || 0,
|
|
|
|
|
-
|
|
|
|
|
- // Timestamps
|
|
|
|
|
- created_at: new Date().toISOString(),
|
|
|
|
|
- updated_at: new Date().toISOString(),
|
|
|
|
|
|
|
+ payload: payload,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- console.log('Inserting call log:', callLogData)
|
|
|
|
|
|
|
+ console.log('Inserting call log for store:', webshopId)
|
|
|
|
|
|
|
|
// Insert call log into database
|
|
// Insert call log into database
|
|
|
const { data, error } = await supabase
|
|
const { data, error } = await supabase
|