arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
201 lines • 6.7 kB
JavaScript
import { RelevanceScorer } from "./scorer.js";
import { SemanticDeduplicator } from "./dedup.js";
/**
* ResultMerger - Merges results from multiple memory layers
*
* Pipeline:
* 1. Collect items from all layers
* 2. Score by relevance
* 3. Deduplicate similar items
* 4. Sort by score
* 5. Filter by minimum score
* 6. Truncate to token limit
*/
export class ResultMerger {
scorer;
deduplicator;
constructor() {
this.scorer = new RelevanceScorer();
this.deduplicator = new SemanticDeduplicator();
}
/**
* Merge results from multiple layers into a single ranked list
*/
merge(routingResult, options) {
const startTime = Date.now();
// Apply deduplication threshold if provided
if (options.deduplicationThreshold !== undefined) {
this.deduplicator.setThreshold(options.deduplicationThreshold);
}
// 1. Collect all items from all layers
const allItems = this.collectItems(routingResult);
// 2. Score items by relevance
const scored = this.scorer.score(routingResult.query, allItems);
// 3. Deduplicate similar items (keeps highest scores)
const deduplicated = this.deduplicator.deduplicate(scored);
// 4. Sort by score (descending)
const sorted = deduplicated.sort((a, b) => b.score - a.score);
// 5. Filter by minimum score
const minScore = options.minScore ?? 0.3;
const filtered = sorted.filter((item) => item.score >= minScore);
// 6. Truncate to token limit
const maxTokens = options.maxTokens ?? 10000;
const truncated = this.truncateToTokens(filtered, maxTokens);
const fusionTime = Date.now() - startTime;
return {
items: truncated,
stats: {
totalItems: allItems.length,
deduplicatedItems: deduplicated.length,
finalItems: truncated.length,
estimatedTokens: this.estimateTokens(truncated),
fusionTime,
},
};
}
/**
* Collect all items from routing result
*/
collectItems(routingResult) {
const allItems = [];
for (const layerResult of routingResult.results) {
// Skip layers with errors
if (layerResult.error || !layerResult.items) {
continue;
}
// Extract weight for this layer
const layerWeight = routingResult.classification.weights[layerResult.layer] ?? 1.0;
// Handle different item formats
const items = this.normalizeItems(layerResult.items, layerResult.layer, layerWeight);
allItems.push(...items);
}
return allItems;
}
/**
* Normalize items to consistent MemoryItem format
*/
normalizeItems(items, layer, layerWeight) {
if (!items)
return [];
// Handle array of items
if (Array.isArray(items)) {
return items
.map((item) => this.normalizeItem(item, layer, layerWeight))
.filter((item) => item !== null);
}
// Handle single item
const normalized = this.normalizeItem(items, layer, layerWeight);
return normalized ? [normalized] : [];
}
/**
* Normalize a single item
*/
normalizeItem(item, layer, layerWeight) {
if (!item)
return null;
// If already a MemoryItem
if (item.content !== undefined) {
return {
content: item.content,
timestamp: item.timestamp,
layer: item.layer || layer,
layerWeight: item.layerWeight || layerWeight,
metadata: item.metadata || {},
};
}
// If it's a string
if (typeof item === "string") {
return {
content: item,
layer: layer,
layerWeight,
metadata: {},
};
}
// If it's an object with text/message field
if (typeof item === "object") {
const content = item.text ||
item.message ||
item.data ||
item.value ||
JSON.stringify(item);
return {
content,
timestamp: item.timestamp || item.created_at || item.createdAt,
layer: layer,
layerWeight,
metadata: item,
};
}
return null;
}
/**
* Truncate items to fit within token limit
*/
truncateToTokens(items, maxTokens) {
const result = [];
let tokens = 0;
for (const item of items) {
const itemTokens = this.estimateItemTokens(item);
if (tokens + itemTokens > maxTokens) {
break;
}
result.push({
content: this.getContent(item),
score: item.score,
layer: item.layer,
metadata: item.metadata || {},
});
tokens += itemTokens;
}
return result;
}
/**
* Estimate tokens for a single item
*/
estimateItemTokens(item) {
const content = this.getContent(item);
// Rough estimate: ~4 chars per token
return Math.ceil(content.length / 4);
}
/**
* Estimate total tokens in items
*/
estimateTokens(items) {
return items.reduce((total, item) => {
return total + this.estimateItemTokens(item);
}, 0);
}
/**
* Extract content from item
*/
getContent(item) {
if (typeof item.content === "string") {
return item.content;
}
if (item.content && typeof item.content === "object") {
return JSON.stringify(item.content);
}
return "";
}
/**
* Apply diversity boosting (prioritize items from different layers)
*/
applyDiversityBoost(items, diversityWeight) {
if (diversityWeight === 0)
return items;
const layerCounts = {};
return items.map((item) => {
const layer = item.layer || "unknown";
layerCounts[layer] = (layerCounts[layer] || 0) + 1;
// Reduce score for items from over-represented layers
const penalty = Math.log(layerCounts[layer] + 1) * diversityWeight;
const adjustedScore = Math.max(0, item.score - penalty);
return {
...item,
score: adjustedScore,
};
});
}
}
//# sourceMappingURL=merger.js.map