feat: implement SOUL.md persona injection system for consistent AI behavior (#158)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Cyrus Gray
2026-03-05 17:43:04 +05:30
committed by GitHub
co-authored by Claude
parent c3718d9e4f
commit e8c93313cd
15 changed files with 794 additions and 12 deletions
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+1 -1
Submodule skills updated: 66ec30016f...94f605d148
+65
View File
@@ -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
+17
View File
@@ -64,6 +64,23 @@ const SettingsHome = () => {
onClick: () => navigateToSettings('skills'),
dangerous: false,
},
{
id: 'ai',
title: 'AI Configuration',
description: 'Configure SOUL persona and AI behavior',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 3a.75.75 0 00-1.5 0v2.25H6a2.25 2.25 0 000 4.5h2.25V12H6a2.25 2.25 0 000 4.5h2.25V18a.75.75 0 001.5 0v-1.5H12V18a.75.75 0 001.5 0v-1.5H18a2.25 2.25 0 000-4.5h-4.5V9.75H18a2.25 2.25 0 000-4.5h-4.5V3a.75.75 0 00-1.5 0v2.25H9.75V3z"
/>
</svg>
),
onClick: () => navigateToSettings('ai'),
dangerous: false,
},
{
id: 'agent-chat',
title: 'Agent Chat',
@@ -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';
};
+122
View File
@@ -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<SoulConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>('');
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 (
<div className="h-full flex flex-col">
<SettingsHeader title="AI Configuration" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="space-y-4">
<h3 className="text-lg font-semibold text-white">SOUL Persona Configuration</h3>
<p className="text-sm text-gray-400">
The SOUL system injects persona context into every user message to ensure consistent AI behavior.
</p>
{loading && (
<div className="text-sm text-gray-400 animate-pulse">Loading SOUL configuration...</div>
)}
{error && (
<div className="bg-red-500/10 border border-red-500/40 rounded-lg p-3">
<div className="text-sm text-red-200">{error}</div>
</div>
)}
{soulConfig && (
<div className="space-y-3">
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Identity</label>
<div className="text-sm text-green-400 font-medium mt-1">
{soulConfig.identity.name}
</div>
<div className="text-xs text-gray-300 mt-1">
{soulConfig.identity.description}
</div>
</div>
{soulConfig.personality.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Personality</label>
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
{formatPersonality(soulConfig)}
</div>
</div>
)}
{soulConfig.safetyRules.length > 0 && (
<div>
<label className="text-xs text-gray-400 uppercase tracking-wide">Safety Rules</label>
<div className="text-xs text-yellow-300 mt-1 leading-relaxed">
{formatSafetyRules(soulConfig)}
</div>
</div>
)}
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
<div className="text-xs text-gray-400">
Source: {soulConfig.isDefault ? 'Bundled' : 'GitHub'}
</div>
<div className="text-xs text-gray-400">
Loaded: {new Date(soulConfig.loadedAt).toLocaleTimeString()}
</div>
</div>
</div>
<button
onClick={loadSoulPreview}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors"
disabled={loading}
>
Refresh SOUL Configuration
</button>
</div>
)}
</section>
</div>
</div>
);
};
export default AIPanel;
+116
View File
@@ -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: `<!--SOUL_CONTEXT:${btoa(soulContext)}-->${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(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
return { ...block, text };
}
return block;
})
};
}
+272
View File
@@ -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<SoulConfig> {
// 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
}
}
+64
View File
@@ -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;
}
+30 -1
View File
@@ -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({
+2
View File
@@ -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 = () => {
<Route path="profile" element={<ProfilePanel />} />
<Route path="advanced" element={<AdvancedPanel />} />
<Route path="agent-chat" element={<AgentChatPanel />} />
<Route path="ai" element={<AIPanel />} />
<Route path="billing" element={<BillingPanel />} />
<Route path="skills" element={<SkillsPanel />} />
<Route path="team" element={<TeamPanel />} />
+35 -3
View File
@@ -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<SendMessageResponseData> => {
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<ApiResponse<SendMessageResponseData>>(
'/chat/sendMessage',
{ message, conversationId }
{ message: processedMessage, conversationId }
);
return response.data;
},
+31 -2
View File
@@ -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
+34 -2
View File
@@ -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<CommandResponse<string>> {
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,