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

@@ -1,6 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';
import { generateObject } from 'ai';
import type { LanguageModel } from 'ai';
import { z } from 'zod';
import { createAiModel } from './ai-provider';
type LeadContext = {
firstName?: string;
@@ -19,23 +22,25 @@ type EnrichmentResult = {
aiSuggestedAction: string;
};
const enrichmentSchema = z.object({
aiSummary: z.string().describe('1-2 sentence summary of who this lead is and their history'),
aiSuggestedAction: z.string().describe('5-10 word suggested action for the agent'),
});
@Injectable()
export class AiEnrichmentService {
private readonly logger = new Logger(AiEnrichmentService.name);
private client: Anthropic | null = null;
private readonly aiModel: LanguageModel | null;
constructor(private config: ConfigService) {
const apiKey = config.get<string>('ai.anthropicApiKey');
if (apiKey) {
this.client = new Anthropic({ apiKey });
} else {
this.logger.warn('ANTHROPIC_API_KEY not set — AI enrichment disabled, using fallback');
this.aiModel = createAiModel(config);
if (!this.aiModel) {
this.logger.warn('AI not configured — enrichment uses fallback');
}
}
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
// Fallback if no API key configured
if (!this.client) {
if (!this.aiModel) {
return this.fallbackEnrichment(lead);
}
@@ -48,7 +53,10 @@ export class AiEnrichmentService {
? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n')
: 'No previous interactions';
const prompt = `You are an AI assistant for a hospital call center at Global Hospital.
const { object } = await generateObject({
model: this.aiModel!,
schema: enrichmentSchema,
prompt: `You are an AI assistant for a hospital call center.
An inbound call is coming in from a lead. Summarize their history and suggest what the call center agent should do.
Lead details:
@@ -60,25 +68,11 @@ Lead details:
- Contact attempts: ${lead.contactAttempts ?? 0}
Recent activity:
${activitiesText}
Respond ONLY with valid JSON (no markdown, no code blocks):
{"aiSummary": "1-2 sentence summary of who this lead is and their history", "aiSuggestedAction": "5-10 word suggested action for the agent"}`;
const response = await this.client.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 200,
messages: [{ role: 'user', content: prompt }],
${activitiesText}`,
});
const text = response.content[0].type === 'text' ? response.content[0].text : '';
const parsed = JSON.parse(text.trim());
this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`);
return {
aiSummary: parsed.aiSummary,
aiSuggestedAction: parsed.aiSuggestedAction,
};
return object;
} catch (error) {
this.logger.error(`AI enrichment failed: ${error}`);
return this.fallbackEnrichment(lead);