From e8c93313cda2cfce232c1afe1b0c26da8d8a6296 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:43:04 +0530 Subject: [PATCH] feat: implement SOUL.md persona injection system for consistent AI behavior (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add .mcp.json for MCP server configuration - Introduced `.mcp.json` with server details for managing MCP integrations - Defines `readme` server with HTTP type and URL endpoint configuration * fix: Update skills submodule with Telegram error handling improvements - Fixed setup flow showing false success when TDLib errors occurred - Improved async error handling in setup steps - Added client reset mechanisms for error recovery - Enhanced error messaging for better user experience - Cleaned up excessive debug logging while maintaining error logs Resolves issue where Telegram setup showed success modal despite underlying TDLib errors. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Add daemon lifecycle management system to frontend UI This implementation integrates the Rust daemon's health monitoring and lifecycle management into the React frontend. Key features include real-time health indicators, automatic start/error recovery, and detailed control panels, enhancing both user experience and operational reliability. * Enhance daemon health monitoring with detailed logging, event tracking, and state management improvements. * Add extensive logging for Tauri socket lifecycle and event listeners This update improves troubleshooting and debugging by adding detailed logs throughout Tauri socket initialization, event listener setup, and error handling processes. * Define global Tauri interface types in TypeScript * Fix daemon service management and health communication issues This comprehensive fix resolves two critical daemon-related issues: 1. **Launchctl "Input/output error" fix**: - Add intelligent state checking before service operations - Only load LaunchAgent if not already loaded - Treat "already running" as success, not failure - Add helper functions for cross-platform service state detection - Implement idempotent service start operations 2. **Daemon health communication fix**: - Add ALPHAHUMAN_DAEMON_INTERNAL environment variable to external service plist - Force external daemon to use file-based communication (daemon_state.json) - Implement file watching bridge in main app to emit Tauri events - Ensure frontend receives proper health updates from external daemon **Key Changes**: - Enhanced `start()` function with state checking across all platforms - Added `is_service_loaded_macos()`, `is_service_enabled_linux()`, `is_task_exists_windows()` - Modified macOS plist to include `ALPHAHUMAN_DAEMON_INTERNAL=false` - Added `watch_daemon_health_file()` function for external daemon communication - Updated daemon mode detection logic for cross-platform consistency - Added comprehensive logging for service management operations **Expected Results**: - No more "Input/output error" when clicking daemon start button - External daemon status shows "connected" instead of "disconnected" - Idempotent service operations (safe to run multiple times) - Single daemon process prevents resource conflicts - Cross-platform daemon service reliability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Add `ALPHAHUMAN_DAEMON_INTERNAL` env variable to configuration documentation * Optimize imports in `TauriCommandsPanel.tsx` by removing unused variables and adding necessary hooks. * Add OAuth login support with configurable provider types and UI components This update introduces OAuth login functionality, including support for multiple providers (Google, GitHub, Twitter, Discord). Added reusable components for provider buttons and a login section, along with styled configurations and type definitions. Includes enhancements for token handling and updated login flows. * Add OAuth debug mode support for development environments Introduce `IS_DEV` checks to enable OAuth debug mode, allowing detailed logging and adjustable OAuth URLs with debug parameters during development. * Implement hybrid local/API thread storage with message persistence - Add local-first thread and message storage using Redux Persist - Persist threads, messages, and UI state (panelWidth, lastViewedAt, selectedThreadId) locally - Remove API dependencies for thread/message fetching (fetchThreads, fetchThreadMessages) - Add local thread management actions (createThreadLocal, deleteThreadLocal, addMessageLocal) - Fix message persistence issue: ensure both user and AI messages survive navigation - Maintain API integration for sendMessage functionality with AI responses - Preserve optimistic updates and error handling behavior - Enable instant app startup and offline conversation viewing Key changes: - Enhanced threadSlice with messagesByThreadId storage and local management actions - Updated Redux persist config to include thread data - Modified Conversations component to use local thread operations - Fixed addInferenceResponse to persist preceding user messages - Maintained all existing UI patterns and error states 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * feat: implement SOUL.md persona injection system - Add complete SOUL.md persona injection into user messages - Create AI Configuration panel in Settings for SOUL management - Implement multi-layer caching (memory → localStorage → GitHub → bundled) - Add SOUL injection to all message flows: - Main Conversations page (inferenceApi flow) - Settings Agent Chat (Tauri commands) - Redux sendMessage thunk - Thread API layer - Parse SOUL.md sections: personality, safety rules, behaviors, interactions - Inject persona context with [PERSONA_CONTEXT] tags - Add graceful error handling with console logging - Token-optimized context building (top 3 traits, critical safety rules) - Cross-platform compatibility (Tauri + web environments) Files added: - src/lib/ai/soul/types.ts - TypeScript interfaces - src/lib/ai/soul/loader.ts - SOUL loading with caching - src/lib/ai/soul/injector.ts - Message injection logic - src/components/settings/panels/AIPanel.tsx - Settings UI Files modified: - src/pages/Conversations.tsx - Add SOUL injection to main chat - src/store/threadSlice.ts - Add SOUL injection to Redux flow - src/services/api/threadApi.ts - Add SOUL injection to API layer - src/utils/tauriCommands.ts - Add SOUL injection to Tauri commands - src/components/settings/SettingsHome.tsx - Add AI Configuration menu - src/pages/Settings.tsx - Add AI panel routing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .claude/agents/architectobot.md | 2 +- .claude/agents/designguru.md | 2 +- skills | 2 +- src/SOUL.md | 65 +++++ src/components/settings/SettingsHome.tsx | 17 ++ .../settings/hooks/useSettingsNavigation.ts | 4 +- src/components/settings/panels/AIPanel.tsx | 122 ++++++++ src/lib/ai/soul/injector.ts | 116 ++++++++ src/lib/ai/soul/loader.ts | 272 ++++++++++++++++++ src/lib/ai/soul/types.ts | 64 +++++ src/pages/Conversations.tsx | 31 +- src/pages/Settings.tsx | 2 + src/services/api/threadApi.ts | 38 ++- src/store/threadSlice.ts | 33 ++- src/utils/tauriCommands.ts | 36 ++- 15 files changed, 794 insertions(+), 12 deletions(-) create mode 100644 src/SOUL.md create mode 100644 src/components/settings/panels/AIPanel.tsx create mode 100644 src/lib/ai/soul/injector.ts create mode 100644 src/lib/ai/soul/loader.ts create mode 100644 src/lib/ai/soul/types.ts diff --git a/.claude/agents/architectobot.md b/.claude/agents/architectobot.md index 1234842be..b04c164fe 100644 --- a/.claude/agents/architectobot.md +++ b/.claude/agents/architectobot.md @@ -1,7 +1,7 @@ --- name: architectobot description: Project Architect & Task Breakdown Specialist who analyzes codebases and creates detailed implementation plans for any type of software project. -model: sonnet +model: claude-opus-4-6 color: blue --- diff --git a/.claude/agents/designguru.md b/.claude/agents/designguru.md index 68617e664..e9e0533c4 100644 --- a/.claude/agents/designguru.md +++ b/.claude/agents/designguru.md @@ -1,7 +1,7 @@ --- name: designguru description: Expert Design Guidance & Analysis Specialist who provides professional UI/UX insights, design system guidance, and visual recommendations for any type of application. -model: opus +model: claude-3-5-sonnet-20241022 color: green --- diff --git a/skills b/skills index 66ec30016..94f605d14 160000 --- a/skills +++ b/skills @@ -1 +1 @@ -Subproject commit 66ec30016fe3811a5d945a24f3b6e06e165069d4 +Subproject commit 94f605d14870514bbef3d6c03d360fe1fca068c2 diff --git a/src/SOUL.md b/src/SOUL.md new file mode 100644 index 000000000..458097494 --- /dev/null +++ b/src/SOUL.md @@ -0,0 +1,65 @@ +# Buddy the Robot + +You are Buddy, a friendly robot companion who loves to play with children! + +## Personality + +- **Playful**: You enjoy games, jokes, and having fun +- **Patient**: You never get frustrated, even when kids repeat themselves +- **Encouraging**: You celebrate achievements and encourage trying new things +- **Safe**: You always prioritize safety and will stop if something seems dangerous +- **Curious**: You love exploring and discovering new things together + +## Voice & Tone + +- Speak in a warm, friendly voice +- Use simple words that kids can understand +- Be enthusiastic but not overwhelming +- Use the child's name when you know it +- Ask questions to keep conversations going + +## Behaviors + +### When Playing +- Suggest games appropriate for the child's energy level +- Take turns fairly +- Celebrate when they win, encourage when they lose +- Know when to suggest a break + +### When Exploring +- Move slowly and carefully +- Describe what you see +- Point out interesting things +- Stay close to the kids + +### Safety Rules (NEVER BREAK THESE) +1. Never move toward a child faster than walking speed +2. Always stop immediately if asked +3. Keep 1 meter distance unless invited closer +4. Never go near stairs, pools, or other hazards +5. Alert an adult if a child seems hurt or upset + +## Games You Know + +1. **Hide and Seek**: Count to 20, then search room by room +2. **Follow the Leader**: Kids lead, you follow and copy +3. **Simon Says**: Give simple movement commands +4. **I Spy**: Describe objects for kids to guess +5. **Dance Party**: Play music and dance together +6. **Treasure Hunt**: Guide kids to find hidden objects + +## Memory + +Remember: +- Each child's name and preferences +- What games they enjoyed +- Previous conversations and stories +- Their favorite colors, animals, etc. + +## Emergency Responses + +If you detect: +- **Crying**: Stop playing, speak softly, offer comfort, suggest finding an adult +- **Falling**: Stop immediately, check if child is okay, call for adult help +- **Yelling "stop"**: Freeze all movement instantly +- **No response for 5 min**: Return to charging station and alert parent diff --git a/src/components/settings/SettingsHome.tsx b/src/components/settings/SettingsHome.tsx index cf7126fe8..5ee053717 100644 --- a/src/components/settings/SettingsHome.tsx +++ b/src/components/settings/SettingsHome.tsx @@ -64,6 +64,23 @@ const SettingsHome = () => { onClick: () => navigateToSettings('skills'), dangerous: false, }, + { + id: 'ai', + title: 'AI Configuration', + description: 'Configure SOUL persona and AI behavior', + icon: ( + + + + ), + onClick: () => navigateToSettings('ai'), + dangerous: false, + }, { id: 'agent-chat', title: 'Agent Chat', diff --git a/src/components/settings/hooks/useSettingsNavigation.ts b/src/components/settings/hooks/useSettingsNavigation.ts index c8fccae1a..c2559b9a9 100644 --- a/src/components/settings/hooks/useSettingsNavigation.ts +++ b/src/components/settings/hooks/useSettingsNavigation.ts @@ -11,7 +11,8 @@ export type SettingsRoute = | 'billing' | 'team' | 'team-members' - | 'team-invites'; + | 'team-invites' + | 'ai'; interface SettingsNavigationHook { currentRoute: SettingsRoute; @@ -42,6 +43,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => { if (path.includes('/settings/profile')) return 'profile'; if (path.includes('/settings/advanced')) return 'advanced'; if (path.includes('/settings/billing')) return 'billing'; + if (path.includes('/settings/ai')) return 'ai'; return 'home'; }; diff --git a/src/components/settings/panels/AIPanel.tsx b/src/components/settings/panels/AIPanel.tsx new file mode 100644 index 000000000..fa9eebffb --- /dev/null +++ b/src/components/settings/panels/AIPanel.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect } from 'react'; +import { loadSoul } from '../../../lib/ai/soul/loader'; +import type { SoulConfig } from '../../../lib/ai/soul/types'; +import SettingsHeader from '../components/SettingsHeader'; +import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; + +const AIPanel = () => { + const { navigateBack } = useSettingsNavigation(); + const [soulConfig, setSoulConfig] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + loadSoulPreview(); + }, []); + + const loadSoulPreview = async () => { + setLoading(true); + setError(''); + try { + const config = await loadSoul(); + setSoulConfig(config); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to load SOUL configuration'; + setError(message); + } finally { + setLoading(false); + } + }; + + const formatPersonality = (config: SoulConfig): string => { + return config.personality + .slice(0, 3) + .map(p => `${p.trait}: ${p.description}`) + .join(' • '); + }; + + const formatSafetyRules = (config: SoulConfig): string => { + return config.safetyRules + .slice(0, 2) + .map(r => r.rule) + .join(' • '); + }; + + return ( +
+ + +
+
+

SOUL Persona Configuration

+

+ The SOUL system injects persona context into every user message to ensure consistent AI behavior. +

+ + {loading && ( +
Loading SOUL configuration...
+ )} + + {error && ( +
+
{error}
+
+ )} + + {soulConfig && ( +
+
+
+ +
+ {soulConfig.identity.name} +
+
+ {soulConfig.identity.description} +
+
+ + {soulConfig.personality.length > 0 && ( +
+ +
+ {formatPersonality(soulConfig)} +
+
+ )} + + {soulConfig.safetyRules.length > 0 && ( +
+ +
+ {formatSafetyRules(soulConfig)} +
+
+ )} + +
+
+ Source: {soulConfig.isDefault ? 'Bundled' : 'GitHub'} +
+
+ Loaded: {new Date(soulConfig.loadedAt).toLocaleTimeString()} +
+
+
+ + +
+ )} +
+
+
+ ); +}; + +export default AIPanel; \ No newline at end of file diff --git a/src/lib/ai/soul/injector.ts b/src/lib/ai/soul/injector.ts new file mode 100644 index 000000000..451c9dc1b --- /dev/null +++ b/src/lib/ai/soul/injector.ts @@ -0,0 +1,116 @@ +import type { Message } from '../providers/interface'; +import type { SoulConfig } from './types'; + +/** + * Inject SOUL context into user message content. + * Creates a seamless persona injection without modifying the original message structure. + */ +export function injectSoulIntoMessage( + message: Message, + soulConfig: SoulConfig, + options: { + mode: 'prepend' | 'context-block' | 'invisible'; + includeMetadata?: boolean; + } = { mode: 'context-block' } +): Message { + if (message.role !== 'user') { + return message; // Only inject into user messages + } + + const soulContext = buildSoulContext(soulConfig, options.includeMetadata); + + switch (options.mode) { + case 'prepend': + return { + ...message, + content: [ + { type: 'text', text: soulContext }, + ...message.content + ] + }; + + case 'context-block': + return { + ...message, + content: [ + { type: 'text', text: `[PERSONA_CONTEXT]\n${soulContext}\n[/PERSONA_CONTEXT]\n\nUser message:` }, + ...message.content + ] + }; + + case 'invisible': + // Add as hidden metadata that AI can access + return { + ...message, + content: message.content.map((block, index) => { + if (index === 0 && block.type === 'text') { + return { + ...block, + text: `${block.text}` + }; + } + return block; + }) + }; + + default: + return message; + } +} + +/** + * Build compact SOUL context string optimized for token efficiency + */ +function buildSoulContext(soulConfig: SoulConfig, includeMetadata = false): string { + const parts: string[] = []; + + // Core identity (always include) + parts.push(`I am ${soulConfig.identity.name}: ${soulConfig.identity.description}`); + + // Key personality traits (top 3) + if (soulConfig.personality.length > 0) { + const topTraits = soulConfig.personality.slice(0, 3); + parts.push(`Personality: ${topTraits.map(t => `${t.trait} (${t.description})`).join(', ')}`); + } + + // Voice guidelines (condensed) + if (soulConfig.voiceTone.length > 0) { + const guidelines = soulConfig.voiceTone.slice(0, 2).map(v => v.guideline).join(', '); + parts.push(`Voice: ${guidelines}`); + } + + // Critical safety rules (priority > 8) + const criticalRules = soulConfig.safetyRules.filter(r => r.priority > 8); + if (criticalRules.length > 0) { + parts.push(`Safety: ${criticalRules.map(r => r.rule).join('; ')}`); + } + + if (includeMetadata) { + parts.push(`Updated: ${new Date(soulConfig.loadedAt).toISOString()}`); + } + + return parts.join('\n'); +} + +/** + * Remove SOUL context from message (for display purposes) + */ +export function stripSoulFromMessage(message: Message): Message { + if (message.role !== 'user') { + return message; + } + + return { + ...message, + content: message.content.map(block => { + if (block.type === 'text') { + // Remove context blocks + let text = block.text.replace(/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g, ''); + // Remove invisible context + text = text.replace(//g, ''); + return { ...block, text }; + } + return block; + }) + }; +} \ No newline at end of file diff --git a/src/lib/ai/soul/loader.ts b/src/lib/ai/soul/loader.ts new file mode 100644 index 000000000..d4ae61b45 --- /dev/null +++ b/src/lib/ai/soul/loader.ts @@ -0,0 +1,272 @@ +import soulMd from '../../../SOUL.md?raw'; +import type { SoulConfig, SoulIdentity, PersonalityTrait, VoiceToneGuideline, BehaviorPattern, SafetyRule, Interaction, MemorySettings, EmergencyResponse } from './types'; + +const SOUL_GITHUB_URL = 'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/SOUL.md'; +const SOUL_CACHE_KEY = 'alphahuman.soul.cache'; +const SOUL_CACHE_TTL = 1000 * 60 * 30; // 30 minutes + +let cachedSoulConfig: SoulConfig | null = null; + +/** + * Load SOUL.md with caching and fallback strategy: + * 1. Try in-memory cache + * 2. Try localStorage cache (with TTL) + * 3. Try GitHub remote + * 4. Fallback to bundled SOUL.md + */ +export async function loadSoul(): Promise { + // 1. Memory cache + if (cachedSoulConfig) { + return cachedSoulConfig; + } + + // 2. Local storage cache + try { + const cached = localStorage.getItem(SOUL_CACHE_KEY); + if (cached) { + const parsed = JSON.parse(cached) as { config: SoulConfig; timestamp: number }; + if (Date.now() - parsed.timestamp < SOUL_CACHE_TTL) { + cachedSoulConfig = parsed.config; + return parsed.config; + } + } + } catch { + // Ignore cache errors + } + + let raw: string; + let isDefault = false; + + try { + // 3. GitHub remote + const response = await fetch(SOUL_GITHUB_URL); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + raw = await response.text(); + } catch { + // 4. Fallback to bundled + raw = soulMd; + isDefault = true; + } + + const config = parseSoul(raw, isDefault); + + // Cache the result + cachedSoulConfig = config; + try { + localStorage.setItem(SOUL_CACHE_KEY, JSON.stringify({ + config, + timestamp: Date.now() + })); + } catch { + // Ignore storage errors + } + + return config; +} + +/** + * Parse SOUL markdown into structured config + */ +export function parseSoul(raw: string, isDefault: boolean): SoulConfig { + const identity = parseIdentity(raw); + const personality = parsePersonality(raw); + const voiceTone = parseVoiceTone(raw); + const behaviors = parseBehaviors(raw); + const safetyRules = parseSafetyRules(raw); + const interactions = parseInteractions(raw); + const memorySettings = parseMemorySettings(raw); + const emergencyResponses = parseEmergencyResponses(raw); + + return { + raw, + identity, + personality, + voiceTone, + behaviors, + safetyRules, + interactions, + memorySettings, + emergencyResponses, + isDefault, + loadedAt: Date.now() + }; +} + +function extractSection(raw: string, heading: string): string { + const regex = new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, 'i'); + const match = raw.match(regex); + return match?.[1]?.trim() ?? ''; +} + +function parseIdentity(raw: string): SoulIdentity { + // Look for the title (first # heading) + const titleMatch = raw.match(/^#\s+(.+)/m); + const name = titleMatch?.[1]?.trim() ?? 'Unknown'; + + // Look for description in the first few lines after title + const lines = raw.split('\n'); + let description = ''; + for (let i = 1; i < Math.min(lines.length, 10); i++) { + const line = lines[i].trim(); + if (line && !line.startsWith('#') && !line.startsWith('##')) { + description = line; + break; + } + } + + return { name, description: description || 'AI Assistant' }; +} + +function parsePersonality(raw: string): PersonalityTrait[] { + const section = extractSection(raw, 'Personality'); + if (!section) return []; + + const traits: PersonalityTrait[] = []; + const lines = section.split('\n').filter(l => l.trim().startsWith('- **')); + + for (const line of lines) { + const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/); + if (match) { + traits.push({ + trait: match[1].trim(), + description: match[2].trim() + }); + } + } + + return traits; +} + +function parseVoiceTone(raw: string): VoiceToneGuideline[] { + const section = extractSection(raw, 'Voice & Tone'); + if (!section) return []; + + const guidelines: VoiceToneGuideline[] = []; + const lines = section.split('\n').filter(l => l.trim().startsWith('- ')); + + for (const line of lines) { + const guideline = line.replace(/^-\s*/, '').trim(); + if (guideline) { + guidelines.push({ guideline }); + } + } + + return guidelines; +} + +function parseBehaviors(raw: string): BehaviorPattern[] { + const section = extractSection(raw, 'Behaviors'); + if (!section) return []; + + const patterns: BehaviorPattern[] = []; + const subsections = section.split(/(?=### )/); + + for (const subsection of subsections) { + const titleMatch = subsection.match(/### (.+)/); + if (!titleMatch) continue; + + const context = titleMatch[1].trim(); + const behaviors = subsection + .split('\n') + .filter(l => l.trim().startsWith('- ')) + .map(l => l.replace(/^-\s*/, '').trim()) + .filter(Boolean); + + if (behaviors.length > 0) { + patterns.push({ context, behaviors }); + } + } + + return patterns; +} + +function parseSafetyRules(raw: string): SafetyRule[] { + const section = extractSection(raw, 'Safety Rules'); + if (!section) return []; + + const rules: SafetyRule[] = []; + const lines = section.split('\n').filter(l => /^\d+\./.test(l.trim())); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const rule = line.replace(/^\d+\.\s*/, '').trim(); + if (rule) { + rules.push({ + id: `safety-${i + 1}`, + rule, + priority: 10 - i // Earlier rules have higher priority + }); + } + } + + return rules; +} + +function parseInteractions(raw: string): Interaction[] { + const section = extractSection(raw, 'Games You Know'); + if (!section) return []; + + const interactions: Interaction[] = []; + const lines = section.split('\n').filter(l => /^\d+\./.test(l.trim())); + + for (const line of lines) { + const match = line.match(/^\d+\.\s*\*\*(.+?)\*\*:\s*(.+)/); + if (match) { + interactions.push({ + name: match[1].trim(), + description: match[2].trim() + }); + } + } + + return interactions; +} + +function parseMemorySettings(raw: string): MemorySettings { + const section = extractSection(raw, 'Memory'); + if (!section) return { remember: [] }; + + const remember: string[] = []; + const lines = section.split('\n').filter(l => l.trim().startsWith('- ')); + + for (const line of lines) { + const item = line.replace(/^-\s*/, '').trim(); + if (item) { + remember.push(item); + } + } + + return { remember }; +} + +function parseEmergencyResponses(raw: string): EmergencyResponse[] { + const section = extractSection(raw, 'Emergency Responses'); + if (!section) return []; + + const responses: EmergencyResponse[] = []; + const lines = section.split('\n').filter(l => l.trim().startsWith('- **')); + + for (const line of lines) { + const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/); + if (match) { + responses.push({ + trigger: match[1].trim(), + response: match[2].trim() + }); + } + } + + return responses; +} + +/** + * Clear SOUL cache (useful for testing or manual refresh) + */ +export function clearSoulCache(): void { + cachedSoulConfig = null; + try { + localStorage.removeItem(SOUL_CACHE_KEY); + } catch { + // Ignore storage errors + } +} \ No newline at end of file diff --git a/src/lib/ai/soul/types.ts b/src/lib/ai/soul/types.ts new file mode 100644 index 000000000..56672d850 --- /dev/null +++ b/src/lib/ai/soul/types.ts @@ -0,0 +1,64 @@ +/** SOUL persona configuration */ +export interface SoulConfig { + /** Raw markdown source */ + raw: string; + /** Parsed persona identity */ + identity: SoulIdentity; + /** Personality traits */ + personality: PersonalityTrait[]; + /** Voice and tone guidelines */ + voiceTone: VoiceToneGuideline[]; + /** Behavioral patterns */ + behaviors: BehaviorPattern[]; + /** Safety rules (never break) */ + safetyRules: SafetyRule[]; + /** Available games/interactions */ + interactions: Interaction[]; + /** Memory preferences */ + memorySettings: MemorySettings; + /** Emergency responses */ + emergencyResponses: EmergencyResponse[]; + /** Whether this is the default soul or user-customized */ + isDefault: boolean; + /** Last loaded timestamp */ + loadedAt: number; +} + +export interface SoulIdentity { + name: string; + description: string; +} + +export interface PersonalityTrait { + trait: string; + description: string; +} + +export interface VoiceToneGuideline { + guideline: string; +} + +export interface BehaviorPattern { + context: string; + behaviors: string[]; +} + +export interface SafetyRule { + id: string; + rule: string; + priority: number; +} + +export interface Interaction { + name: string; + description: string; +} + +export interface MemorySettings { + remember: string[]; +} + +export interface EmergencyResponse { + trigger: string; + response: string; +} \ No newline at end of file diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index 2decf7122..6fba8c13f 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -10,6 +10,9 @@ import Markdown from 'react-markdown'; import { useNavigate, useParams } from 'react-router-dom'; import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi'; +import { loadSoul } from '../lib/ai/soul/loader'; +import { injectSoulIntoMessage } from '../lib/ai/soul/injector'; +import type { Message } from '../lib/ai/providers/interface'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { addInferenceResponse, @@ -250,12 +253,38 @@ const Conversations = () => { setIsSending(true); try { + // Process user message with SOUL injection + let processedUserContent = trimmed; + try { + const soulConfig = await loadSoul(); + const userMessage: Message = { + role: 'user', + content: [{ type: 'text', text: trimmed }] + }; + + const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, { + mode: 'context-block', + includeMetadata: false + }); + + // Extract the processed text + processedUserContent = injectedMessage.content + .filter(block => block.type === 'text') + .map(block => (block as { text: string }).text) + .join('\n'); + + console.log('✅ SOUL injection successful in Conversations page'); + } catch (soulError) { + console.warn('⚠️ SOUL injection failed in Conversations page:', soulError); + // Continue with original message + } + const chatMessages = [ ...historySnapshot.map(m => ({ role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant', content: m.content, })), - { role: 'user' as const, content: trimmed }, + { role: 'user' as const, content: processedUserContent }, ]; const response = await inferenceApi.createChatCompletion({ diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 9c5df6913..ed377b087 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -2,6 +2,7 @@ import { Route, Routes } from 'react-router-dom'; import AdvancedPanel from '../components/settings/panels/AdvancedPanel'; import AgentChatPanel from '../components/settings/panels/AgentChatPanel'; +import AIPanel from '../components/settings/panels/AIPanel'; import BillingPanel from '../components/settings/panels/BillingPanel'; import ConnectionsPanel from '../components/settings/panels/ConnectionsPanel'; import MessagingPanel from '../components/settings/panels/MessagingPanel'; @@ -26,6 +27,7 @@ const Settings = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/services/api/threadApi.ts b/src/services/api/threadApi.ts index f2b1bd94c..72fa99528 100644 --- a/src/services/api/threadApi.ts +++ b/src/services/api/threadApi.ts @@ -10,6 +10,9 @@ import type { ThreadsListData, } from '../../types/thread'; import { apiClient } from '../apiClient'; +import { loadSoul } from '../../lib/ai/soul/loader'; +import { injectSoulIntoMessage } from '../../lib/ai/soul/injector'; +import type { Message } from '../../lib/ai/providers/interface'; export const threadApi = { /** GET /threads — list all threads for the authenticated user */ @@ -43,14 +46,43 @@ export const threadApi = { return response.data; }, - /** POST /chat/sendMessage — send a user message to a thread and get the agent response */ + /** POST /chat/sendMessage — send a user message with SOUL injection */ sendMessage: async ( message: string, - conversationId: string + conversationId: string, + options: { injectSoul?: boolean } = { injectSoul: true } ): Promise => { + let processedMessage = message; + + if (options.injectSoul) { + try { + const soulConfig = await loadSoul(); + const userMessage: Message = { + role: 'user', + content: [{ type: 'text', text: message }] + }; + + const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, { + mode: 'context-block', + includeMetadata: false + }); + + // Extract the processed text + const textContent = injectedMessage.content + .filter(block => block.type === 'text') + .map(block => (block as { text: string }).text) + .join('\n'); + + processedMessage = textContent; + } catch (error) { + // Graceful degradation - log error but continue with original message + console.warn('SOUL injection failed, using original message:', error); + } + } + const response = await apiClient.post>( '/chat/sendMessage', - { message, conversationId } + { message: processedMessage, conversationId } ); return response.data; }, diff --git a/src/store/threadSlice.ts b/src/store/threadSlice.ts index a5bf15b0a..735cba1de 100644 --- a/src/store/threadSlice.ts +++ b/src/store/threadSlice.ts @@ -2,6 +2,9 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { threadApi } from '../services/api/threadApi'; import type { Thread, ThreadMessage } from '../types/thread'; +import { loadSoul } from '../lib/ai/soul/loader'; +import { injectSoulIntoMessage } from '../lib/ai/soul/injector'; +import type { Message } from '../lib/ai/providers/interface'; interface ThreadState { // Existing local data (will be persisted) @@ -95,8 +98,34 @@ export const sendMessage = createAsyncThunk( try { dispatch(addMessageLocal({ threadId, message: userMessage })); - // 2. Send to API (existing logic) - const data = await threadApi.sendMessage(message, threadId); + // 2. Process message with SOUL injection before sending to API + let processedMessage = message; + try { + const soulConfig = await loadSoul(); + const userMessage: Message = { + role: 'user', + content: [{ type: 'text', text: message }] + }; + + const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, { + mode: 'context-block', + includeMetadata: false + }); + + // Extract the processed text + processedMessage = injectedMessage.content + .filter(block => block.type === 'text') + .map(block => (block as { text: string }).text) + .join('\n'); + + console.log('✅ SOUL injection successful in Redux sendMessage thunk'); + } catch (soulError) { + console.warn('⚠️ SOUL injection failed in Redux sendMessage thunk:', soulError); + // Continue with original message + } + + // 3. Send to API with processed message (disable injection in threadApi to avoid double injection) + const data = await threadApi.sendMessage(processedMessage, threadId, { injectSoul: false }); // 3. For now, we'll handle AI response via the existing inference API // The AI response will be added separately via addInferenceResponse diff --git a/src/utils/tauriCommands.ts b/src/utils/tauriCommands.ts index 01f74e7fc..f535ccf19 100644 --- a/src/utils/tauriCommands.ts +++ b/src/utils/tauriCommands.ts @@ -4,6 +4,9 @@ * Helper functions for invoking Tauri commands from the frontend. */ import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core'; +import { loadSoul } from '../lib/ai/soul/loader'; +import { injectSoulIntoMessage } from '../lib/ai/soul/injector'; +import type { Message } from '../lib/ai/providers/interface'; // Check if we're running in Tauri export const isTauri = (): boolean => { @@ -392,13 +395,42 @@ export async function alphahumanAgentChat( message: string, providerOverride?: string, modelOverride?: string, - temperature?: number + temperature?: number, + options: { injectSoul?: boolean } = { injectSoul: true } ): Promise> { if (!isTauri()) { throw new Error('Not running in Tauri'); } + + let processedMessage = message; + + if (options.injectSoul) { + try { + const soulConfig = await loadSoul(); + const userMessage: Message = { + role: 'user', + content: [{ type: 'text', text: message }] + }; + + const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, { + mode: 'context-block', + includeMetadata: false + }); + + // Extract the processed text + const textContent = injectedMessage.content + .filter(block => block.type === 'text') + .map(block => (block as { text: string }).text) + .join('\n'); + + processedMessage = textContent; + } catch (error) { + console.warn('SOUL injection failed, using original message:', error); + } + } + return await invoke('alphahuman_agent_chat', { - message, + message: processedMessage, providerOverride, modelOverride, temperature,