senangwebs-chatbot
Version:
Lightweight JavaScript library with OpenRouter API integration for AI-powered conversations.
301 lines (262 loc) • 7.92 kB
JavaScript
/**
* Context Manager
* Manages conversation context for AI interactions
* Implements sliding window for context management
*/
class ContextManager {
constructor(config = {}) {
this.maxMessages = config.maxMessages || 10;
this.systemPrompt = config.systemPrompt || "You are a helpful assistant.";
this.contextWindow = [];
this.totalTokensEstimate = 0;
this.maxTokens = config.maxTokens || 2000; // Context token limit
this.debug = config.debug || false;
}
/**
* Add message to context
* @param {string} role - Message role: 'system', 'user', or 'assistant'
* @param {string} content - Message content
*/
addMessage(role, content) {
if (!role || !content) {
console.warn(
"[ContextManager] Invalid message: role and content are required"
);
return;
}
const message = {
role: role,
content: content,
timestamp: new Date().toISOString(),
tokens: this._estimateTokens(content),
};
this.contextWindow.push(message);
this.totalTokensEstimate += message.tokens;
if (this.debug) {
console.log(
`[ContextManager] Added ${role} message (${message.tokens} tokens)`
);
console.log(
`[ContextManager] Total messages: ${this.contextWindow.length}, estimated tokens: ${this.totalTokensEstimate}`
);
}
// Trim context if needed
this._trimContext();
}
/**
* Get formatted context for API
* @param {boolean} includeSystem - Include system prompt
* @returns {Array} Array of message objects for API
*/
getContext(includeSystem = true) {
const messages = [];
// Add system prompt if requested
if (includeSystem && this.systemPrompt) {
messages.push({
role: "system",
content: this.systemPrompt,
});
}
// Add context window messages (without metadata)
this.contextWindow.forEach((msg) => {
messages.push({
role: msg.role,
content: msg.content,
});
});
if (this.debug) {
console.log(
`[ContextManager] Returning ${messages.length} messages for API`
);
}
return messages;
}
/**
* Get last N messages from context
* @param {number} count - Number of messages to retrieve
* @returns {Array} Last N messages
*/
getLastMessages(count = 5) {
return this.contextWindow.slice(-count);
}
/**
* Clear all context except system prompt
*/
clear() {
this.contextWindow = [];
this.totalTokensEstimate = 0;
if (this.debug) {
console.log("[ContextManager] Context cleared");
}
}
/**
* Update system prompt
* @param {string} prompt - New system prompt
*/
setSystemPrompt(prompt) {
this.systemPrompt = prompt;
if (this.debug) {
console.log("[ContextManager] System prompt updated");
}
}
/**
* Get context statistics
* @returns {Object} Context statistics
*/
getStats() {
return {
messageCount: this.contextWindow.length,
estimatedTokens: this.totalTokensEstimate,
maxMessages: this.maxMessages,
maxTokens: this.maxTokens,
systemPrompt: this.systemPrompt
? this.systemPrompt.substring(0, 50) + "..."
: null,
};
}
/**
* Trim context to stay within limits
* Uses sliding window approach - removes oldest messages first
* Always keeps at least one exchange (user + assistant)
* @private
*/
_trimContext() {
// Trim by message count
while (this.contextWindow.length > this.maxMessages) {
const removed = this.contextWindow.shift();
this.totalTokensEstimate = Math.max(
0,
this.totalTokensEstimate - removed.tokens
);
if (this.debug) {
console.log(
`[ContextManager] Removed oldest message (${removed.role}), ${this.contextWindow.length} remaining`
);
}
}
// Trim by token count (more aggressive if needed)
while (
this.totalTokensEstimate > this.maxTokens &&
this.contextWindow.length > 2
) {
const removed = this.contextWindow.shift();
this.totalTokensEstimate = Math.max(
0,
this.totalTokensEstimate - removed.tokens
);
if (this.debug) {
console.log(
`[ContextManager] Removed message to reduce tokens (${removed.role}), ${this.totalTokensEstimate} tokens remaining`
);
}
}
}
/**
* Estimate token count for a message
* Simple approximation: ~4 characters per token
* @private
*/
_estimateTokens(text) {
if (!text) return 0;
// Rough estimation: 1 token ≈ 4 characters for English text
// This is a simplified approach; real tokenization is more complex
return Math.ceil(text.length / 4);
}
/**
* Summarize context for long conversations
* Creates a summary of older messages to reduce token count
* @returns {string} Summary of context
*/
summarize() {
if (this.contextWindow.length < 3) {
return "";
}
// Get first half of messages
const messagesToSummarize = this.contextWindow.slice(
0,
Math.floor(this.contextWindow.length / 2)
);
// Create summary
const summary = messagesToSummarize
.map((msg) => {
const preview = msg.content.substring(0, 100);
return `${msg.role}: ${preview}${
msg.content.length > 100 ? "..." : ""
}`;
})
.join("\n");
return `Previous conversation summary:\n${summary}`;
}
/**
* Export context for persistence
* @returns {Object} Serializable context data
*/
export() {
return {
version: "1.0",
timestamp: new Date().toISOString(),
systemPrompt: this.systemPrompt,
maxMessages: this.maxMessages,
maxTokens: this.maxTokens,
contextWindow: this.contextWindow.map((msg) => ({
role: msg.role,
content: msg.content,
timestamp: msg.timestamp,
})),
stats: this.getStats(),
};
}
/**
* Import context from exported data
* @param {Object} data - Exported context data
* @returns {boolean} Success status
*/
import(data) {
try {
if (!data || !data.contextWindow || !Array.isArray(data.contextWindow)) {
throw new Error("Invalid context data format");
}
// Clear existing context
this.clear();
// Import settings
if (data.systemPrompt) {
this.systemPrompt = data.systemPrompt;
}
if (data.maxMessages) {
this.maxMessages = data.maxMessages;
}
if (data.maxTokens) {
this.maxTokens = data.maxTokens;
}
// Import messages
data.contextWindow.forEach((msg) => {
this.addMessage(msg.role, msg.content);
});
if (this.debug) {
console.log(
`[ContextManager] Imported ${this.contextWindow.length} messages`
);
}
return true;
} catch (error) {
console.error("[ContextManager] Import failed:", error);
return false;
}
}
/**
* Inject knowledge base context into system prompt
* @param {string} knowledge - Knowledge base information
*/
injectKnowledge(knowledge) {
if (!knowledge) return;
const enhancedPrompt = `${this.systemPrompt}\n\nRelevant knowledge base information:\n${knowledge}`;
this.setSystemPrompt(enhancedPrompt);
if (this.debug) {
console.log("[ContextManager] Knowledge injected into system prompt");
}
}
}
// Export for use in other modules
if (typeof module !== "undefined" && module.exports) {
module.exports = ContextManager;
}