mirror of
https://github.com/Heretek-AI/heretek-openclaw-core.git
synced 2026-07-01 14:17:57 -04:00
297 lines
8.0 KiB
JavaScript
297 lines
8.0 KiB
JavaScript
/**
|
|
* Curiosity Engine
|
|
*
|
|
* Drives exploratory behavior in agents through novelty detection,
|
|
* information gap measurement, and intrinsic motivation.
|
|
*/
|
|
|
|
class CuriosityEngine {
|
|
constructor(options = {}) {
|
|
this.knowledgeBase = options.knowledgeBase || new Map();
|
|
this.curiosityThreshold = options.curiosityThreshold || 0.3;
|
|
this.explorationBudget = options.explorationBudget || 100;
|
|
this.history = [];
|
|
this.noveltyWeights = {
|
|
completelyNew: 1.0,
|
|
partiallyKnown: 0.5,
|
|
wellKnown: 0.1
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate curiosity score for a topic/query
|
|
* @param {string} topic - Topic to evaluate
|
|
* @returns {number} Curiosity score (0-1)
|
|
*/
|
|
calculateCuriosity(topic) {
|
|
const familiarity = this._getFamiliarity(topic);
|
|
const novelty = 1 - familiarity;
|
|
|
|
// Apply novelty weights
|
|
let weight = this.noveltyWeights.wellKnown;
|
|
if (novelty > 0.8) {
|
|
weight = this.noveltyWeights.completelyNew;
|
|
} else if (novelty > 0.3) {
|
|
weight = this.noveltyWeights.partiallyKnown;
|
|
}
|
|
|
|
const curiosityScore = novelty * weight;
|
|
|
|
return Math.min(1, Math.max(0, curiosityScore));
|
|
}
|
|
|
|
/**
|
|
* Get familiarity level with a topic (0-1)
|
|
*/
|
|
_getFamiliarity(topic) {
|
|
const normalizedTopic = topic.toLowerCase().trim();
|
|
|
|
if (!this.knowledgeBase.has(normalizedTopic)) {
|
|
return 0;
|
|
}
|
|
|
|
const entry = this.knowledgeBase.get(normalizedTopic);
|
|
const now = Date.now();
|
|
const age = now - entry.lastAccessed;
|
|
|
|
// Decay familiarity over time (half-life of 24 hours)
|
|
const decayFactor = Math.pow(0.5, age / (24 * 60 * 60 * 1000));
|
|
|
|
return entry.familiarity * decayFactor;
|
|
}
|
|
|
|
/**
|
|
* Record learning about a topic
|
|
* @param {string} topic - Topic learned about
|
|
* @param {number} depth - Depth of learning (0-1)
|
|
*/
|
|
recordLearning(topic, depth = 0.5) {
|
|
const normalizedTopic = topic.toLowerCase().trim();
|
|
const existing = this.knowledgeBase.get(normalizedTopic);
|
|
|
|
const entry = {
|
|
topic: normalizedTopic,
|
|
familiarity: existing ? Math.min(1, existing.familiarity + depth) : depth,
|
|
firstLearned: existing ? existing.firstLearned : Date.now(),
|
|
lastAccessed: Date.now(),
|
|
accessCount: (existing?.accessCount || 0) + 1,
|
|
relatedTopics: existing?.relatedTopics || []
|
|
};
|
|
|
|
this.knowledgeBase.set(normalizedTopic, entry);
|
|
this._recordHistory('learning', topic, depth);
|
|
}
|
|
|
|
/**
|
|
* Detect information gaps in knowledge
|
|
* @param {Array} knownTopics - List of known topics
|
|
* @returns {Array} Identified gaps with curiosity scores
|
|
*/
|
|
detectGaps(knownTopics) {
|
|
const gaps = [];
|
|
|
|
// Generate potential related topics
|
|
for (const topic of knownTopics) {
|
|
const related = this._generateRelatedTopics(topic);
|
|
|
|
for (const relTopic of related) {
|
|
const familiarity = this._getFamiliarity(relTopic);
|
|
|
|
if (familiarity < this.curiosityThreshold) {
|
|
const curiosityScore = this.calculateCuriosity(relTopic);
|
|
|
|
if (curiosityScore >= this.curiosityThreshold) {
|
|
gaps.push({
|
|
topic: relTopic,
|
|
curiosityScore,
|
|
familiarity,
|
|
relatedTo: topic
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by curiosity score descending
|
|
return gaps.sort((a, b) => b.curiosityScore - a.curiosityScore);
|
|
}
|
|
|
|
/**
|
|
* Generate related topics for gap detection
|
|
*/
|
|
_generateRelatedTopics(topic) {
|
|
// Simple heuristic: add common question words and aspects
|
|
const aspects = ['how', 'why', 'what', 'when', 'where', 'who'];
|
|
const relations = ['causes', 'effects', 'history', 'future', 'examples'];
|
|
|
|
const related = [];
|
|
|
|
for (const aspect of aspects) {
|
|
related.push(`${aspect} ${topic}`);
|
|
}
|
|
|
|
for (const relation of relations) {
|
|
related.push(`${topic} ${relation}`);
|
|
}
|
|
|
|
return related.slice(0, 5); // Limit to prevent explosion
|
|
}
|
|
|
|
/**
|
|
* Prioritize exploration queue based on curiosity
|
|
* @param {Array} candidates - Candidate topics to explore
|
|
* @returns {Array} Prioritized list
|
|
*/
|
|
prioritizeExploration(candidates) {
|
|
const scored = candidates.map(candidate => ({
|
|
topic: typeof candidate === 'string' ? candidate : candidate.topic,
|
|
curiosityScore: this.calculateCuriosity(typeof candidate === 'string' ? candidate : candidate.topic),
|
|
metadata: typeof candidate === 'object' ? candidate : {}
|
|
}));
|
|
|
|
return scored.sort((a, b) => b.curiosityScore - a.curiosityScore);
|
|
}
|
|
|
|
/**
|
|
* Check if agent should explore or exploit
|
|
* @returns {boolean} True if should explore
|
|
*/
|
|
shouldExplore() {
|
|
const recentHistory = this.history.slice(-10);
|
|
const explorationRatio = recentHistory.filter(h => h.type === 'exploration').length / recentHistory.length;
|
|
|
|
// Explore if we haven't explored much recently
|
|
return explorationRatio < 0.3 && this.explorationBudget > 0;
|
|
}
|
|
|
|
/**
|
|
* Allocate exploration budget
|
|
* @param {number} amount - Budget to allocate
|
|
* @returns {boolean} Success status
|
|
*/
|
|
allocateExploration(amount) {
|
|
if (amount > this.explorationBudget) {
|
|
return {
|
|
success: false,
|
|
reason: 'Insufficient exploration budget',
|
|
allocated: 0,
|
|
remaining: this.explorationBudget
|
|
};
|
|
}
|
|
|
|
this.explorationBudget -= amount;
|
|
this._recordHistory('budget_allocation', null, -amount);
|
|
|
|
return {
|
|
success: true,
|
|
allocated: amount,
|
|
remaining: this.explorationBudget
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset exploration budget
|
|
* @param {number} newBudget - New budget amount
|
|
*/
|
|
resetBudget(newBudget = 100) {
|
|
this.explorationBudget = newBudget;
|
|
this._recordHistory('budget_reset', null, newBudget);
|
|
}
|
|
|
|
/**
|
|
* Get most curious topics from a list
|
|
* @param {Array} topics - Topics to evaluate
|
|
* @param {number} limit - Number to return
|
|
* @returns {Array} Top curious topics
|
|
*/
|
|
getMostCurious(topics, limit = 5) {
|
|
const scored = topics.map(topic => ({
|
|
topic,
|
|
curiosityScore: this.calculateCuriosity(topic)
|
|
}));
|
|
|
|
return scored
|
|
.filter(t => t.curiosityScore >= this.curiosityThreshold)
|
|
.sort((a, b) => b.curiosityScore - a.curiosityScore)
|
|
.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Record history entry
|
|
*/
|
|
_recordHistory(type, topic, value) {
|
|
this.history.push({
|
|
type,
|
|
topic,
|
|
value,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Keep history bounded
|
|
if (this.history.length > 1000) {
|
|
this.history = this.history.slice(-500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get curiosity statistics
|
|
*/
|
|
getStats() {
|
|
const topics = Array.from(this.knowledgeBase.values());
|
|
const avgFamiliarity = topics.length > 0
|
|
? topics.reduce((sum, t) => sum + t.familiarity, 0) / topics.length
|
|
: 0;
|
|
|
|
return {
|
|
totalTopics: this.knowledgeBase.size,
|
|
avgFamiliarity,
|
|
explorationBudget: this.explorationBudget,
|
|
historyLength: this.history.length,
|
|
highCuriosityTopics: topics.filter(t => (1 - t.familiarity) >= this.curiosityThreshold).length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export knowledge base
|
|
* @returns {Object} Serialized knowledge
|
|
*/
|
|
exportKnowledge() {
|
|
return {
|
|
topics: Array.from(this.knowledgeBase.entries()).map(([key, value]) => ({
|
|
topic: key,
|
|
...value
|
|
})),
|
|
exportedAt: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Import knowledge base
|
|
* @param {Object} data - Serialized knowledge
|
|
*/
|
|
importKnowledge(data) {
|
|
if (data.topics && Array.isArray(data.topics)) {
|
|
for (const topic of data.topics) {
|
|
this.knowledgeBase.set(topic.topic, {
|
|
familiarity: topic.familiarity,
|
|
firstLearned: topic.firstLearned,
|
|
lastAccessed: topic.lastAccessed,
|
|
accessCount: topic.accessCount,
|
|
relatedTopics: topic.relatedTopics || []
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all knowledge
|
|
*/
|
|
clear() {
|
|
this.knowledgeBase.clear();
|
|
this.history = [];
|
|
this.resetBudget(100);
|
|
}
|
|
}
|
|
|
|
module.exports = { CuriosityEngine };
|