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,