feat: migrate AI to Vercel AI SDK, add OpenAI provider, fix worklist

- Replace raw @anthropic-ai/sdk with Vercel AI SDK (generateText, tool, generateObject)
- Add provider abstraction (ai-provider.ts) — swap OpenAI/Anthropic via env var
- AI chat controller: dynamic KB from platform (clinics, packages, insurance), zero hardcoding
- AI enrichment service: use generateObject with Zod schema instead of manual JSON parsing
- Worklist: resolve agent name from platform currentUser API instead of JWT decode
- Worklist: fix GraphQL field names to match platform remapping (source, status, direction, etc.)
- Config: add AI_PROVIDER, AI_MODEL, OPENAI_API_KEY env vars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 16:45:05 +05:30
parent 6f7d408724
commit 9688d5144e
9 changed files with 1150 additions and 409 deletions

View File

@@ -16,9 +16,9 @@ export class WorklistService {
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
const [missedCalls, followUps, marketingLeads] = await Promise.all([
this.getMissedCallsWithToken(agentName, authHeader),
this.getPendingFollowUpsWithToken(agentName, authHeader),
this.getAssignedLeadsWithToken(agentName, authHeader),
this.getMissedCalls(agentName, authHeader),
this.getPendingFollowUps(agentName, authHeader),
this.getAssignedLeads(agentName, authHeader),
]);
return {
@@ -29,108 +29,64 @@ export class WorklistService {
};
}
private async getAssignedLeadsWithToken(agentName: string, authHeader: string): Promise<any[]> {
private async getAssignedLeads(agentName: string, authHeader: string): Promise<any[]> {
try {
const data = await this.platform.queryWithAuth<{
leads: { edges: { node: any }[] };
}>(
`query GetAssignedLeads($filter: LeadFilterInput, $first: Int, $orderBy: [LeadOrderByInput]) {
leads(filter: $filter, first: $first, orderBy: $orderBy) {
edges {
node {
id createdAt
contactName { firstName lastName }
contactPhone { number callingCode }
contactEmail { address }
leadSource leadStatus interestedService
assignedAgent campaignId adId
contactAttempts spamScore isSpam
aiSummary aiSuggestedAction
}
}
}
}`,
{
filter: { assignedAgent: { eq: agentName } },
first: 20,
orderBy: [{ createdAt: 'AscNullsLast' }],
},
const data = await this.platform.queryWithAuth<any>(
`{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node {
id createdAt
contactName { firstName lastName }
contactPhone { primaryPhoneNumber }
contactEmail { primaryEmail }
source status interestedService
assignedAgent campaignId
contactAttempts spamScore isSpam
aiSummary aiSuggestedAction
} } } }`,
undefined,
authHeader,
);
return data.leads.edges.map((e) => e.node);
return data.leads.edges.map((e: any) => e.node);
} catch (err) {
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
return [];
}
}
private async getPendingFollowUpsWithToken(agentName: string, authHeader: string): Promise<any[]> {
private async getPendingFollowUps(agentName: string, authHeader: string): Promise<any[]> {
try {
const data = await this.platform.queryWithAuth<{
followUps: { edges: { node: any }[] };
}>(
`query GetPendingFollowUps($filter: FollowUpFilterInput, $first: Int) {
followUps(filter: $filter, first: $first) {
edges {
node {
id createdAt
followUpType followUpStatus
scheduledAt completedAt
priority assignedAgent
patientId callId
}
}
}
}`,
{
filter: {
assignedAgent: { eq: agentName },
followUpStatus: { in: ['PENDING', 'OVERDUE'] },
},
first: 20,
},
const data = await this.platform.queryWithAuth<any>(
`{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node {
id name createdAt
typeCustom status scheduledAt completedAt
priority assignedAgent
patientId callId
} } } }`,
undefined,
authHeader,
);
return data.followUps.edges.map((e) => e.node);
// Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields
return data.followUps.edges
.map((e: any) => e.node)
.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE');
} catch (err) {
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
return [];
}
}
private async getMissedCallsWithToken(agentName: string, authHeader: string): Promise<any[]> {
private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> {
try {
const data = await this.platform.queryWithAuth<{
calls: { edges: { node: any }[] };
}>(
`query GetMissedCalls($filter: CallFilterInput, $first: Int, $orderBy: [CallOrderByInput]) {
calls(filter: $filter, first: $first, orderBy: $orderBy) {
edges {
node {
id createdAt
callDirection callStatus
callerNumber { number callingCode }
agentName startedAt endedAt
durationSeconds disposition
callNotes leadId
}
}
}
}`,
{
filter: {
callStatus: { eq: 'MISSED' },
agentName: { eq: agentName },
},
first: 20,
orderBy: [{ createdAt: 'AscNullsLast' }],
},
const data = await this.platform.queryWithAuth<any>(
`{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: "MISSED" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
id name createdAt
direction callStatus callerNumber agentName
startedAt endedAt durationSec
disposition leadId
} } } }`,
undefined,
authHeader,
);
return data.calls.edges.map((e) => e.node);
return data.calls.edges.map((e: any) => e.node);
} catch (err) {
this.logger.warn(`Failed to fetch missed calls: ${err}`);
return [];