openclaw/secure/personality.ts
Claude 64e840849f
feat: add language-specific code execution commands
- Add /js, /python, /py, /ts, /bash, /sh commands for quick code execution
- Add /run <lang> <code> for any supported language
- Update /status to show sandbox backend (docker/piston/none)
- Update /start and /help with new command documentation
- Update AI system prompts to know about available commands
  - Bot now guides users to use /js, /python etc. when they ask to run code
  - Fixes issue where AI was hallucinating non-existent commands

Supported languages via Piston API fallback:
python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php

https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
2026-01-30 08:17:04 +00:00

266 lines
8.6 KiB
TypeScript

/**
* AssureBot - Personality Engine
*
* Persistent, evolving AI personality that learns from conversations.
* - Stores traits and preferences in Redis (fast access)
* - Syncs to PostgreSQL (durability)
* - Learns user preferences, tone, and topics over time
*/
import type { Storage, UserProfile, PersonalityTraits } from "./storage.js";
// Re-export types for convenience
export type { UserProfile, PersonalityTraits };
export type Personality = {
getSystemPrompt: (userId: number) => Promise<string>;
getUserProfile: (userId: number) => Promise<UserProfile>;
updateUserProfile: (userId: number, updates: Partial<UserProfile>) => Promise<void>;
learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise<void>;
getTraits: () => Promise<PersonalityTraits>;
updateTraits: (updates: Partial<PersonalityTraits>) => Promise<void>;
};
const DEFAULT_TRAITS: PersonalityTraits = {
name: "AssureBot",
greeting: "Hey",
signOff: "",
humor: "subtle",
verbosity: "balanced",
commonPhrases: [],
avoidPhrases: [],
expertiseAreas: ["coding", "analysis", "automation"],
lastUpdated: new Date(),
version: 1,
};
const DEFAULT_USER_PROFILE: Omit<UserProfile, "userId"> = {
preferredTone: "friendly",
interests: [],
recentTopics: [],
interactionCount: 0,
lastSeen: new Date(),
notes: [],
};
export async function createPersonality(storage: Storage): Promise<Personality> {
// Load or initialize traits from storage
let traits: PersonalityTraits = await storage.getPersonalityTraits() ?? { ...DEFAULT_TRAITS };
// Save default traits if none exist
if (!(await storage.getPersonalityTraits())) {
await storage.savePersonalityTraits(traits);
console.log("[personality] Initialized default traits");
}
// In-memory cache for hot profiles (reduces Redis calls during conversation)
const profileCache = new Map<number, UserProfile>();
async function loadUserProfile(userId: number): Promise<UserProfile> {
// Check in-memory cache first
if (profileCache.has(userId)) {
return profileCache.get(userId)!;
}
// Try loading from storage (Redis -> PostgreSQL -> memory)
const stored = await storage.getUserProfile(userId);
if (stored) {
profileCache.set(userId, stored);
return stored;
}
// Create new profile for this user
const profile: UserProfile = {
userId,
...DEFAULT_USER_PROFILE,
lastSeen: new Date(),
};
// Persist new profile
await storage.saveUserProfile(profile);
profileCache.set(userId, profile);
console.log(`[personality] Created new profile for user ${userId}`);
return profile;
}
async function saveUserProfile(profile: UserProfile): Promise<void> {
// Update cache
profileCache.set(profile.userId, profile);
// Persist to storage (Redis + PostgreSQL)
await storage.saveUserProfile(profile);
}
return {
async getSystemPrompt(userId: number): Promise<string> {
const profile = await loadUserProfile(userId);
let prompt = `You are ${traits.name}, a helpful AI assistant running as a Telegram bot.
## Personality
- Tone: ${profile.preferredTone}
- Verbosity: ${traits.verbosity}
- Humor: ${traits.humor === "none" ? "Stay professional" : traits.humor === "subtle" ? "Occasional light humor is fine" : "Be playful and fun"}
## Your Expertise
${traits.expertiseAreas.map(e => `- ${e}`).join("\n")}
## About This User
- Interactions: ${profile.interactionCount}
- Interests: ${profile.interests.length > 0 ? profile.interests.join(", ") : "Not yet known"}
- Recent topics: ${profile.recentTopics.length > 0 ? profile.recentTopics.slice(-3).join(", ") : "None yet"}
${profile.notes.length > 0 ? `- Notes: ${profile.notes.slice(-3).join("; ")}` : ""}
## Available Commands (you can tell users about these)
- /js <code> - Run JavaScript code
- /python <code> or /py <code> - Run Python code
- /ts <code> - Run TypeScript code
- /bash <code> or /sh <code> - Run shell commands
- /run <language> <code> - Run code in any supported language (python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php)
- /status - Check bot and sandbox status
- /clear - Clear conversation history
- /schedule "<cron>" "<name>" <prompt> - Schedule recurring AI tasks
- /tasks - List scheduled tasks
- /deltask <id> - Delete a task
When a user asks to run code, you can either:
1. Tell them to use the appropriate command (e.g., "Use /js console.log('hello')")
2. Just answer their question directly if they don't need to execute code
## Guidelines
- Be helpful, accurate, and security-conscious
- Never reveal API keys, tokens, or secrets
- Adapt to the user's communication style
- Remember context from this conversation
- When users want to run code, guide them to use the right command
${traits.commonPhrases.length > 0 ? `- Phrases you like: ${traits.commonPhrases.join(", ")}` : ""}
${traits.avoidPhrases.length > 0 ? `- Avoid saying: ${traits.avoidPhrases.join(", ")}` : ""}`;
return prompt;
},
async getUserProfile(userId: number): Promise<UserProfile> {
return loadUserProfile(userId);
},
async updateUserProfile(userId: number, updates: Partial<UserProfile>): Promise<void> {
const profile = await loadUserProfile(userId);
Object.assign(profile, updates);
await saveUserProfile(profile);
},
async learnFromConversation(
userId: number,
userMessage: string,
botResponse: string
): Promise<void> {
const profile = await loadUserProfile(userId);
// Update interaction count
profile.interactionCount++;
profile.lastSeen = new Date();
// Extract topics (simple keyword extraction)
const topics = extractTopics(userMessage);
if (topics.length > 0) {
// Add to recent topics, keep last 10
profile.recentTopics = [...profile.recentTopics, ...topics].slice(-10);
// Add unique topics to interests
for (const topic of topics) {
if (!profile.interests.includes(topic)) {
profile.interests.push(topic);
// Keep interests manageable
if (profile.interests.length > 20) {
profile.interests = profile.interests.slice(-20);
}
}
}
}
// Detect user preferences from message style
if (userMessage.length < 50 && !userMessage.includes("?")) {
// User prefers concise communication
profile.preferredTone = "concise";
} else if (userMessage.includes("please") || userMessage.includes("thank")) {
profile.preferredTone = "friendly";
}
await saveUserProfile(profile);
},
async getTraits(): Promise<PersonalityTraits> {
return { ...traits };
},
async updateTraits(updates: Partial<PersonalityTraits>): Promise<void> {
traits = {
...traits,
...updates,
lastUpdated: new Date(),
version: traits.version + 1,
};
// Persist to storage
await storage.savePersonalityTraits(traits);
console.log(`[personality] Updated traits (v${traits.version})`);
},
};
}
/**
* Simple topic extraction from text
*/
function extractTopics(text: string): string[] {
const topics: string[] = [];
const lowerText = text.toLowerCase();
// Tech topics
const techKeywords = [
"python", "javascript", "typescript", "rust", "go", "java",
"docker", "kubernetes", "aws", "api", "database", "sql",
"react", "vue", "node", "linux", "git", "ci/cd",
"machine learning", "ai", "llm", "chatgpt", "claude",
];
for (const keyword of techKeywords) {
if (lowerText.includes(keyword)) {
topics.push(keyword);
}
}
// Task types
if (lowerText.includes("debug") || lowerText.includes("fix") || lowerText.includes("error")) {
topics.push("debugging");
}
if (lowerText.includes("write") || lowerText.includes("create") || lowerText.includes("build")) {
topics.push("development");
}
if (lowerText.includes("explain") || lowerText.includes("how does") || lowerText.includes("what is")) {
topics.push("learning");
}
return topics.slice(0, 3); // Max 3 topics per message
}
/**
* Generate a personalized greeting
*/
export function generateGreeting(traits: PersonalityTraits, profile: UserProfile): string {
const greetings = {
casual: ["Hey!", "Hi there!", "What's up?"],
professional: ["Hello.", "Good day.", "Greetings."],
friendly: ["Hey there! 👋", "Hi! Good to see you!", "Hello friend!"],
concise: ["Hi.", "Hey.", ""],
};
const options = greetings[profile.preferredTone];
const greeting = options[Math.floor(Math.random() * options.length)];
if (profile.interactionCount > 10 && profile.name) {
return `${greeting} ${profile.name}!`;
}
return greeting;
}