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
JavaScript
"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