arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
169 lines • 5.66 kB
JavaScript
import path from "node:path";
import fs from "fs-extra";
import { buildIndex, search } from "../rag/index.js";
const DEFAULT_TOP_K = 5;
export class VectorMemory {
cwd;
constructor(cwd = process.cwd()) {
this.cwd = cwd;
}
get indexPath() {
return path.join(this.cwd, ".arela", ".rag-index.json");
}
/**
* Hexi-004: Initialization wrapper.
* For the current implementation, construction with `cwd` is enough,
* so this is effectively a no-op that verifies the project path.
*/
async init(projectPath) {
// Keep behaviour simple: just ensure the index directory exists.
const expectedDir = path.dirname(this.indexPath);
if (projectPath && path.resolve(projectPath) !== path.resolve(this.cwd)) {
// Caller passed a different project path than the one used by the instance.
// We don't throw here to remain backwards compatible; we simply trust `cwd`.
}
await fs.ensureDir(expectedDir);
}
async isReady() {
return fs.pathExists(this.indexPath);
}
async getStats() {
if (!(await this.isReady())) {
return {
ready: false,
filesIndexed: 0,
embeddings: 0,
indexPath: this.indexPath,
};
}
const index = await fs.readJson(this.indexPath);
const files = new Set();
for (const embedding of index.embeddings ?? []) {
if (embedding.file) {
files.add(embedding.file);
}
}
let indexSizeBytes;
try {
const stats = await fs.stat(this.indexPath);
indexSizeBytes = stats.size;
}
catch {
indexSizeBytes = undefined;
}
return {
ready: true,
filesIndexed: files.size,
embeddings: index.embeddings?.length ?? 0,
model: index.model,
lastIndexedAt: index.timestamp,
indexPath: this.indexPath,
indexSizeBytes,
};
}
async rebuildIndex(options) {
await buildIndex({
cwd: this.cwd,
...(options ?? {}),
});
return this.getStats();
}
/**
* Existing semantic query used by Tri-Memory.
*/
async query(question, topK = DEFAULT_TOP_K) {
if (!(await this.isReady())) {
throw new Error("Vector memory is not initialized. Run `arela index` or `arela memory init --refresh-vector` first.");
}
const results = await search(question, { cwd: this.cwd }, topK);
return results.map((item) => ({
file: item.file,
snippet: summarizeSnippet(item.chunk),
score: Number(item.score?.toFixed(4) ?? 0),
}));
}
/**
* Hexi-004: Wrapper search API that returns full chunks with basic source info.
*/
async search(queryText, limit = DEFAULT_TOP_K) {
if (!(await this.isReady())) {
throw new Error("Vector memory is not initialized. Run `arela index` or `arela memory init --refresh-vector` first.");
}
const results = await search(queryText, { cwd: this.cwd }, limit);
return results.map((item) => ({
file: item.file,
chunk: item.chunk,
score: Number(item.score?.toFixed(4) ?? 0),
// Line information is not currently tracked in the index.
lineStart: 0,
lineEnd: 0,
}));
}
/**
* Hexi-004: Search by a pre-computed embedding vector.
* This avoids re-generating embeddings via Ollama for advanced Meta-RAG flows.
*/
async searchByEmbedding(embedding, limit = DEFAULT_TOP_K) {
if (!(await this.isReady())) {
throw new Error("Vector memory is not initialized. Run `arela index` or `arela memory init --refresh-vector` first.");
}
const index = await fs.readJson(this.indexPath);
const results = (index.embeddings ?? []).map((item) => ({
file: item.file,
chunk: item.chunk,
score: cosineSimilarity(embedding, item.embedding),
lineStart: 0,
lineEnd: 0,
}));
return results
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
/**
* Hexi-004: Convenience helper to get index size in bytes.
*/
async getIndexSize() {
try {
const stats = await fs.stat(this.indexPath);
return stats.size;
}
catch {
return 0;
}
}
/**
* Hexi-004: Convenience helper to get total chunk count.
*/
async getChunkCount() {
if (!(await this.isReady())) {
return 0;
}
const index = await fs.readJson(this.indexPath);
return index.embeddings?.length ?? 0;
}
}
function summarizeSnippet(chunk) {
const normalized = chunk.replace(/\s+/g, " ").trim();
if (normalized.length <= 160) {
return normalized;
}
return `${normalized.slice(0, 157)}...`;
}
function cosineSimilarity(a, b) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || b.length === 0 || a.length !== b.length) {
return 0;
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) {
return 0;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
//# sourceMappingURL=vector.js.map