import { execSync } from 'child_process';
class AIService {
constructor(config = {}) {
this.projectId = config.projectId || process.env.GCP_PROJECT_ID || 'amgn-app';
this.location = config.location || process.env.GCP_LOCATION || 'global';
this.modelId = config.modelId || process.env.ANTHROPIC_MODEL_ID || 'claude-opus-4-5';
// Note: Claude Opus 4.5 uses 'global' location, not regional endpoints
this.endpoint = 'aiplatform.googleapis.com';
this.isVercel = process.env.VERCEL === '1' || process.env.VERCEL_ENV;
this.useApiKey = !!process.env.VERTEX_AI_API_KEY;
// Only verify gcloud if we're not on Vercel and not using API key
if (!this.isVercel && !this.useApiKey) {
try {
execSync('gcloud --version', { stdio: 'ignore' });
} catch (error) {
console.warn('⚠️ gcloud CLI not found. Set VERTEX_AI_API_KEY for cloud deployments.');
// Don't throw - allow service to be created, but getAccessToken will fail
}
}
}
// Helper function to extract JSON from text that might contain markdown code blocks
extractJSON(text) {
if (!text || typeof text !== 'string') {
return null;
}
// Try to find JSON inside markdown code blocks
// Match ```json ... ``` or ``` ... ```
const codeBlockRegex = /```(?:json)?\s*\n?([\s\S]*?)\n?```/g;
const codeBlockMatch = codeBlockRegex.exec(text);
if (codeBlockMatch && codeBlockMatch[1]) {
const jsonText = codeBlockMatch[1].trim();
// Try to find JSON object/array in the extracted text
const jsonMatch = jsonText.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (jsonMatch) {
return jsonMatch[0];
}
return jsonText;
}
// Try to find JSON object/array directly in the text
const jsonMatch = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (jsonMatch) {
return jsonMatch[0];
}
// Return original text if no JSON found (will fail in JSON.parse, but at least we tried)
return text.trim();
}
async getAccessToken() {
// Method 1: Service Account JSON from environment variable (for Vercel/cloud)
if (process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON) {
try {
const serviceAccount = JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON);
const token = await this.getServiceAccountToken(serviceAccount);
return token;
} catch (error) {
console.error('Error parsing service account JSON:', error.message);
// Fall through to next method
}
}
// Method 2: API key (for Vertex AI Express Mode or if already a token)
if (process.env.VERTEX_AI_API_KEY) {
// Check if it looks like a Bearer token (starts with eyJ for JWT or is a long string)
if (process.env.VERTEX_AI_API_KEY.length > 100 && process.env.VERTEX_AI_API_KEY.startsWith('eyJ')) {
// It's a JWT token, use as-is
return process.env.VERTEX_AI_API_KEY;
}
// Otherwise treat as API key (might need different handling)
return process.env.VERTEX_AI_API_KEY;
}
// Method 3: Service account file path (local development)
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
try {
// Try to read the file and use it
const fs = await import('fs');
const path = await import('path');
const credPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
const serviceAccount = JSON.parse(fs.readFileSync(credPath, 'utf8'));
const token = await this.getServiceAccountToken(serviceAccount);
return token;
} catch (error) {
// Fall through to gcloud CLI
}
}
// Method 4: gcloud CLI (local development)
try {
const token = execSync('gcloud auth print-access-token', { encoding: 'utf8' }).trim();
return token;
} catch (error) {
throw new Error(
'Failed to get access token. Set one of:\n' +
'1. GOOGLE_APPLICATION_CREDENTIALS_JSON (service account JSON as string)\n' +
'2. VERTEX_AI_API_KEY (OAuth2 access token)\n' +
'3. GOOGLE_APPLICATION_CREDENTIALS (path to service account JSON file)\n' +
'4. Run: gcloud auth login'
);
}
}
async getServiceAccountToken(serviceAccount) {
// Generate JWT and exchange for access token
const jwt = await this.createJWT(serviceAccount);
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt
})
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
throw new Error(`Failed to get access token: ${tokenResponse.status} - ${errorText}`);
}
const tokenData = await tokenResponse.json();
return tokenData.access_token;
}
async createJWT(serviceAccount) {
const crypto = await import('crypto');
const now = Math.floor(Date.now() / 1000);
const header = {
alg: 'RS256',
typ: 'JWT'
};
const claim = {
iss: serviceAccount.client_email,
scope: 'https://www.googleapis.com/auth/cloud-platform',
aud: 'https://oauth2.googleapis.com/token',
exp: now + 3600,
iat: now
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
const encodedClaim = Buffer.from(JSON.stringify(claim)).toString('base64url');
const signatureInput = `${encodedHeader}.${encodedClaim}`;
const signature = crypto.createSign('RSA-SHA256')
.update(signatureInput)
.sign(serviceAccount.private_key, 'base64url');
return `${signatureInput}.${signature}`;
}
async callVertexAI(messages, maxTokens = 1024, temperature = 1.0, stream = false, retries = 3) {
const accessToken = await this.getAccessToken();
// For 'global' location, use aiplatform.googleapis.com directly
// For regional locations, use region-specific endpoint (e.g., us-central1-aiplatform.googleapis.com)
const endpointUrl = this.location === 'global'
? this.endpoint
: `${this.location}-${this.endpoint}`;
const url = `https://${endpointUrl}/v1/projects/${this.projectId}/locations/${this.location}/publishers/anthropic/models/${this.modelId}:${stream ? 'streamRawPredict' : 'rawPredict'}`;
const requestBody = {
anthropic_version: 'vertex-2023-10-16',
stream: stream,
max_tokens: maxTokens,
temperature: temperature,
messages: messages
};
for (let attempt = 0; attempt <= retries; attempt++) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
// Success - break out of retry loop
if (stream) {
// Handle streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'content_block_delta' && data.delta?.text) {
fullText += data.delta.text;
}
if (data.type === 'message_stop') {
break;
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
return { content: [{ type: 'text', text: fullText }] };
} else {
const result = await response.json();
// Handle Vertex AI response format - might be nested in predictions array
if (result.predictions && Array.isArray(result.predictions) && result.predictions[0]) {
return result.predictions[0];
}
// Standard format
return result;
}
}
// Handle errors
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
errorData = { error: { code: response.status, message: errorText } };
}
// Retry on 429 (rate limit) or 503 (service unavailable)
const shouldRetry = (response.status === 429 || response.status === 503) && attempt < retries;
if (shouldRetry) {
// Exponential backoff: wait 2^attempt seconds (1s, 2s, 4s, 8s...)
const waitTime = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30 seconds
console.warn(`Vertex AI rate limited (${response.status}). Retrying in ${waitTime/1000}s... (attempt ${attempt + 1}/${retries + 1})`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
// Non-retryable error or out of retries
throw new Error(`Vertex AI API error: ${response.status} - ${errorText}`);
}
// This should never be reached, but TypeScript needs it
throw new Error('Unexpected error in callVertexAI');
}
async callOpenAI(messages, maxTokens = 1024, temperature = 1.0, stream = false, retries = 3) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY environment variable is required for OpenAI provider');
}
const modelId = process.env.OPENAI_MODEL_ID || 'gpt-4o';
const url = `https://api.openai.com/v1/chat/completions`;
const requestBody = {
model: modelId,
messages: messages,
max_tokens: maxTokens,
temperature: temperature,
stream: stream
};
for (let attempt = 0; attempt <= retries; attempt++) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
if (stream) {
// Handle streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6);
if (dataStr === '[DONE]') break;
try {
const data = JSON.parse(dataStr);
if (data.choices?.[0]?.delta?.content) {
fullText += data.choices[0].delta.content;
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
return { content: [{ type: 'text', text: fullText }] };
} else {
const result = await response.json();
// OpenAI response format
if (result.choices && result.choices[0] && result.choices[0].message) {
return {
content: [{ type: 'text', text: result.choices[0].message.content }]
};
}
return result;
}
}
// Handle errors
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
errorData = { error: { code: response.status, message: errorText } };
}
// Retry on 429 (rate limit) or 503 (service unavailable)
const shouldRetry = (response.status === 429 || response.status === 503) && attempt < retries;
if (shouldRetry) {
// Exponential backoff: wait 2^attempt seconds (1s, 2s, 4s, 8s...)
const waitTime = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30 seconds
console.warn(`OpenAI rate limited (${response.status}). Retrying in ${waitTime/1000}s... (attempt ${attempt + 1}/${retries + 1})`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
// Non-retryable error or out of retries
throw new Error(`OpenAI API error: ${response.status} - ${errorText}`);
}
// This should never be reached
throw new Error('Unexpected error in callOpenAI');
}
async callAI(messages, maxTokens = 1024, temperature = 1.0, stream = false) {
const provider = process.env.AI_PROVIDER || 'vertex';
if (provider === 'openai') {
return await this.callOpenAI(messages, maxTokens, temperature, stream);
} else {
return await this.callVertexAI(messages, maxTokens, temperature, stream);
}
}
async scoreLead(profileData) {
try {
const prompt = `You are a B2B lead scoring expert. Score this LinkedIn profile on a scale of 0-100 based on:
- Profile completeness (20%): How complete is their profile?
- Job title relevance (25%): Is their position decision-maker level?
- Company quality (20%): Company size, industry, growth stage
- Activity level (15%): Recent posts, engagement
- Network strength (10%): Connections count, mutual connections
- Engagement signals (10%): Open to opportunities, active poster
Profile Data:
Name: ${profileData.name}
Headline: ${profileData.headline}
About: ${profileData.about || 'Not provided'}
Location: ${profileData.location}
Connections: ${profileData.connections}
Experience: ${JSON.stringify(profileData.experience)}
Education: ${JSON.stringify(profileData.education)}
Skills: ${profileData.skills.join(', ')}
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"score": <number 0-100>,
"reasoning": "<detailed explanation>",
"strengths": ["<strength 1>", "<strength 2>"],
"weaknesses": ["<weakness 1>", "<weakness 2>"],
"recommended_approach": "<personalization strategy>"
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
1000,
0.7,
false
);
// Extract text content from AI response (OpenAI or Vertex AI)
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
async generateMessage(profileData, context = {}) {
try {
const { valueProposition = '', messageType = 'connection', previousMessages = [] } = context;
let prompt = `You are an expert at writing personalized LinkedIn outreach messages. Create a message for this profile.
Profile Data:
Name: ${profileData.name}
Headline: ${profileData.headline}
About: ${profileData.about || 'Not provided'}
Location: ${profileData.location}
Recent Experience: ${profileData.experience[0] ? JSON.stringify(profileData.experience[0]) : 'Not available'}
Skills: ${profileData.skills.slice(0, 5).join(', ')}
Value Proposition: ${valueProposition}
Message Type: ${messageType === 'connection' ? 'Connection request (max 300 chars)' : 'Direct message (max 500 chars)'}
${previousMessages.length > 0 ? `Previous messages sent:
${previousMessages.map(m => `- ${m.text}`).join('\n')}` : ''}
Requirements:
1. Reference specific details from their profile
2. Be genuine and human-sounding
3. Clear call-to-action
4. No generic templates
5. Professional but conversational tone
${messageType === 'connection' ? '6. Under 300 characters' : '6. Under 500 characters'}
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"subject": "<subject line for connection request>",
"message": "<personalized message text>",
"personalization_elements": ["<element 1>", "<element 2>"]
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
500,
0.8,
false
);
// Extract text content from AI response (OpenAI or Vertex AI)
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
async generateFollowUpSequence(profileData, initialMessage, numberOfFollowUps = 3) {
try {
const prompt = `Create a follow-up sequence for this LinkedIn prospect who received our initial message but hasn't responded.
Profile:
Name: ${profileData.name}
Headline: ${profileData.headline}
Initial Message Sent:
${initialMessage}
Create ${numberOfFollowUps} follow-up messages with:
1. Different angles/value props each time
2. Increasing urgency but staying professional
3. Timing: 3 days, 7 days, 14 days after initial message
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"sequence": [
{
"days_after": 3,
"message": "<follow-up 1>",
"angle": "<what makes this different>"
},
{
"days_after": 7,
"message": "<follow-up 2>",
"angle": "<what makes this different>"
},
{
"days_after": 14,
"message": "<follow-up 3>",
"angle": "<what makes this different>"
}
]
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
1500,
0.8,
false
);
// Extract text content from AI response (OpenAI or Vertex AI)
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
async analyzeProduct(productInfo) {
try {
const { name, website_url, description, value_proposition, target_audience } = productInfo;
const prompt = `You are a B2B market research expert. Deeply analyze this product/service and provide comprehensive insights.
Product Information:
Name: ${name}
Website: ${website_url || 'Not provided'}
Description: ${description || 'Not provided'}
Value Proposition: ${value_proposition || 'Not provided'}
Target Audience: ${target_audience || 'Not provided'}
${website_url ? `Analyze the website if you have information about it, or use the provided details.` : ''}
Analyze:
1. Product/Service Category & Positioning
2. Key Features & Benefits
3. Unique Selling Points (USPs)
4. Target Market Analysis
5. Current Market Trends in this space (2024-2025)
6. Competitive Landscape Insights
7. Ideal Customer Characteristics (who benefits most?)
8. Pain Points This Product Solves
9. Buying Personas That Would Benefit
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"category": "<product category>",
"positioning": "<market positioning>",
"key_features": ["<feature 1>", "<feature 2>", ...],
"benefits": ["<benefit 1>", "<benefit 2>", ...],
"usps": ["<USP 1>", "<USP 2>", ...],
"target_market": "<target market description>",
"market_trends": ["<trend 1>", "<trend 2>", ...],
"competitive_landscape": "<competitive position analysis>",
"ideal_customer_characteristics": {
"job_titles": ["<title 1>", "<title 2>", ...],
"company_sizes": ["<size 1>", "<size 2>", ...],
"industries": ["<industry 1>", "<industry 2>", ...],
"geographic_regions": ["<region 1>", "<region 2>", ...],
"pain_points": ["<pain 1>", "<pain 2>", ...]
},
"buying_personas": [
{
"name": "<persona name>",
"title": "<job title>",
"company_type": "<company description>",
"challenges": ["<challenge 1>", ...],
"goals": ["<goal 1>", ...],
"decision_criteria": ["<criterion 1>", ...]
}
],
"recommended_outreach_angles": ["<angle 1>", "<angle 2>", ...],
"summary": "<executive summary of analysis>"
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
2000,
0.7,
false
);
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
async generateICP(productAnalysis) {
try {
const prompt = `Based on this comprehensive product analysis, generate an Ideal Customer Profile (ICP) for LinkedIn prospecting.
Product Analysis:
${JSON.stringify(productAnalysis, null, 2)}
Generate a detailed ICP that includes:
1. Specific search criteria for LinkedIn (keywords, locations, industries, job titles)
2. Target characteristics (company size, stage, technology stack, etc.)
3. Multiple ICP variations (if applicable) for different buyer personas
4. Current market trends that influence targeting
Consider 2024-2025 market trends and use data-driven insights.
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"icp_name": "<ICP name based on primary persona>",
"description": "<detailed ICP description>",
"search_criteria": {
"keywords": "<LinkedIn search keywords, e.g., 'AI Startup CEO OR Founder'>",
"locations": ["<location 1>", "<location 2>", ...],
"industries": ["<industry 1>", "<industry 2>", ...],
"job_titles": ["<title 1>", "<title 2>", ...],
"company_sizes": ["<size 1>", "<size 2>", ...],
"seniority_levels": ["<level 1>", "<level 2>", ...]
},
"target_characteristics": {
"company_stage": ["<stage 1>", "<stage 2>", ...],
"revenue_range": "<revenue range>",
"employee_count": "<employee range>",
"technology_stack": ["<tech 1>", "<tech 2>", ...],
"funding_stage": ["<stage 1>", "<stage 2>", ...],
"growth_stage": "<growth indicators>"
},
"priority_indicators": ["<indicator 1>", "<indicator 2>", ...],
"exclusion_criteria": ["<criteria 1>", "<criteria 2>", ...],
"linkedin_search_query": "<formatted LinkedIn search query>",
"trending_considerations": ["<trend 1 affecting targeting>", "<trend 2>", ...],
"alternative_icps": [
{
"name": "<alternative ICP name>",
"search_criteria": { ... },
"use_case": "<when to use this ICP>"
}
]
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
2000,
0.7,
false
);
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
async generateCampaignSequence(productAnalysis, icpData, numberOfFollowUps = 3) {
try {
const prompt = `Create a dynamic, personalized follow-up sequence for a LinkedIn outreach campaign. This should be tailored to the product and ICP, NOT a template.
Product Analysis:
${JSON.stringify(productAnalysis, null, 2)}
ICP Data:
${JSON.stringify(icpData, null, 2)}
Create a personalized outreach sequence with:
1. Initial connection message (max 300 chars) - highly personalized, referencing specific ICP characteristics
2. ${numberOfFollowUps} follow-up messages - each with unique angles, not repetitive
3. Dynamic timing based on buyer behavior patterns
4. Messages that feel human and conversational, not templated
5. Each message should reference product benefits relevant to ICP pain points
6. Progressive value delivery (not just asking for time)
IMPORTANT:
- DO NOT use generic templates
- Make each message feel personally crafted
- Reference specific product features that solve ICP pain points
- Vary the communication style across messages
- Include clear but soft CTAs
Respond with JSON only (no markdown code blocks, no backticks, just raw JSON):
{
"initial_message": {
"subject": "<connection request subject>",
"message": "<personalized initial message>",
"personalization_points": ["<point 1>", "<point 2>", ...],
"target_send_time": "immediate"
},
"follow_up_sequence": [
{
"days_after_previous": 3,
"message": "<follow-up message 1>",
"angle": "<unique angle/approach>",
"value_proposition": "<what value does this message deliver>",
"cta_type": "<type of call-to-action>"
},
{
"days_after_previous": 4,
"message": "<follow-up message 2>",
"angle": "<unique angle/approach - DIFFERENT from message 1>",
"value_proposition": "<what value does this message deliver>",
"cta_type": "<type of call-to-action>"
},
{
"days_after_previous": 7,
"message": "<follow-up message 3>",
"angle": "<unique angle/approach - DIFFERENT from messages 1 & 2>",
"value_proposition": "<what value does this message deliver>",
"cta_type": "<type of call-to-action>"
}
],
"sequence_strategy": "<overall strategy explanation>",
"personalization_guide": {
"profile_elements_to_reference": ["<element 1>", "<element 2>", ...],
"product_benefits_to_highlight": ["<benefit 1>", "<benefit 2>", ...],
"icp_pain_points_to_address": ["<pain 1>", "<pain 2>", ...]
}
}`;
const response = await this.callAI(
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
2500,
0.8,
false
);
let textContent = '';
if (response.content && Array.isArray(response.content)) {
textContent = response.content[0]?.text || '';
} else if (response.text) {
textContent = response.text;
} else if (typeof response === 'string') {
textContent = response;
}
if (!textContent) {
throw new Error('No text content in response');
}
// Extract JSON from text (handles markdown code blocks)
const jsonText = this.extractJSON(textContent);
const result = JSON.parse(jsonText);
return { success: true, ...result };
} catch (error) {
return { success: false, error: error.message };
}
}
}
export default AIService;