@cyqlelabs/mcp-dual-cycle-reasoner
Version:
MCP server implementing dual-cycle metacognitive reasoning framework for autonomous agents
461 lines (460 loc) • 19.6 kB
JavaScript
import natural from 'natural';
import nlp from 'compromise';
import { semanticAnalyzer } from './semantic-analyzer.js';
// Extract needed components from natural
const { SentimentAnalyzer, PorterStemmer, WordTokenizer } = natural;
export class Adjudicator {
caseBase = [];
caseIndex = new Map(); // Index for faster retrieval
semanticIntents = [
'performing action',
'checking status',
'retrieving information',
'processing data',
'handling error',
'completing task',
'initiating process',
'validating result',
'organizing information',
'communicating result',
];
/**
* Update semantic intents for domain-specific analysis
*/
updateSemanticIntents(intents) {
if (intents.length > 0) {
this.semanticIntents = intents;
}
}
/**
* Enhanced store experience with quality management and indexing
*/
async storeExperience(case_) {
// Extract semantic features for better retrieval
try {
// Check if semantic analyzer is available
if (!semanticAnalyzer.isReady()) {
throw new Error('SemanticAnalyzer is not ready');
}
const problemFeatures = await semanticAnalyzer.extractSemanticFeatures(case_.problem_description, this.semanticIntents);
const solutionFeatures = await semanticAnalyzer.extractSemanticFeatures(case_.solution, this.semanticIntents);
// Combine features from both problem and solution
const combinedFeatures = {
intents: [...(problemFeatures.intents || []), ...(solutionFeatures.intents || [])],
sentiment: case_.outcome ? 'positive' : 'negative',
keywords: this.extractKeywords(case_.problem_description + ' ' + case_.solution),
};
// Calculate initial confidence score based on various factors
const confidenceScore = this.calculateCaseConfidence(case_, problemFeatures, solutionFeatures);
// Enhance the case with computed features
const enhancedCase = {
...case_,
semantic_features: combinedFeatures,
confidence_score: confidenceScore,
validation_score: this.validateCase(case_),
usage_count: 0,
success_rate: case_.outcome ? 1.0 : 0.0,
};
// Check for duplicates and quality
if (this.isDuplicateCase(enhancedCase)) {
this.updateExistingCase(enhancedCase);
return;
}
if (confidenceScore < 0.3) {
console.warn('Case rejected due to low confidence score:', confidenceScore);
return;
}
// Store the case
this.caseBase.push(enhancedCase);
this.updateIndex(enhancedCase);
// Manage case base size with intelligent pruning
if (this.caseBase.length > 1000) {
this.pruneeCaseBase();
}
}
catch (error) {
console.error('Error storing experience:', error);
// Fallback to simple storage
this.caseBase.push(case_);
}
}
/**
* Enhanced retrieve similar cases with filtering and semantic matching
*/
async retrieveSimilarCases(problemDescription, maxResults = 5, filters = {}) {
try {
// Check if semantic analyzer is available
if (!semanticAnalyzer.isReady()) {
throw new Error('SemanticAnalyzer is not ready');
}
// Extract semantic features from the query
const queryFeatures = await semanticAnalyzer.extractSemanticFeatures(problemDescription, this.semanticIntents);
// Filter cases based on provided filters
let filteredCases = this.caseBase;
if (filters.context_filter) {
filteredCases = filteredCases.filter((case_) => case_.context?.includes(filters.context_filter) ||
case_.problem_description.includes(filters.context_filter));
}
if (filters.difficulty_filter) {
filteredCases = filteredCases.filter((case_) => case_.difficulty_level === filters.difficulty_filter);
}
if (filters.outcome_filter !== undefined) {
filteredCases = filteredCases.filter((case_) => case_.outcome === filters.outcome_filter);
}
// Calculate enhanced similarity scores
const scoredCases = await Promise.all(filteredCases.map(async (case_) => {
const similarity = await this.calculateEnhancedSimilarity(problemDescription, case_.problem_description, queryFeatures, case_.semantic_features);
// Apply usage-based boost for proven cases
const usageBoost = Math.min(0.1, (case_.usage_count || 0) * 0.02);
const successBoost = case_.outcome ? 0.05 : 0;
const confidenceBoost = (case_.confidence_score || 0) * 0.1;
const adjustedSimilarity = similarity + usageBoost + successBoost + confidenceBoost;
return {
case: {
...case_,
similarity_metrics: {
...case_.similarity_metrics,
combined_similarity: adjustedSimilarity,
},
},
similarity: adjustedSimilarity,
};
}));
// Filter by minimum similarity threshold
const minSimilarity = filters.min_similarity || 0.1;
const validCases = scoredCases.filter((item) => item.similarity >= minSimilarity);
// Sort by similarity and success rate
const sortedCases = validCases.sort((a, b) => {
// Primary sort by similarity
if (Math.abs(a.similarity - b.similarity) > 0.05) {
return b.similarity - a.similarity;
}
// Secondary sort by success rate for similar cases
const aSuccessRate = a.case.success_rate || (a.case.outcome ? 1 : 0);
const bSuccessRate = b.case.success_rate || (b.case.outcome ? 1 : 0);
return bSuccessRate - aSuccessRate;
});
// Update usage statistics for retrieved cases
const results = sortedCases.slice(0, maxResults);
results.forEach((item) => {
item.case.usage_count = (item.case.usage_count || 0) + 1;
});
return results.map((item) => item.case);
}
catch (error) {
console.error('Error retrieving similar cases:', error);
// Fallback to simple similarity matching
return this.fallbackRetrieveSimilarCases(problemDescription, maxResults);
}
}
/**
* Enhanced semantic similarity calculation using multiple NLP techniques
*/
calculateCaseSimilarity(current, stored) {
// Parse both texts with compromise
const currentDoc = nlp(current);
const storedDoc = nlp(stored);
// Extract and stem key terms
const currentTerms = currentDoc
.terms()
.out('array')
.map((term) => PorterStemmer.stem(term.toLowerCase()))
.filter((term) => term.length > 2);
const storedTerms = storedDoc
.terms()
.out('array')
.map((term) => PorterStemmer.stem(term.toLowerCase()))
.filter((term) => term.length > 2);
// Calculate Jaccard similarity for stemmed terms
const jaccardSimilarity = this.calculateJaccardDistance(currentTerms, storedTerms);
const jaccardScore = 1 - jaccardSimilarity;
// Calculate sentiment similarity using natural library
const tokenizer = new WordTokenizer();
const analyzer = new SentimentAnalyzer('English', PorterStemmer, 'afinn');
const currentTokens = tokenizer.tokenize(current) || [];
const storedTokens = tokenizer.tokenize(stored) || [];
const currentSentiment = analyzer.getSentiment(currentTokens);
const storedSentiment = analyzer.getSentiment(storedTokens);
const sentimentSimilarity = 1 - Math.abs(currentSentiment - storedSentiment);
// Calculate TF-IDF based similarity for better semantic matching
const allTerms = [...new Set([...currentTerms, ...storedTerms])];
const currentVector = this.createTfIdfVector(currentTerms, allTerms);
const storedVector = this.createTfIdfVector(storedTerms, allTerms);
const cosineSimilarity = this.calculateCosineSimilarity(currentVector, storedVector);
// Combine multiple similarity measures
const combinedSimilarity = jaccardScore * 0.4 + sentimentSimilarity * 0.3 + cosineSimilarity * 0.3;
return Math.max(0, Math.min(1, combinedSimilarity));
}
/**
* Create TF-IDF vector for semantic similarity
*/
createTfIdfVector(terms, allTerms) {
const termFreq = terms.reduce((freq, term) => {
freq[term] = (freq[term] || 0) + 1;
return freq;
}, {});
return allTerms.map((term) => {
const tf = (termFreq[term] || 0) / terms.length;
// Simplified IDF calculation
const idf = Math.log(1 + 1 / Math.max(1, termFreq[term] || 0));
return tf * idf;
});
}
/**
* Calculate cosine similarity between two vectors
*/
calculateCosineSimilarity(vecA, vecB) {
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
if (magnitudeA === 0 || magnitudeB === 0)
return 0;
return dotProduct / (magnitudeA * magnitudeB);
}
/**
* Enhanced evidence gathering using semantic analysis
*/
/**
* Calculate Jaccard distance between two string arrays
*/
calculateJaccardDistance(set1, set2) {
const s1 = new Set(set1);
const s2 = new Set(set2);
const intersection = new Set([...s1].filter((x) => s2.has(x)));
const union = new Set([...s1, ...s2]);
if (union.size === 0)
return 0;
const jaccardSimilarity = intersection.size / union.size;
return 1 - jaccardSimilarity; // Return distance (1 - similarity)
}
/**
* Enhanced similarity calculation combining semantic and traditional methods
*/
async calculateEnhancedSimilarity(query, caseDescription, queryFeatures, caseFeatures) {
// Get semantic similarity from semantic analyzer
const semanticResult = await semanticAnalyzer.calculateSemanticSimilarity(query, caseDescription);
const semanticSimilarity = semanticResult.similarity;
// Traditional NLP similarity (fallback)
const traditionalSimilarity = this.calculateCaseSimilarity(query, caseDescription);
// Feature-based similarity
const featureSimilarity = this.calculateFeatureSimilarity(queryFeatures, caseFeatures);
// Combine similarities with weights
const combinedSimilarity = semanticSimilarity * 0.5 + traditionalSimilarity * 0.3 + featureSimilarity * 0.2;
return Math.max(0, Math.min(1, combinedSimilarity));
}
/**
* Calculate similarity between semantic features
*/
calculateFeatureSimilarity(features1, features2) {
if (!features1 || !features2)
return 0;
let similarity = 0;
let weights = 0;
// Intent similarity
if (features1.intents && features2.intents) {
const intentOverlap = this.calculateArrayOverlap(features1.intents, features2.intents);
similarity += intentOverlap * 0.4;
weights += 0.4;
}
// Keyword similarity
if (features1.keywords && features2.keywords) {
const keywordOverlap = this.calculateArrayOverlap(features1.keywords, features2.keywords);
similarity += keywordOverlap * 0.4;
weights += 0.4;
}
// Sentiment similarity
if (features1.sentiment && features2.sentiment) {
const sentimentMatch = features1.sentiment === features2.sentiment ? 1 : 0;
similarity += sentimentMatch * 0.2;
weights += 0.2;
}
return weights > 0 ? similarity / weights : 0;
}
/**
* Calculate overlap between two arrays
*/
calculateArrayOverlap(arr1, arr2) {
if (!arr1.length || !arr2.length)
return 0;
const set1 = new Set(arr1);
const set2 = new Set(arr2);
const intersection = new Set([...set1].filter((x) => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
/**
* Extract keywords from text using simple heuristics
*/
extractKeywords(text) {
const words = text.toLowerCase().split(/\s+/);
const stopWords = new Set([
'the',
'a',
'an',
'and',
'or',
'but',
'in',
'on',
'at',
'to',
'for',
'of',
'with',
'by',
'is',
'was',
'are',
'were',
'be',
'been',
'have',
'has',
'had',
'do',
'does',
'did',
'will',
'would',
'could',
'should',
'may',
'might',
'can',
'this',
'that',
'these',
'those',
]);
return words
.filter((word) => word.length > 2 && !stopWords.has(word))
.filter((word, index, arr) => arr.indexOf(word) === index) // Remove duplicates
.slice(0, 10); // Limit to top 10 keywords
}
/**
* Calculate confidence score for a case
*/
calculateCaseConfidence(case_, problemFeatures, solutionFeatures) {
let confidence = 0.5; // Base confidence
// Length-based confidence (longer descriptions tend to be more detailed)
const descriptionLength = case_.problem_description.length + case_.solution.length;
confidence += Math.min(0.2, (descriptionLength / 1000) * 0.2);
// Feature quality confidence
const featureCount = (problemFeatures.intents?.length || 0) + (solutionFeatures.intents?.length || 0);
confidence += Math.min(0.2, featureCount * 0.05);
// Sentiment consistency (positive sentiment for successful cases)
if (case_.outcome &&
(problemFeatures.sentiment === 'positive' || solutionFeatures.sentiment === 'positive')) {
confidence += 0.1;
}
return Math.max(0, Math.min(1, confidence));
}
/**
* Validate case quality
*/
validateCase(case_) {
let score = 0.5; // Base score
// Check for minimum description length
if (case_.problem_description.length < 10 || case_.solution.length < 10) {
score -= 0.3;
}
// Check for generic or vague descriptions
const genericTerms = ['error', 'problem', 'issue', 'failed', 'broken'];
const genericCount = genericTerms.reduce((count, term) => count + (case_.problem_description.toLowerCase().includes(term) ? 1 : 0), 0);
score -= genericCount * 0.1;
// Bonus for specific context information
if (case_.context && case_.context.length > 5) {
score += 0.2;
}
return Math.max(0, Math.min(1, score));
}
/**
* Check if case is duplicate
*/
isDuplicateCase(newCase) {
return this.caseBase.some((existingCase) => this.calculateCaseSimilarity(newCase.problem_description, existingCase.problem_description) > 0.9 && this.calculateCaseSimilarity(newCase.solution, existingCase.solution) > 0.9);
}
/**
* Update existing case with new information
*/
updateExistingCase(newCase) {
const existingIndex = this.caseBase.findIndex((existingCase) => this.calculateCaseSimilarity(newCase.problem_description, existingCase.problem_description) > 0.9 && this.calculateCaseSimilarity(newCase.solution, existingCase.solution) > 0.9);
if (existingIndex !== -1) {
const existing = this.caseBase[existingIndex];
// Update success rate
const totalUses = (existing.usage_count || 0) + 1;
const previousSuccesses = (existing.success_rate || 0) * (existing.usage_count || 0);
const newSuccesses = previousSuccesses + (newCase.outcome ? 1 : 0);
existing.success_rate = newSuccesses / totalUses;
existing.usage_count = totalUses;
existing.timestamp = Date.now();
}
}
/**
* Update case index for faster retrieval
*/
updateIndex(case_) {
// Index by context
if (case_.context) {
if (!this.caseIndex.has(case_.context)) {
this.caseIndex.set(case_.context, []);
}
this.caseIndex.get(case_.context).push(case_);
}
}
/**
* Intelligent case base pruning
*/
pruneeCaseBase() {
// Sort by quality score combining multiple factors
const scoredCases = this.caseBase.map((case_) => ({
case: case_,
quality: this.calculateCaseQuality(case_),
}));
// Keep top 800 cases
const prunedCases = scoredCases
.sort((a, b) => b.quality - a.quality)
.slice(0, 800)
.map((item) => item.case);
this.caseBase = prunedCases;
this.rebuildIndex();
}
/**
* Calculate overall case quality for pruning decisions
*/
calculateCaseQuality(case_) {
let quality = 0;
// Success rate contribution
quality += (case_.success_rate || 0) * 0.3;
// Usage count contribution (normalized)
quality += Math.min(0.2, ((case_.usage_count || 0) / 10) * 0.2);
// Confidence score contribution
quality += (case_.confidence_score || 0) * 0.2;
// Validation score contribution
quality += (case_.validation_score || 0) * 0.2;
// Recency contribution (newer cases get slight boost)
const ageDays = (Date.now() - (case_.timestamp || 0)) / (1000 * 60 * 60 * 24);
quality += Math.max(0, 0.1 - ageDays * 0.001);
return quality;
}
/**
* Rebuild index after pruning
*/
rebuildIndex() {
this.caseIndex.clear();
this.caseBase.forEach((case_) => this.updateIndex(case_));
}
/**
* Fallback method for case retrieval when semantic analysis fails
*/
fallbackRetrieveSimilarCases(problemDescription, maxResults) {
const scoredCases = this.caseBase.map((case_) => ({
case: case_,
similarity: this.calculateCaseSimilarity(problemDescription, case_.problem_description),
}));
return scoredCases
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults)
.map((item) => item.case);
}
}