import { Injectable, Logger } from '@nestjs/common'; import { AppError, ErrorCode } from '../../common/errors/app.error'; import { OpenAPIOperation } from '../../common/interfaces/swagger.interface'; interface EndpointContext { path: string; method: string; operation: OpenAPIOperation; } interface ZaiMessage { role: 'system' | 'user' | 'assistant'; content: string; } interface ZaiChatRequest { model: string; messages: ZaiMessage[]; temperature: number; max_tokens: number; stream?: boolean; } interface ZaiChatResponse { choices: Array<{ message: { content: string; role: string; }; }>; } @Injectable() export class ZaiService { private readonly logger = new Logger(ZaiService.name); private readonly apiKey: string; private readonly baseUrl: string; constructor() { this.apiKey = process.env.ZAI_API_KEY || ''; this.baseUrl = 'https://api.z.ai/api/paas/v4'; if (this.apiKey) { this.logger.log('Z.AI client initialized successfully'); } else { this.logger.warn('No Z.AI API key found. AI descriptions will be skipped.'); } } async generateDescription(context: EndpointContext): Promise { if (!this.apiKey) { this.logger.warn('No API key - using fallback description'); return this.generateFallbackDescription(context); } try { const prompt = this.buildPrompt(context); const model = process.env.ZAI_MODEL || 'glm-4.5-flash'; this.logger.debug(`Calling Z.AI for ${context.method} ${context.path} with model ${model}`); const requestBody: ZaiChatRequest = { model, messages: [ { role: 'system', content: 'You are an API documentation writer. Generate concise, clear endpoint descriptions in 1-2 paragraphs. Focus on: purpose, key parameters, and expected behavior. Write in English.', }, { role: 'user', content: prompt, }, ], temperature: 0.5, max_tokens: 256, stream: false, }; const url = `${this.baseUrl}/chat/completions`; this.logger.debug(`Calling Z.AI API: ${url}`); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, 'Accept-Language': 'en-US,en', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); this.logger.error(`Z.AI API error: ${response.status} - ${errorText}`); throw new Error(`Z.AI API returned status ${response.status}: ${errorText}`); } const data: ZaiChatResponse = await response.json(); this.logger.debug(`Z.AI response:`, JSON.stringify(data)); const description = data.choices[0]?.message?.content?.trim(); if (!description) { this.logger.warn('No description in Z.AI response - using fallback'); return this.generateFallbackDescription(context); } this.logger.log(`✓ Generated Z.AI description for ${context.method} ${context.path}`); return description; } catch (error) { this.logger.error(`Z.AI service error: ${(error as Error).message}`); this.logger.debug(`Error details:`, error); return this.generateFallbackDescription(context); } } async enrichSwaggerWithDescriptions( paths: Record>, ): Promise>> { if (!this.apiKey) { this.logger.log('Skipping Z.AI enrichment - no API key configured'); return paths; } this.logger.log('Starting parallel Z.AI enrichment...'); // Collect all endpoints const tasks: Array<{ path: string; method: string; operation: OpenAPIOperation; }> = []; for (const [path, methods] of Object.entries(paths)) { for (const [method, operation] of Object.entries(methods)) { tasks.push({ path, method: method.toUpperCase(), operation }); } } this.logger.log(`Processing ${tasks.length} endpoints in parallel (batch size: 10)`); // Process in batches of 10 for faster processing const batchSize = 10; const enrichedResults: Array<{ path: string; method: string; operation: OpenAPIOperation; description: string; }> = []; for (let i = 0; i < tasks.length; i += batchSize) { const batch = tasks.slice(i, i + batchSize); this.logger.debug(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(tasks.length / batchSize)}`); const batchResults = await Promise.all( batch.map(async (task) => { const description = await this.generateDescription(task); return { ...task, description }; }), ); enrichedResults.push(...batchResults); } // Rebuild paths structure const enrichedPaths: Record> = {}; for (const result of enrichedResults) { if (!enrichedPaths[result.path]) { enrichedPaths[result.path] = {}; } enrichedPaths[result.path][result.method.toLowerCase()] = { ...result.operation, description: result.description, }; } this.logger.log('Completed Z.AI enrichment for all endpoints'); return enrichedPaths; } private buildPrompt(context: EndpointContext): string { const { path, method, operation } = context; let prompt = `${method} ${path}`; // Add minimal context const parts: string[] = []; if (operation.parameters && operation.parameters.length > 0) { const paramCount = operation.parameters.length; parts.push(`${paramCount} param${paramCount > 1 ? 's' : ''}`); } if (operation.requestBody) { parts.push('body'); } if (parts.length > 0) { prompt += ` (${parts.join(', ')})`; } prompt += `. Describe in 2-3 sentences: what it does and how to use.`; return prompt; } private generateFallbackDescription(context: EndpointContext): string { const { path, method, operation } = context; const action = this.getActionVerb(method); const resource = this.extractResource(path); let description = `${action} ${resource}`; if (operation.parameters && operation.parameters.length > 0) { const pathParams = operation.parameters.filter((p) => p.in === 'path'); const queryParams = operation.parameters.filter((p) => p.in === 'query'); if (pathParams.length > 0) { description += ` by ${pathParams.map((p) => p.name).join(', ')}`; } if (queryParams.length > 0) { description += `. Supports filtering via query parameters.`; } } return description; } private getActionVerb(method: string): string { const verbMap: Record = { GET: 'Retrieve', POST: 'Create', PUT: 'Update', PATCH: 'Partially update', DELETE: 'Delete', OPTIONS: 'Get options for', HEAD: 'Get headers for', }; return verbMap[method.toUpperCase()] || 'Perform operation on'; } private extractResource(path: string): string { const parts = path.split('/').filter((p) => p && !p.startsWith('{')); if (parts.length === 0) { return 'resource'; } return parts[parts.length - 1]; } }