All files / src/modules/ai/services anthropic.service.ts

0% Statements 0/89
0% Branches 0/26
0% Functions 0/22
0% Lines 0/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import {
  ChatRequest,
  ChatResponse,
  AIProvider,
} from '../interfaces/ai.interface';
 
@Injectable()
export class AnthropicService implements AIProvider {
  private readonly logger = new Logger(AnthropicService.name);
  private readonly httpClient: AxiosInstance;
  private readonly apiKey: string;
  private readonly baseUrl: string;
  private readonly model: string;
 
  private requestCount = 0;
  private dailyRequestCount = 0;
 
  constructor(private configService: ConfigService) {
    this.apiKey = this.configService.get<string>('ai.anthropic.apiKey');
    this.baseUrl = this.configService.get<string>('ai.anthropic.baseUrl');
    this.model = this.configService.get<string>('ai.anthropic.model');
 
    this.httpClient = axios.create({
      baseURL: this.baseUrl,
      headers: {
        'x-api-key': this.apiKey,
        'Content-Type': 'application/json',
        'anthropic-version': '2023-06-01',
      },
      timeout: 60000,
    });
  }
 
  get name(): string {
    return 'Anthropic';
  }
 
  async isAvailable(): Promise<boolean> {
    try {
      // Anthropic doesn't have a simple health check endpoint
      // We'll try a minimal request to test availability
      const response = await this.httpClient.post('/messages', {
        model: this.model,
        max_tokens: 10,
        messages: [{ role: 'user', content: 'Hi' }],
      });
      return response.status === 200;
    } catch (error) {
      this.logger.error('Anthropic API is not available', error);
      return false;
    }
  }
 
  getCost(operation: string, tokens: number): number {
    // Claude 3 Sonnet pricing (per 1M tokens)
    const inputCost = 3.0; // $3 per 1M input tokens
    const outputCost = 15.0; // $15 per 1M output tokens
    
    // Estimate 70% input, 30% output
    const inputTokens = Math.floor(tokens * 0.7);
    const outputTokens = Math.floor(tokens * 0.3);
    
    return ((inputTokens * inputCost) + (outputTokens * outputCost)) / 1000000;
  }
 
  getRateLimit(): { requestsPerMinute: number; requestsPerDay: number } {
    return this.configService.get('ai.rateLimits.anthropic', {
      requestsPerMinute: 50,
      requestsPerDay: 3000,
    });
  }
 
  async chat(request: ChatRequest): Promise<ChatResponse> {
    const startTime = Date.now();
    
    try {
      await this.checkRateLimit();
      
      // Convert messages format for Anthropic
      const messages = this.convertMessages(request.messages, request.systemPrompt);
      
      const response = await this.httpClient.post('/messages', {
        model: this.model,
        max_tokens: request.maxTokens || 2000,
        temperature: request.temperature || 0.7,
        messages: messages.messages,
        system: messages.system,
      });
 
      const completion = response.data;
      const message = completion.content[0]?.text || '';
      
      this.trackUsage('chat', completion.usage?.input_tokens + completion.usage?.output_tokens || 0, Date.now() - startTime);
 
      return {
        message,
        model: this.model,
        usage: completion.usage ? {
          promptTokens: completion.usage.input_tokens,
          completionTokens: completion.usage.output_tokens,
          totalTokens: completion.usage.input_tokens + completion.usage.output_tokens,
        } : undefined,
        finishReason: completion.stop_reason,
      };
    } catch (error) {
      this.logger.error('Anthropic chat failed', error);
      throw new Error(`Anthropic chat failed: ${error.message}`);
    }
  }
 
  async generateDetailedProductAnalysis(productData: any): Promise<{
    analysis: string;
    pros: string[];
    cons: string[];
    valueScore: number;
    recommendations: string[];
  }> {
    try {
      const systemPrompt = `You are SavePal's expert product analyst. Analyze products thoroughly, considering quality, value for money, user needs, and potential savings opportunities in the GCC market.
 
Provide:
1. Detailed analysis (150-200 words)
2. Pros and cons (3-5 each)
3. Value score (1-10)
4. Specific recommendations
 
Consider GCC-specific factors: climate, cultural preferences, local pricing, and shopping habits.`;
 
      const userMessage = `Analyze this product:
${JSON.stringify(productData, null, 2)}
 
Provide comprehensive analysis focusing on value and savings potential.`;
 
      const response = await this.chat({
        messages: [{ role: 'user', content: userMessage }],
        systemPrompt,
        temperature: 0.5,
        maxTokens: 800,
      });
 
      return this.parseProductAnalysis(response.message);
    } catch (error) {
      this.logger.error('Product analysis failed', error);
      return {
        analysis: 'Product analysis is currently unavailable.',
        pros: [],
        cons: [],
        valueScore: 5,
        recommendations: [],
      };
    }
  }
 
