giga-code
Version:
A personal AI CLI assistant powered by Grok for local development.
342 lines • 14.2 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RAGService = void 0;
const generative_ai_1 = require("@google/generative-ai");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
class RAGService {
constructor(projectPath, config) {
this.genAI = null;
this.vectorIndex = null;
this.dbPath = path.join(projectPath, '.giga', 'embeddings');
this.indexPath = path.join(this.dbPath, 'vectors.json');
// Default configuration
this.config = {
enabled: true,
embeddingProvider: 'gemini',
embeddingModel: 'gemini-embedding-001',
searchThreshold: 0.20,
maxResults: 5,
chunkingStrategy: 'logical',
includePatterns: ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx', '**/*.py', '**/*.java', '**/*.cpp', '**/*.c', '**/*.h'],
excludePatterns: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/coverage/**'],
...config
};
// Initialize Gemini AI if using Gemini embeddings
if (this.config.embeddingProvider === 'gemini') {
// Try to load API key from various sources
let apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
// If not found in env, try loading from settings
if (!apiKey) {
try {
const { loadApiKeys } = require('../utils/api-keys');
const apiKeys = loadApiKeys();
apiKey = apiKeys.googleApiKey;
}
catch (error) {
console.warn('Failed to load API keys:', error);
}
}
if (apiKey) {
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey);
}
else {
console.warn('Google API key not found. RAG embeddings require a Google API key.');
}
}
}
async initialize() {
try {
// Ensure database directory exists
if (!fs.existsSync(this.dbPath)) {
fs.mkdirSync(this.dbPath, { recursive: true });
}
// Load existing index or create new one
await this.loadIndex();
}
catch (error) {
console.error('Failed to initialize RAG service:', error);
throw error;
}
}
async loadIndex() {
if (fs.existsSync(this.indexPath)) {
try {
const indexData = fs.readFileSync(this.indexPath, 'utf-8');
this.vectorIndex = JSON.parse(indexData);
}
catch (error) {
console.warn('Failed to load existing index, creating new one:', error);
this.createEmptyIndex();
}
}
else {
this.createEmptyIndex();
}
}
createEmptyIndex() {
this.vectorIndex = {
documents: [],
metadata: {
count: 0,
lastUpdated: new Date().toISOString(),
version: '1.0.0'
}
};
}
async saveIndex() {
if (!this.vectorIndex)
return;
this.vectorIndex.metadata.lastUpdated = new Date().toISOString();
this.vectorIndex.metadata.count = this.vectorIndex.documents.length;
fs.writeFileSync(this.indexPath, JSON.stringify(this.vectorIndex, null, 2));
}
truncateText(text, maxChars = 25000) {
if (text.length <= maxChars) {
return text;
}
return text.substring(0, maxChars) + '...';
}
async generateEmbedding(text) {
if (this.config.embeddingProvider === 'gemini' && this.genAI) {
try {
const model = this.genAI.getGenerativeModel({ model: this.config.embeddingModel });
const result = await model.embedContent(this.truncateText(text));
return result.embedding.values;
}
catch (error) {
console.error('Failed to generate Gemini embedding:', error);
// Fall back to simple embedding
return this.createSimpleEmbedding(text);
}
}
else {
// Fall back to simple embedding if no API key or different provider
return this.createSimpleEmbedding(text);
}
}
createSimpleEmbedding(text) {
// Create a simple feature vector with same dimensions as Gemini (3072)
const words = text.toLowerCase().split(/\s+/);
const embedding = new Array(3072).fill(0); // Match Gemini dimension size
// Basic text features
embedding[0] = Math.min(words.length / 100, 1); // Document length (normalized)
embedding[1] = Math.min((text.match(/function/g) || []).length / 10, 1); // Function count
embedding[2] = Math.min((text.match(/class/g) || []).length / 10, 1); // Class count
embedding[3] = Math.min((text.match(/import/g) || []).length / 10, 1); // Import count
embedding[4] = Math.min((text.match(/export/g) || []).length / 10, 1); // Export count
embedding[5] = Math.min((text.match(/interface/g) || []).length / 10, 1); // Interface count
embedding[6] = Math.min((text.match(/type/g) || []).length / 10, 1); // Type count
embedding[7] = Math.min((text.match(/const/g) || []).length / 10, 1); // Const count
embedding[8] = Math.min((text.match(/let/g) || []).length / 10, 1); // Let count
embedding[9] = Math.min((text.match(/var/g) || []).length / 10, 1); // Var count
// Character-based features for better diversity
for (let i = 10; i < embedding.length; i++) {
const charIndex = i % text.length;
const charCode = text.charCodeAt(charIndex) || 0;
embedding[i] = (charCode / 255) * 0.01; // Very small normalized character features
}
return embedding;
}
cosineSimilarity(a, b) {
if (a.length !== b.length) {
console.warn('Embedding dimension mismatch');
return 0;
}
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
if (magnitudeA === 0 || magnitudeB === 0)
return 0;
return dotProduct / (magnitudeA * magnitudeB);
}
async indexChunks(chunks) {
if (!this.vectorIndex) {
await this.initialize();
}
if (!this.vectorIndex || chunks.length === 0) {
return;
}
try {
console.log(`🚀 Indexing ${chunks.length} code chunks...`);
// Process chunks in batches to avoid overwhelming the embedding API
const batchSize = 5; // Smaller batch size for API rate limits
for (let i = 0; i < chunks.length; i += batchSize) {
const batch = chunks.slice(i, i + batchSize);
for (const chunk of batch) {
// Check if chunk already exists
const existingIndex = this.vectorIndex.documents.findIndex(doc => doc.id === chunk.id);
// Generate embedding
const embedding = await this.generateEmbedding(chunk.content);
const storedDoc = {
id: chunk.id,
content: chunk.content,
embedding,
metadata: {
filePath: chunk.filePath,
type: chunk.type,
name: chunk.name || '',
startLine: chunk.startLine || 0,
endLine: chunk.endLine || 0,
...chunk.metadata
}
};
if (existingIndex >= 0) {
// Update existing document
this.vectorIndex.documents[existingIndex] = storedDoc;
}
else {
// Add new document
this.vectorIndex.documents.push(storedDoc);
}
}
// Save progress and add delay to respect API rate limits
await this.saveIndex();
if (i + batchSize < chunks.length) {
console.log(`📊 Progress: ${i + batch.length}/${chunks.length} chunks indexed`);
await new Promise(resolve => setTimeout(resolve, 200));
}
}
await this.saveIndex();
console.log(`✅ Indexed ${chunks.length} code chunks`);
}
catch (error) {
console.error('Failed to index chunks:', error);
throw error;
}
}
async search(query, maxResults) {
if (!this.vectorIndex) {
await this.initialize();
}
if (!this.vectorIndex || this.vectorIndex.documents.length === 0) {
return [];
}
try {
// Generate embedding for the query
const queryEmbedding = await this.generateEmbedding(query);
// Calculate similarity scores for all documents
const searchResults = [];
for (const doc of this.vectorIndex.documents) {
const similarity = this.cosineSimilarity(queryEmbedding, doc.embedding);
const distance = 1 - similarity; // Convert similarity to distance
if (similarity >= this.config.searchThreshold) {
const chunk = {
id: doc.id,
content: doc.content,
filePath: doc.metadata.filePath,
type: doc.metadata.type,
name: doc.metadata.name || undefined,
startLine: doc.metadata.startLine || undefined,
endLine: doc.metadata.endLine || undefined,
metadata: doc.metadata
};
searchResults.push({
chunk,
score: similarity,
distance
});
}
}
// Sort by similarity score (highest first) and limit results
return searchResults
.sort((a, b) => b.score - a.score)
.slice(0, maxResults || this.config.maxResults);
}
catch (error) {
console.error('Failed to search:', error);
return [];
}
}
async deleteChunk(chunkId) {
if (!this.vectorIndex) {
await this.initialize();
}
if (!this.vectorIndex) {
return;
}
try {
const index = this.vectorIndex.documents.findIndex(doc => doc.id === chunkId);
if (index >= 0) {
this.vectorIndex.documents.splice(index, 1);
await this.saveIndex();
}
}
catch (error) {
console.error('Failed to delete chunk:', error);
}
}
async deleteByFilePath(filePath) {
if (!this.vectorIndex) {
await this.initialize();
}
if (!this.vectorIndex) {
return;
}
try {
const initialCount = this.vectorIndex.documents.length;
this.vectorIndex.documents = this.vectorIndex.documents.filter(doc => doc.metadata.filePath !== filePath);
const deletedCount = initialCount - this.vectorIndex.documents.length;
if (deletedCount > 0) {
await this.saveIndex();
console.log(`🗑️ Deleted ${deletedCount} chunks from ${filePath}`);
}
}
catch (error) {
console.error('Failed to delete chunks by file path:', error);
}
}
async getCollectionInfo() {
if (!this.vectorIndex) {
await this.initialize();
}
const count = this.vectorIndex?.documents.length || 0;
return { count, config: this.config };
}
async clearCollection() {
try {
this.createEmptyIndex();
await this.saveIndex();
console.log('🗑️ Cleared vector index');
}
catch (error) {
console.error('Failed to clear collection:', error);
}
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
getConfig() {
return { ...this.config };
}
static generateChunkId(filePath, type, name, startLine) {
const identifier = `${filePath}:${type}:${name || 'anonymous'}:${startLine || 0}`;
return crypto.createHash('sha256').update(identifier).digest('hex').substring(0, 16);
}
}
exports.RAGService = RAGService;
//# sourceMappingURL=rag-service.js.map