Skip to content

Commit 23a691f

Browse files
authored
Merge pull request #16 from Gonna-AI/claude/peaceful-tereshkova
feat: Implement AI-powered caller name extraction via Groq tool calls…
2 parents 90a16a5 + 015a7a4 commit 23a691f

File tree

3 files changed

+95
-103
lines changed

3 files changed

+95
-103
lines changed

src/components/DemoCall/CallHistoryList.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ export default function CallHistoryList({ isDark = true, showFilters = true }: C
365365
)}
366366

367367
{/* AI Suggestions */}
368+
{item.summary.suggestions && item.summary.suggestions.length > 0 && (
368369
<div>
369370
<h4 className={cn("text-xs uppercase mb-2 font-semibold tracking-wider flex items-center gap-2", isDark ? "text-white/40" : "text-black/40")}>
370371
<TrendingUp className="w-3 h-3" /> AI Suggestions
@@ -374,22 +375,22 @@ export default function CallHistoryList({ isDark = true, showFilters = true }: C
374375
isDark ? "bg-black/20 border-white/10" : "bg-white border-black/5"
375376
)}>
376377
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/10 blur-[40px] rounded-full pointer-events-none" />
377-
<div className="relative z-10 space-y-4"> {/* Increased spacing */}
378-
<div className={cn("flex items-start gap-3 text-sm min-w-0", isDark ? "text-white/70" : "text-gray-600")}>
379-
<div className="w-6 h-6 rounded-lg bg-purple-500/10 flex items-center justify-center flex-shrink-0 text-purple-400">
380-
<Smile className="w-3 h-3" />
381-
</div>
382-
<p className="flex-1 break-words">Consider offering a follow-up consultation to address the client's specific concerns mentioned.</p>
383-
</div>
384-
<div className={cn("flex items-start gap-3 text-sm min-w-0", isDark ? "text-white/70" : "text-gray-600")}>
385-
<div className="w-6 h-6 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0 text-blue-400">
386-
<ListTodo className="w-3 h-3" />
378+
<div className="relative z-10 space-y-4">
379+
{item.summary.suggestions.map((suggestion, i) => (
380+
<div key={i} className={cn("flex items-start gap-3 text-sm min-w-0", isDark ? "text-white/70" : "text-gray-600")}>
381+
<div className={cn(
382+
"w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0",
383+
i % 2 === 0 ? "bg-purple-500/10 text-purple-400" : "bg-blue-500/10 text-blue-400"
384+
)}>
385+
{i % 2 === 0 ? <Smile className="w-3 h-3" /> : <ListTodo className="w-3 h-3" />}
386+
</div>
387+
<p className="flex-1 break-words">{suggestion}</p>
387388
</div>
388-
<p className="flex-1 break-words">Update the user's profile with the new contact preferences.</p>
389-
</div>
389+
))}
390390
</div>
391391
</div>
392392
</div>
393+
)}
393394
</div>
394395

395396
{/* Action Items - Moved to full width or separate grid if needed, keeping here for flow */}

src/contexts/DemoCallContext.tsx

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -853,73 +853,17 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
853853
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
854854
}, [currentCall?.id, currentCall?.status, cachedToken]);
855855

