UNPKG

gcloud-sonar-ai

Version:

A production-ready NPM package that provides a Perplexity Sonar alternative using Google Cloud Vertex AI and Gemini models.

253 lines 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SonarAI = void 0; const vertexai_1 = require("@google-cloud/vertexai"); const auth_1 = require("./auth"); const utils_1 = require("./utils"); class SonarAI { constructor(config = {}) { this.config = { apiKey: config.apiKey || process.env.GEMINI_API_KEY || '', projectId: config.projectId || process.env.GOOGLE_CLOUD_PROJECT || '', location: config.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1', dataStoreId: config.dataStoreId || process.env.VERTEX_AI_DATASTORE_ID || '', searchEngineId: config.searchEngineId || '', useGoogleSearch: config.useGoogleSearch ?? true, maxSearchResults: config.maxSearchResults || 10, searchTimeout: config.searchTimeout || 15000, model: config.model || 'gemini-2.5-flash', thinkingBudget: config.thinkingBudget ?? 1024, maxOutputTokens: config.maxOutputTokens || 2048, temperature: config.temperature ?? 0.1, topP: config.topP ?? 0.95, topK: config.topK ?? 40, enableSafetySettings: config.enableSafetySettings ?? true, customInstructions: config.customInstructions || '', debugMode: config.debugMode ?? false }; if (!this.config.projectId || !this.config.location) { throw new Error('projectId and location are required for Vertex AI'); } this.auth = new auth_1.AuthManager(this.config.apiKey, this.config.projectId); this.vertexAI = new vertexai_1.VertexAI({ project: this.config.projectId, location: this.config.location }); if (this.config.debugMode) { console.log('SonarAI initialized with config:', utils_1.SonarUtils.sanitizeConfig(this.config)); } } async search(query, options) { const startTime = Date.now(); const mergedConfig = { ...this.config, ...options }; try { const isAuthValid = await this.auth.validateAuth(); if (!isAuthValid) { throw new Error('Authentication validation failed'); } const generativeModel = this.vertexAI.getGenerativeModel({ model: mergedConfig.model, }); const tools = this.buildSearchTools(mergedConfig); const result = await generativeModel.generateContent({ contents: [{ role: 'user', parts: [{ text: utils_1.SonarUtils.buildEnhancedPrompt(query, mergedConfig.customInstructions) }] }], tools, generationConfig: { maxOutputTokens: mergedConfig.maxOutputTokens, temperature: mergedConfig.temperature, topP: mergedConfig.topP, topK: mergedConfig.topK, }, safetySettings: mergedConfig.enableSafetySettings ? [ { category: vertexai_1.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: vertexai_1.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE }, { category: vertexai_1.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: vertexai_1.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE } ] : [], }); const response = result.response; const responseText = response.candidates && response.candidates[0].content.parts[0].text || ""; const metadata = response.candidates && response.candidates[0].groundingMetadata; const sources = utils_1.SonarUtils.extractSources(metadata, mergedConfig.maxSearchResults); const searchQueries = metadata?.webSearchQueries || metadata?.retrievalQueries || []; const tokensUsed = utils_1.SonarUtils.calculateTokenUsage(response); const sonarResponse = { text: responseText, sources, groundingMetadata: metadata, searchQueries, responseTime: Date.now() - startTime, tokensUsed, model: mergedConfig.model, timestamp: utils_1.SonarUtils.formatTimestamp() }; if (mergedConfig.debugMode) { console.log('Search completed:', { query, responseTime: sonarResponse.responseTime, sourcesFound: sources.length, tokensUsed: sonarResponse.tokensUsed }); } return sonarResponse; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; if (this.config.debugMode) { console.error('Search failed:', errorMessage, error); } throw new Error(`Sonar search failed: ${errorMessage}`); } } async *searchStream(query, options) { const startTime = Date.now(); const mergedConfig = { ...this.config, ...options }; let accumulatedText = ''; let finalResponse; try { const tools = this.buildSearchTools(mergedConfig); const generativeModel = this.vertexAI.getGenerativeModel({ model: mergedConfig.model }); const result = await generativeModel.generateContentStream({ contents: [{ role: 'user', parts: [{ text: utils_1.SonarUtils.buildEnhancedPrompt(query, mergedConfig.customInstructions) }] }], tools, generationConfig: { maxOutputTokens: mergedConfig.maxOutputTokens, temperature: mergedConfig.temperature, topP: mergedConfig.topP, topK: mergedConfig.topK } }); for await (const chunk of result.stream) { if (chunk.candidates && chunk.candidates[0].content.parts[0].text) { const chunkText = chunk.candidates[0].content.parts[0].text; accumulatedText += chunkText; yield { text: chunkText, isComplete: false }; } } finalResponse = await result.response; const metadata = finalResponse.candidates && finalResponse.candidates[0].groundingMetadata; const sources = utils_1.SonarUtils.extractSources(metadata, mergedConfig.maxSearchResults); const searchQueries = metadata?.webSearchQueries || metadata?.retrievalQueries || []; const tokensUsed = utils_1.SonarUtils.calculateTokenUsage(finalResponse); return { text: accumulatedText, sources, groundingMetadata: metadata, searchQueries, responseTime: Date.now() - startTime, tokensUsed, model: mergedConfig.model, timestamp: utils_1.SonarUtils.formatTimestamp() }; } catch (error) { throw new Error(`Sonar stream search failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createChat() { const tools = this.buildSearchTools(this.config); const history = []; const generativeModel = this.vertexAI.getGenerativeModel({ model: this.config.model, tools }); const chat = generativeModel.startChat({ history }); return { sendMessage: async (message) => { const startTime = Date.now(); const enhancedMessage = utils_1.SonarUtils.buildEnhancedPrompt(message, this.config.customInstructions); const result = await chat.sendMessage(enhancedMessage); const response = result.response; const responseText = response.candidates && response.candidates[0].content.parts[0].text || ""; history.push({ role: 'user', parts: [{ text: enhancedMessage }] }); history.push({ role: 'model', parts: [{ text: responseText }] }); const metadata = response.candidates && response.candidates[0].groundingMetadata; const sources = utils_1.SonarUtils.extractSources(metadata, this.config.maxSearchResults); const searchQueries = metadata?.webSearchQueries || metadata?.retrievalQueries || []; const tokensUsed = utils_1.SonarUtils.calculateTokenUsage(response); return { text: responseText, sources, groundingMetadata: metadata, searchQueries, responseTime: Date.now() - startTime, tokensUsed, model: this.config.model, timestamp: utils_1.SonarUtils.formatTimestamp() }; }, sendMessageStream: async function* (message) { const startTime = Date.now(); let accumulatedText = ''; const enhancedMessage = utils_1.SonarUtils.buildEnhancedPrompt(message, this.config.customInstructions); const result = await chat.sendMessageStream(enhancedMessage); for await (const chunk of result.stream) { if (chunk.candidates && chunk.candidates[0].content.parts[0].text) { const chunkText = chunk.candidates[0].content.parts[0].text; accumulatedText += chunkText; yield { text: chunkText, isComplete: false }; } } history.push({ role: 'user', parts: [{ text: enhancedMessage }] }); history.push({ role: 'model', parts: [{ text: accumulatedText }] }); const finalResponse = await result.response; const metadata = finalResponse.candidates && finalResponse.candidates[0].groundingMetadata; const sources = utils_1.SonarUtils.extractSources(metadata, this.config.maxSearchResults); const searchQueries = metadata?.webSearchQueries || metadata?.retrievalQueries || []; const tokensUsed = utils_1.SonarUtils.calculateTokenUsage(finalResponse); return { text: accumulatedText, sources, groundingMetadata: metadata, searchQueries, responseTime: Date.now() - startTime, tokensUsed, model: this.config.model, timestamp: utils_1.SonarUtils.formatTimestamp() }; }.bind(this), getHistory: () => history.map((h) => ({ role: h.role, content: h.parts.map((p) => p.text).join(''), timestamp: utils_1.SonarUtils.formatTimestamp() })), clearHistory: () => { history.length = 0; } }; } async healthCheck() { try { const testResponse = await this.search('Health check test query', { maxOutputTokens: 50, }); return { status: 'healthy', details: { responseTime: testResponse.responseTime, tokensUsed: testResponse.tokensUsed.total, sourcesFound: testResponse.sources.length, lastChecked: utils_1.SonarUtils.formatTimestamp() } }; } catch (error) { return { status: 'unhealthy', details: { error: error instanceof Error ? error.message : 'Unknown error', lastChecked: utils_1.SonarUtils.formatTimestamp() } }; } } async getAvailableModels() { return [ 'gemini-2.5-flash-latest', 'gemini-2.5-pro-latest', 'gemini-1.0-pro', ]; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; if (this.config.debugMode) { console.log('Configuration updated:', utils_1.SonarUtils.sanitizeConfig(newConfig)); } } getConfig() { return utils_1.SonarUtils.sanitizeConfig(this.config); } buildSearchTools(config) { const tools = []; if (config.useGoogleSearch) { tools.push({ google_search: { disableAttribution: false, } }); } if (config.dataStoreId && config.projectId && config.location) { tools.push({ retrieval: { vertexAiSearch: { datastore: `projects/${config.projectId}/locations/${config.location}/collections/default_collection/dataStores/${config.dataStoreId}`, }, disableAttribution: false, } }); } return tools; } } exports.SonarAI = SonarAI; //# sourceMappingURL=sonar.js.map