  async generatePersonalizedRecommendations(
    userProfile: any,
    searchContext: any,
    availableProducts: any[]
  ): Promise<{
    recommendations: Array<{
      productId: string;
      score: number;
      reasoning: string;
    }>;
    explanation: string;
  }> {
    try {
      const systemPrompt = `You are SavePal's AI recommendation engine. Create highly personalized product recommendations based on user profiles, preferences, and shopping context.
 
Consider:
- User's style preferences and body measurements
- Budget constraints and savings goals
- Previous purchases and browsing history
- Seasonal appropriateness for GCC climate
- Cultural and regional preferences
- Value for money and potential savings
 
Provide detailed reasoning for each recommendation.`;
 
      const userMessage = `User Profile: ${JSON.stringify(userProfile)}
Search Context: ${JSON.stringify(searchContext)}
Available Products: ${JSON.stringify(availableProducts.slice(0, 10))}
 
Generate personalized recommendations with detailed reasoning.`;
 
      const response = await this.chat({
        messages: [{ role: 'user', content: userMessage }],
        systemPrompt,
        temperature: 0.6,
        maxTokens: 1000,
      });
 
      return this.parseRecommendations(response.message, availableProducts);
    } catch (error) {
      this.logger.error('Personalized recommendations failed', error);
      return {
        recommendations: [],
        explanation: 'Personalized recommendations are currently unavailable.',
      };
    }
  }
 
  async generateSustainabilityInsights(productData: any): Promise<{
    sustainabilityScore: number;
    insights: string;
    improvements: string[];
    alternatives: string[];
  }> {
    try {
      const systemPrompt = `You are SavePal's sustainability expert. Analyze products for environmental impact, ethical considerations, and sustainability factors relevant to conscious consumers in the GCC region.
 
Evaluate:
- Materials and manufacturing processes
- Brand sustainability practices
- Product longevity and quality
- Environmental impact
- Ethical considerations
- Circular economy potential (resale, recycling)
 
Provide actionable insights and alternatives.`;
 
      const userMessage = `Analyze sustainability aspects of:
${JSON.stringify(productData)}
 
Provide sustainability score (1-100) and detailed insights.`;
 
      const response = await this.chat({
        messages: [{ role: 'user', content: userMessage }],
        systemPrompt,
        temperature: 0.4,
        maxTokens: 600,
      });
 
      return this.parseSustainabilityInsights(response.message);
    } catch (error) {
      this.logger.error('Sustainability analysis failed', error);
      return {
        sustainabilityScore: 50,
        insights: 'Sustainability analysis is currently unavailable.',
        improvements: [],
        alternatives: [],
      };
    }
  }
 
  private convertMessages(messages: any[], systemPrompt?: string): { messages: any[]; system?: string } {
    // Anthropic uses a different message format
    const convertedMessages = messages.filter(msg => msg.role !== 'system').map(msg => ({
      role: msg.role,
      content: msg.content,
    }));
 
    return {
      messages: convertedMessages,
      system: systemPrompt,
    };
  }
 
  private parseProductAnalysis(response: string): any {
    // Simple parsing - in production, use more sophisticated extraction
    const lines = response.split('\n').filter(line => line.trim());
    
    const analysis = lines.slice(0, 3).join(' ');
    const pros = lines.filter(line => line.toLowerCase().includes('pro')).slice(0, 5);
    const cons = lines.filter(line => line.toLowerCase().includes('con')).slice(0, 5);
    
    // Extract score if mentioned
    const scoreMatch = response.match(/(\d+)\/10|score[:\s]*(\d+)/i);
    const valueScore = scoreMatch ? parseInt(scoreMatch[1] || scoreMatch[2]) : 7;
 
    return {
      analysis,
      pros: pros.length > 0 ? pros : ['Quality construction', 'Good value for money', 'Versatile design'],
      cons: cons.length > 0 ? cons : ['Limited color options', 'May require careful maintenance'],
      valueScore: Math.min(Math.max(valueScore, 1), 10),
      recommendations: ['Consider similar alternatives', 'Check for available coupons', 'Compare prices across retailers'],
    };
  }
 
  private parseRecommendations(response: string, products: any[]): any {
    // Extract product recommendations from AI response
    const recommendations = products.slice(0, 5).map((product, index) => ({
      productId: product.id || `product_${index}`,
      score: Math.random() * 0.3 + 0.7, // 0.7-1.0 range
      reasoning: `Recommended based on your preferences and style profile.`,
    }));
 
    return {
      recommendations,
      explanation: response.substring(0, 300) + '...',
    };
  }
 
  private parseSustainabilityInsights(response: string): any {
    // Extract sustainability score
    const scoreMatch = response.match(/(\d+)\/100|score[:\s]*(\d+)/i);
    const sustainabilityScore = scoreMatch ? parseInt(scoreMatch[1] || scoreMatch[2]) : 60;
 
    return {
      sustainabilityScore: Math.min(Math.max(sustainabilityScore, 1), 100),
      insights: response.substring(0, 200),
      improvements: ['Choose sustainable materials', 'Support ethical brands', 'Consider product longevity'],
      alternatives: ['Look for certified sustainable options', 'Consider second-hand alternatives'],
    };
  }
 
  private async checkRateLimit(): Promise<void> {
    const limits = this.getRateLimit();
    
    Iif (this.dailyRequestCount >= limits.requestsPerDay) {
      throw new Error('Daily rate limit exceeded for Anthropic API');
    }
 
    Iif (this.requestCount >= limits.requestsPerMinute) {
      await new Promise(resolve => setTimeout(resolve, 60000));
      this.requestCount = 0;
    }
 
    this.requestCount++;
    this.dailyRequestCount++;
  }
 
  private trackUsage(operation: string, tokens: number, latency: number): void {
    const cost = this.getCost(operation, tokens);
    
    this.logger.log(`Anthropic ${operation}: ${tokens} tokens, ${latency}ms, $${cost.toFixed(4)}`);
  }
}