856-
// Helper to find caller name from various sources
856+
// Helper to find caller name — extracted fields only, no regex.
857+
// Actual name extraction from the transcript is handled by Groq tool call in summarizeCall.
857858
const findCallerName = (call: CallSession): string => {
858-
// First try extracted fields (most reliable)
859859
const nameField = call.extractedFields.find(f =>
860860
f.id === 'name' ||
861861
f.label.toLowerCase().includes('name') ||
862862
f.label.toLowerCase().includes('caller')
863863
);
864864
if (nameField?.value && nameField.value.trim() && nameField.value !== 'Unknown Caller') {
865-
console.log('✅ Found caller name from extracted fields:', nameField.value);
866865
return nameField.value.trim();
867866
}
868-
869-
// Common words to filter out as false positives
870-
const commonWords = ['hello', 'hi', 'hey', 'yes', 'no', 'okay', 'sure', 'thanks', 'thank', 'you', 'the', 'a', 'an', 'how', 'can', 'help', 'may', 'please', 'need', 'want', 'have', 'for', 'reaching', 'out', 'your', 'this', 'that', 'with', 'about', 'today', 'there', 'got', 'it'];
871-
872-
// Try to find name in messages where user introduced themselves
873-
const userMessages = call.messages.filter(m => m.speaker === 'user');
874-
for (const msg of userMessages) {
875-
// Common patterns: "My name is X", "I'm X", "This is X", "I am X", "Hi, I'm X", "Hello, this is X"
876-
const patterns = [
877-
/(?:my name is|i'?m|this is|i am|call me|hi,?\s*i'?m|hello,?\s*this is)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i,
878-
/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:here|calling|speaking)/i,
879-
/(?:hi|hello),?\s*([A-Z][a-z]+)/i,
880-
// Pattern for just stating name directly like "Animesh Mishra" or "Animesh Mishra 9650848339"
881-
/^([A-Z][a-z]+\s+[A-Z][a-z]+)(?:\s+\d+)?$/i,
882-
// Pattern for "im Animesh" or "its Animesh"
883-
/(?:im|its|it's)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i,
884-
];
885-
886-
for (const pattern of patterns) {
887-
const match = msg.text.match(pattern);
888-
if (match?.[1]) {
889-
const name = match[1].trim();
890-
// Filter out common false positives
891-
if (!commonWords.includes(name.toLowerCase()) && name.length > 2) {
892-
console.log('✅ Found caller name from message pattern:', name);
893-
return name;
894-
}
895-
}
896-
}
897-
}
898-
899-
// Try agent messages that might reference the caller's name
900-
const agentMessages = call.messages.filter(m => m.speaker === 'agent');
901-
for (const msg of agentMessages) {
902-
const patterns = [
903-
/(?:hi|hello),?\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i,
904-
/(?:thanks|thank you),?\s+([A-Z][a-z]+)/i,
905-
// Pattern for "Mr./Ms./Mrs. X" - common in formal responses
906-
/(?:Mr\.?|Ms\.?|Mrs\.?)\s+([A-Z][a-z]+)/i,
907-
// Pattern for "Hello, Mr. Mishra" style
908-
/(?:hello|hi),?\s+(?:Mr\.?|Ms\.?|Mrs\.?)\s+([A-Z][a-z]+)/i,
909-
];
910-
for (const pattern of patterns) {
911-
const match = msg.text.match(pattern);
912-
if (match?.[1]) {
913-
const name = match[1].trim();
914-
if (!commonWords.includes(name.toLowerCase()) && name.length > 2) {
915-
console.log('✅ Found caller name from agent message:', name);
916-
return name;
917-
}
918-
}
919-
}
920-
}
921-
922-
console.log('⚠️ Could not find caller name, using "Unknown Caller"');
923867
return 'Unknown Caller';
924868
};
925869

@@ -1056,7 +1000,8 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
10561000
finalSummary = summaryResponse.summary;
10571001
tags = summaryResponse.tags;
10581002

1059-
if (summaryResponse.callerName && callerName === 'Unknown Caller') {
1003+
// Always prefer the AI-extracted name (Groq tool call result)
1004+
if (summaryResponse.callerName && summaryResponse.callerName !== 'Unknown Caller') {
10601005
callerName = summaryResponse.callerName;
10611006
}
10621007
console.log('✅ AI summary generated successfully');
@@ -1070,10 +1015,6 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
10701015
};
10711016
}
10721017

1073-
if (callerName === 'Unknown Caller') {
1074-
callerName = findCallerName(endedCall);
1075-
}
1076-
10771018
// Create the final history item with real summary
10781019
const finalHistoryItem: CallHistoryItem = {
10791020
id: historyId,

src/services/localLLMService.ts

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,79 @@ class GroqLLMService {
902902
}
903903

904904

905+
/**
906+
* Extract caller name from transcript using a forced Groq tool call.
907+
* No regex, no fallback — if the model can't find a name, returns null.
908+
*/
909+
async extractCallerName(messages: CallMessage[]): Promise<string | null> {
910+
if (!this.groqAvailable) return null;
911+
912+
const settings = getCurrentGroqSettings();
913+
const transcript = messages
914+
.map(m => `${m.speaker === 'agent' ? 'Agent' : 'Caller'}: ${m.text}`)
915+
.join('\n');
916+
917+
const callerInfoTool = {
918+
type: 'function' as const,
919+
function: {
920+
name: 'extract_caller_info',
921+
description: 'Extract the caller\'s name from the conversation transcript.',
922+
parameters: {
923+
type: 'object',
924+
properties: {
925+
caller_name: {
926+
type: 'string',
927+
description:
928+
'Full name of the caller exactly as they stated it. Return null if the caller never explicitly introduced themselves by name.',
929+
},
930+
},
931+
required: ['caller_name'],
932+
},
933+
},
934+
};
935+
936+
try {
937+
const response = await proxyJSON<GroqResponse>(
938+
ProxyRoutes.COMPLETIONS,
939+
{
940+
model: settings.model,
941+
messages: [
942+
{
943+
role: 'system',
944+
content:
945+
'You extract the caller\'s name from a conversation transcript. ' +
946+
'Only return a name if the caller explicitly stated it (e.g. "my name is …", "I\'m …", "this is …"). ' +
947+
'Do NOT infer names from greetings like "good afternoon" or generic words. ' +
948+
'If the caller never introduced themselves, set caller_name to null.',
949+
},
950+
{
951+
role: 'user',
952+
content: `Extract the caller's name from this transcript:\n\n${transcript}`,
953+
},
954+
],
955+
tools: [callerInfoTool],
956+
tool_choice: { type: 'function', function: { name: 'extract_caller_info' } },
957+
temperature: 0,
958+
max_tokens: 64,
959+
},
960+
{ timeout: 10000 }
961+
);
962+
963+
const toolCall = response?.choices?.[0]?.message?.tool_calls?.[0];
964+
if (toolCall?.function?.name === 'extract_caller_info') {
965+
const args = JSON.parse(toolCall.function.arguments) as { caller_name?: string | null };
966+
const name = args.caller_name;
967+
if (name && name !== 'null' && name.toLowerCase() !== 'unknown' && name.trim().length > 1) {
968+
return name.trim();
969+
}
970+
}
971+
} catch (e) {
972+
log.warn('⚠️ extractCallerName tool call failed:', e);
973+
}
974+
975+
return null;
976+
}
977+
905978
/**
906979
* Generate comprehensive call summary using function calling approach
907980
* Based on Groq API function calling pattern for structured data extraction
@@ -957,34 +1030,11 @@ class GroqLLMService {
9571030
const categoryHint = category ? `\nCategory Hint: ${category.name} (${category.id})` : '';
9581031
const priorityHint = priority ? `\nPriority Hint: ${priority}` : '';
9591032

960-
// Try to find caller name from extracted fields first
961-
let callerName = extractedFields.find(f => f.id === 'name')?.value;
962-
963-
// If no name in fields, try to extract from messages
964-
if (!callerName) {
965-
for (const msg of messages) {
966-
if (msg.speaker === 'user') {
967-
// Look for name patterns - enhanced for all cultures
968-
const patterns = [
969-
/(?:my name is|i'?m|this is|i am)\s+([A-Za-z][A-Za-z]+(?:\s+[A-Za-z][A-Za-z]+)?)/i,
970-
/^([A-Za-z][A-Za-z]+(?:\s+[A-Za-z][A-Za-z]+)?)\s+(?:here|calling|speaking)/i,
971-
/(?:im|its|it's)\s+([A-Za-z][A-Za-z]+(?:\s+[A-Za-z][A-Za-z]+)?)/i,
972-
];
973-
for (const pattern of patterns) {
974-
const match = msg.text.match(pattern);
975-
if (match?.[1] && match[1].length > 2) {
976-
const name = match[1].trim();
977-
const commonWords = ['hello', 'hi', 'hey', 'yes', 'no', 'okay', 'sure', 'thanks', 'thank', 'you', 'the', 'help', 'need', 'want', 'thing', 'calling', 'good', 'fine', 'here'];
978-
if (!commonWords.includes(name.toLowerCase())) {
979-
callerName = name;
980-
break;
981-
}
982-
}
983-
}
984-
if (callerName) break;
985-
}
986-
}
987-
}
1033+
// Extract caller name: extracted fields first, then Groq tool call — no regex
1034+
const callerName =
1035+
extractedFields.find(f => f.id === 'name')?.value?.trim() ||
1036+
(await this.extractCallerName(messages)) ||
1037+
null;
9881038

9891039
// Enhanced prompt for detailed summary with comprehensive analysis
9901040
const prompt = `Analyze this call transcript thoroughly and provide a comprehensive summary.

0 commit comments

Comments
 (0)