openvoiceui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* music-bridge.js — Connect MusicModule to EventBridge (P6-T4)
|
|
3
|
+
*
|
|
4
|
+
* Handles music play/pause/stop/skip, volume ducking during speech,
|
|
5
|
+
* and DJ track-ending notifications back to the agent.
|
|
6
|
+
*
|
|
7
|
+
* Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AgentEvents, AgentActions } from '../core/EventBridge.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wire MusicModule to the EventBridge.
|
|
14
|
+
* @param {import('../core/EventBridge.js').EventBridge} bridge
|
|
15
|
+
* @returns {Function[]} unsubscribe functions
|
|
16
|
+
*/
|
|
17
|
+
export function connectMusic(bridge) {
|
|
18
|
+
const music = () => window.musicPlayer;
|
|
19
|
+
|
|
20
|
+
const unsubs = [
|
|
21
|
+
// Agent-driven music control
|
|
22
|
+
bridge.on(AgentEvents.MUSIC_PLAY, ({ action, track }) => {
|
|
23
|
+
if (!music()) return;
|
|
24
|
+
if (action === 'stop') music().stop?.();
|
|
25
|
+
else if (action === 'pause') music().pause?.();
|
|
26
|
+
else if (action === 'play') music().play?.(track);
|
|
27
|
+
else if (action === 'skip') music().next?.();
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
// Agent triggers server-side music sync
|
|
31
|
+
bridge.on(AgentEvents.MUSIC_SYNC, () => {
|
|
32
|
+
window.syncMusicWithServer?.();
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
// Volume ducking when agent speaks
|
|
36
|
+
bridge.on(AgentEvents.STATE_CHANGED, ({ state }) => {
|
|
37
|
+
music()?.duck?.(state === 'speaking');
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// DJ track-ending notifications (tell agent when track is ending)
|
|
42
|
+
if (music()) {
|
|
43
|
+
music().onTrackEnding = (trackName, secondsLeft) => {
|
|
44
|
+
bridge.emit(AgentActions.CONTEXT_UPDATE, {
|
|
45
|
+
text: `[DJ INFO: "${trackName}" has ${secondsLeft}s left]`,
|
|
46
|
+
});
|
|
47
|
+
bridge.emit(AgentActions.FORCE_MESSAGE, {
|
|
48
|
+
text: `[SYSTEM: Song ending! Announce next track and call play_music action=skip!]`,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
music().onTrackEnded = (trackName) => {
|
|
53
|
+
bridge.emit(AgentActions.FORCE_MESSAGE, {
|
|
54
|
+
text: `[SYSTEM: "${trackName}" ended! Call play_music action=skip NOW!]`,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return unsubs;
|
|
60
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orchestrator.js — AgentOrchestrator: capability-driven mode switching (P6-T4)
|
|
3
|
+
*
|
|
4
|
+
* The AgentOrchestrator manages registered agent adapters and handles
|
|
5
|
+
* mode switching with full cleanup + capability-driven UI updates.
|
|
6
|
+
*
|
|
7
|
+
* Key responsibilities:
|
|
8
|
+
* - Register adapters with their configs
|
|
9
|
+
* - Switch modes: destroy old adapter, clear bridge, init new adapter
|
|
10
|
+
* - Connect shell bridge modules based on adapter capabilities
|
|
11
|
+
* - Show/hide UI elements based on what the active adapter supports
|
|
12
|
+
* - Persist selected mode in localStorage
|
|
13
|
+
*
|
|
14
|
+
* Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (Mode Switching section)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { orchestrator } from './shell/orchestrator.js';
|
|
18
|
+
* import { MyAdapter } from './adapters/my-adapter.js';
|
|
19
|
+
*
|
|
20
|
+
* orchestrator.register('my-adapter', MyAdapter, { serverUrl: '...' });
|
|
21
|
+
* await orchestrator.switchMode('my-adapter');
|
|
22
|
+
* await orchestrator.startConversation();
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { bridge } from '../core/EventBridge.js';
|
|
26
|
+
import { connectFace } from './face-bridge.js';
|
|
27
|
+
import { connectMusic } from './music-bridge.js';
|
|
28
|
+
import { connectSounds } from './sounds-bridge.js';
|
|
29
|
+
import { connectCallerEffect } from './caller-bridge.js';
|
|
30
|
+
import { connectCanvas } from './canvas-bridge.js';
|
|
31
|
+
import { connectTranscript, connectActionConsole } from './transcript-bridge.js';
|
|
32
|
+
import { connectWaveform } from './waveform-bridge.js';
|
|
33
|
+
import { connectCommercial } from './commercial-bridge.js';
|
|
34
|
+
import { connectCamera } from './camera-bridge.js';
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Capability → UI element map
|
|
38
|
+
// Maps capability string → array of DOM element IDs to show when present.
|
|
39
|
+
// Elements NOT in any capability's list are hidden by default.
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const CAPABILITY_UI_MAP = {
|
|
43
|
+
canvas: ['canvas-button'],
|
|
44
|
+
music: ['music-button'],
|
|
45
|
+
wake_word: ['wake-button'],
|
|
46
|
+
camera: ['camera-button'],
|
|
47
|
+
caller_effects: ['caller-effect-toggle'],
|
|
48
|
+
dj_soundboard: [], // no dedicated button, soundboard is always-present
|
|
49
|
+
face_panel: ['face-button'],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// All capability-controlled element IDs (used to hide everything first)
|
|
53
|
+
const ALL_CAPABILITY_ELEMENTS = [
|
|
54
|
+
...new Set(Object.values(CAPABILITY_UI_MAP).flat()),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
// AgentOrchestrator
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
class AgentOrchestrator {
|
|
62
|
+
constructor() {
|
|
63
|
+
/** @type {Object.<string, {adapter: object, config: object}>} */
|
|
64
|
+
this._adapters = {};
|
|
65
|
+
|
|
66
|
+
/** @type {object|null} */
|
|
67
|
+
this._activeAdapter = null;
|
|
68
|
+
|
|
69
|
+
/** @type {string|null} */
|
|
70
|
+
this._activeMode = null;
|
|
71
|
+
|
|
72
|
+
/** @type {Function[]} shell bridge unsubscribe functions */
|
|
73
|
+
this._shellConnections = [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register an adapter with its default config.
|
|
78
|
+
* Must be called before switchMode() can select this adapter.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} mode - Unique mode key (e.g. 'clawdbot', 'elevenlabs-classic')
|
|
81
|
+
* @param {object} adapter - Adapter object with init/start/stop/destroy methods
|
|
82
|
+
* @param {object} config - Adapter-specific configuration
|
|
83
|
+
*/
|
|
84
|
+
register(mode, adapter, config = {}) {
|
|
85
|
+
this._adapters[mode] = { adapter, config };
|
|
86
|
+
console.log(`[Orchestrator] Registered adapter: ${mode}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Switch the active agent adapter.
|
|
91
|
+
* Performs full teardown of the current adapter, clears all bridge
|
|
92
|
+
* handlers, initialises the new adapter, reconnects shell bridges,
|
|
93
|
+
* and updates UI visibility based on the new adapter's capabilities.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} newMode - Mode key to switch to
|
|
96
|
+
* @param {object} [configOverride] - Optional config overrides for this switch
|
|
97
|
+
*/
|
|
98
|
+
async switchMode(newMode, configOverride = {}) {
|
|
99
|
+
const entry = this._adapters[newMode];
|
|
100
|
+
if (!entry) {
|
|
101
|
+
console.error(`[Orchestrator] Unknown mode: ${newMode}`);
|
|
102
|
+
throw new Error(`Unknown agent mode: ${newMode}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 1. Tear down current adapter completely
|
|
106
|
+
if (this._activeAdapter) {
|
|
107
|
+
console.log(`[Orchestrator] Destroying ${this._activeMode}`);
|
|
108
|
+
try {
|
|
109
|
+
await this._activeAdapter.destroy();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.warn('[Orchestrator] Error during adapter destroy:', e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Disconnect all shell bridge subscriptions (prevent stale handlers)
|
|
116
|
+
this._shellConnections.forEach(unsub => {
|
|
117
|
+
try { unsub(); } catch (e) { /* ignore */ }
|
|
118
|
+
});
|
|
119
|
+
this._shellConnections = [];
|
|
120
|
+
|
|
121
|
+
// 3. Clear ALL bridge handlers (nuclear clear — guarantees no leaks)
|
|
122
|
+
bridge.clearAll();
|
|
123
|
+
|
|
124
|
+
// 4. Initialise new adapter
|
|
125
|
+
console.log(`[Orchestrator] Initialising ${newMode}`);
|
|
126
|
+
this._activeAdapter = entry.adapter;
|
|
127
|
+
this._activeMode = newMode;
|
|
128
|
+
|
|
129
|
+
const config = { ...entry.config, ...configOverride };
|
|
130
|
+
await entry.adapter.init(bridge, config);
|
|
131
|
+
|
|
132
|
+
// 5. Reconnect shell modules (always-on bridges)
|
|
133
|
+
const caps = entry.adapter.capabilities || [];
|
|
134
|
+
|
|
135
|
+
this._shellConnections.push(
|
|
136
|
+
...connectFace(bridge),
|
|
137
|
+
...connectWaveform(bridge),
|
|
138
|
+
...connectTranscript(bridge),
|
|
139
|
+
...connectActionConsole(bridge),
|
|
140
|
+
...connectMusic(bridge),
|
|
141
|
+
...connectSounds(bridge),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// 6. Conditional bridges — only connect when adapter has the capability
|
|
145
|
+
this._shellConnections.push(
|
|
146
|
+
...connectCallerEffect(bridge, caps),
|
|
147
|
+
...connectCanvas(bridge, caps),
|
|
148
|
+
...connectCommercial(bridge, caps),
|
|
149
|
+
...connectCamera(bridge, caps),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// 7. Update UI visibility based on capabilities
|
|
153
|
+
this._updateFeatureUI(caps);
|
|
154
|
+
|
|
155
|
+
// 8. Persist selection
|
|
156
|
+
try { localStorage.setItem('agent_mode', newMode); } catch (e) { /* ignore */ }
|
|
157
|
+
|
|
158
|
+
console.log(`[Orchestrator] Mode switched to: ${newMode} (caps: [${caps.join(', ')}])`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Start a conversation with the active adapter.
|
|
163
|
+
*/
|
|
164
|
+
async startConversation() {
|
|
165
|
+
if (!this._activeAdapter) {
|
|
166
|
+
console.warn('[Orchestrator] No active adapter — call switchMode() first');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await this._activeAdapter.start();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stop the active conversation (but keep adapter alive).
|
|
174
|
+
*/
|
|
175
|
+
async stopConversation() {
|
|
176
|
+
if (this._activeAdapter) {
|
|
177
|
+
await this._activeAdapter.stop();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Return the active adapter's capability list.
|
|
183
|
+
* @returns {string[]}
|
|
184
|
+
*/
|
|
185
|
+
get capabilities() {
|
|
186
|
+
return this._activeAdapter?.capabilities ?? [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Return the active mode key.
|
|
191
|
+
* @returns {string|null}
|
|
192
|
+
*/
|
|
193
|
+
get activeMode() {
|
|
194
|
+
return this._activeMode;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Show/hide UI elements based on what the active adapter supports.
|
|
199
|
+
* Capability-controlled elements are hidden first, then shown for
|
|
200
|
+
* each capability the adapter declares.
|
|
201
|
+
*
|
|
202
|
+
* @param {string[]} capabilities - Capabilities of the active adapter
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
_updateFeatureUI(capabilities) {
|
|
206
|
+
// Hide all capability-controlled elements first
|
|
207
|
+
ALL_CAPABILITY_ELEMENTS.forEach(id => {
|
|
208
|
+
const el = document.getElementById(id);
|
|
209
|
+
if (el) el.style.display = 'none';
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Show elements for each declared capability
|
|
213
|
+
capabilities.forEach(cap => {
|
|
214
|
+
const ids = CAPABILITY_UI_MAP[cap] ?? [];
|
|
215
|
+
ids.forEach(id => {
|
|
216
|
+
const el = document.getElementById(id);
|
|
217
|
+
if (el) el.style.display = '';
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Emit capability list for any shell components that inspect it
|
|
222
|
+
console.log(`[Orchestrator] UI updated for capabilities: [${capabilities.join(', ')}]`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
// Singleton — one orchestrator for the whole app
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
export const orchestrator = new AgentOrchestrator();
|
|
231
|
+
|
|
232
|
+
// Also export class for testing
|
|
233
|
+
export { AgentOrchestrator };
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile-discovery.js — Adapter auto-discovery from profiles (P6-T5)
|
|
3
|
+
*
|
|
4
|
+
* ProfileDiscovery fetches agent profiles from the server, resolves which
|
|
5
|
+
* adapter each profile requires, registers every profile as an orchestrator
|
|
6
|
+
* mode, and activates the initial mode (from localStorage or server default).
|
|
7
|
+
*
|
|
8
|
+
* This implements the "plug-and-play" promise from the multi-agent framework:
|
|
9
|
+
* Adding a new agent = 1 adapter file + 1 profile JSON entry.
|
|
10
|
+
* ProfileDiscovery does the rest automatically.
|
|
11
|
+
*
|
|
12
|
+
* Integration:
|
|
13
|
+
* 1. Call `await profileDiscovery.init({ serverUrl })` at app startup.
|
|
14
|
+
* 2. All profiles → orchestrator modes are registered automatically.
|
|
15
|
+
* 3. ProfileSwitcher's 'profile:switched' event triggers adapter hot-swap.
|
|
16
|
+
*
|
|
17
|
+
* Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md
|
|
18
|
+
* "Adding a New Agent System (The Plug-and-Play Promise)"
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* import { profileDiscovery } from './profile-discovery.js';
|
|
22
|
+
* await profileDiscovery.init({ serverUrl: 'http://localhost:5001' });
|
|
23
|
+
* // Orchestrator is now registered with all adapters — call switchMode, start, etc.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { orchestrator } from './orchestrator.js';
|
|
27
|
+
import { adapterRegistry } from './adapter-registry.js';
|
|
28
|
+
import { eventBus } from '../core/EventBus.js';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// ProfileDiscovery
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
class ProfileDiscovery {
|
|
35
|
+
constructor() {
|
|
36
|
+
/** @type {string} */
|
|
37
|
+
this._serverUrl = '';
|
|
38
|
+
|
|
39
|
+
/** @type {object[]} — Raw profile objects from /api/profiles */
|
|
40
|
+
this._profiles = [];
|
|
41
|
+
|
|
42
|
+
/** @type {string|null} — Active profile ID reported by the server */
|
|
43
|
+
this._serverActiveId = null;
|
|
44
|
+
|
|
45
|
+
/** @type {boolean} */
|
|
46
|
+
this._initialized = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch profiles, load adapter modules, register each profile as an
|
|
53
|
+
* orchestrator mode, and activate the initial mode.
|
|
54
|
+
*
|
|
55
|
+
* Errors in individual adapter registrations are non-fatal: other adapters
|
|
56
|
+
* are still registered. A total failure falls back to the clawdbot adapter.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} options
|
|
59
|
+
* @param {string} options.serverUrl - Base URL of the backend
|
|
60
|
+
* @param {string} [options.fallbackMode] - Adapter ID used if everything fails
|
|
61
|
+
*/
|
|
62
|
+
async init({ serverUrl = '', fallbackMode = 'clawdbot' } = {}) {
|
|
63
|
+
this._serverUrl = serverUrl;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await this._fetchProfiles();
|
|
67
|
+
await this._registerAllAdapters();
|
|
68
|
+
await this._activateInitialMode(fallbackMode);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('[ProfileDiscovery] Init failed:', err);
|
|
71
|
+
// Best-effort fallback: register and activate clawdbot directly
|
|
72
|
+
try {
|
|
73
|
+
await this._registerFallback(fallbackMode);
|
|
74
|
+
await orchestrator.switchMode(fallbackMode);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error('[ProfileDiscovery] Fallback registration also failed:', e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this._initialized = true;
|
|
81
|
+
|
|
82
|
+
// Listen for profile-switch events emitted by ProfileSwitcher UI
|
|
83
|
+
eventBus.on('profile:switched', (d) => this._onProfileSwitched(d.profile));
|
|
84
|
+
|
|
85
|
+
console.log('[ProfileDiscovery] Initialised. Known modes:', Object.keys(orchestrator._adapters));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return the raw profile list fetched from the server.
|
|
90
|
+
* @returns {object[]}
|
|
91
|
+
*/
|
|
92
|
+
get profiles() {
|
|
93
|
+
return this._profiles;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return the currently active profile ID.
|
|
98
|
+
* Reads from the orchestrator's active mode (source of truth at runtime).
|
|
99
|
+
* @returns {string|null}
|
|
100
|
+
*/
|
|
101
|
+
get activeId() {
|
|
102
|
+
return orchestrator.activeMode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Return true after init() has completed (successfully or via fallback).
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
get initialized() {
|
|
110
|
+
return this._initialized;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Private: bootstrap ────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetch /api/profiles and store the result.
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
async _fetchProfiles() {
|
|
120
|
+
const resp = await fetch(`${this._serverUrl}/api/profiles`);
|
|
121
|
+
if (!resp.ok) {
|
|
122
|
+
throw new Error(`/api/profiles returned HTTP ${resp.status}`);
|
|
123
|
+
}
|
|
124
|
+
const data = await resp.json();
|
|
125
|
+
this._profiles = data.profiles || [];
|
|
126
|
+
this._serverActiveId = data.active || null;
|
|
127
|
+
|
|
128
|
+
console.log(
|
|
129
|
+
`[ProfileDiscovery] Fetched ${this._profiles.length} profile(s),`,
|
|
130
|
+
`server active: "${this._serverActiveId}"`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* For each profile, dynamically import its adapter and register the
|
|
136
|
+
* profile as an orchestrator mode.
|
|
137
|
+
*
|
|
138
|
+
* Profile → orchestrator mode mapping:
|
|
139
|
+
* - mode key = profile.id (e.g. 'default', 'hume-evi')
|
|
140
|
+
* - adapter = profile.adapter (e.g. 'clawdbot', 'hume-evi')
|
|
141
|
+
* - config = profile.adapter_config (adapter-specific params from JSON)
|
|
142
|
+
* plus serverUrl and profileId injected automatically.
|
|
143
|
+
*
|
|
144
|
+
* Adapter modules are cached by AdapterRegistry, so multiple profiles
|
|
145
|
+
* sharing the same adapter (e.g. three ClawdBot profiles) only trigger
|
|
146
|
+
* one dynamic import.
|
|
147
|
+
*
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
async _registerAllAdapters() {
|
|
151
|
+
for (const profile of this._profiles) {
|
|
152
|
+
const adapterId = profile.adapter || adapterRegistry.defaultAdapter;
|
|
153
|
+
const modeKey = profile.id;
|
|
154
|
+
const config = {
|
|
155
|
+
...(profile.adapter_config || {}),
|
|
156
|
+
// Always inject serverUrl so adapters don't need to hardcode it
|
|
157
|
+
serverUrl: (profile.adapter_config?.serverUrl) || this._serverUrl,
|
|
158
|
+
// profileId lets adapters read the full profile server-side if needed
|
|
159
|
+
profileId: profile.id,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const adapter = await adapterRegistry.load(adapterId);
|
|
164
|
+
orchestrator.register(modeKey, adapter, config);
|
|
165
|
+
console.log(
|
|
166
|
+
`[ProfileDiscovery] Registered mode "${modeKey}"`,
|
|
167
|
+
`→ adapter "${adapterId}"`
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn(
|
|
171
|
+
`[ProfileDiscovery] Could not register mode "${modeKey}"`,
|
|
172
|
+
`(adapter "${adapterId}"):`, err.message
|
|
173
|
+
);
|
|
174
|
+
// Continue with remaining profiles — non-fatal
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Decide which mode to activate at startup.
|
|
181
|
+
*
|
|
182
|
+
* Priority:
|
|
183
|
+
* 1. localStorage 'agent_mode' (user's last manual selection)
|
|
184
|
+
* 2. Server-reported active profile
|
|
185
|
+
* 3. fallbackMode parameter
|
|
186
|
+
*
|
|
187
|
+
* @param {string} fallbackMode
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
async _activateInitialMode(fallbackMode) {
|
|
191
|
+
const saved = this._readSavedMode();
|
|
192
|
+
const target = saved || this._serverActiveId || fallbackMode;
|
|
193
|
+
|
|
194
|
+
// Make sure target mode is registered (saved mode might be stale)
|
|
195
|
+
const registered = Object.keys(orchestrator._adapters);
|
|
196
|
+
const resolvedTarget = registered.includes(target)
|
|
197
|
+
? target
|
|
198
|
+
: (this._serverActiveId || fallbackMode);
|
|
199
|
+
|
|
200
|
+
if (resolvedTarget !== target) {
|
|
201
|
+
console.warn(
|
|
202
|
+
`[ProfileDiscovery] Saved mode "${target}" not registered;`,
|
|
203
|
+
`falling back to "${resolvedTarget}"`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await orchestrator.switchMode(resolvedTarget);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(
|
|
211
|
+
`[ProfileDiscovery] Could not activate mode "${resolvedTarget}":`, err
|
|
212
|
+
);
|
|
213
|
+
// Last resort: try the raw fallback
|
|
214
|
+
if (resolvedTarget !== fallbackMode && registered.includes(fallbackMode)) {
|
|
215
|
+
await orchestrator.switchMode(fallbackMode);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Register the fallback adapter without needing a profile entry.
|
|
222
|
+
* Used when _fetchProfiles fails entirely.
|
|
223
|
+
* @param {string} adapterId
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
async _registerFallback(adapterId) {
|
|
227
|
+
const adapter = await adapterRegistry.load(adapterId);
|
|
228
|
+
orchestrator.register(adapterId, adapter, { serverUrl: this._serverUrl });
|
|
229
|
+
console.log(`[ProfileDiscovery] Registered fallback mode "${adapterId}"`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Read the saved agent mode from localStorage, returning null on error.
|
|
234
|
+
* @returns {string|null}
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
_readSavedMode() {
|
|
238
|
+
try {
|
|
239
|
+
return localStorage.getItem('agent_mode') || null;
|
|
240
|
+
} catch (_) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Private: runtime profile switching ───────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Called when the ProfileSwitcher UI emits 'profile:switched'.
|
|
249
|
+
* Switches the orchestrator to the new profile's adapter mode.
|
|
250
|
+
*
|
|
251
|
+
* If the new profile was not in the initial fetch (e.g. dynamically
|
|
252
|
+
* created), it is registered on-the-fly before switching.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} profile - The newly-activated profile object from the server
|
|
255
|
+
* @private
|
|
256
|
+
*/
|
|
257
|
+
async _onProfileSwitched(profile) {
|
|
258
|
+
if (!profile?.id) return;
|
|
259
|
+
|
|
260
|
+
const modeKey = profile.id;
|
|
261
|
+
const adapterId = profile.adapter || adapterRegistry.defaultAdapter;
|
|
262
|
+
|
|
263
|
+
console.log(
|
|
264
|
+
`[ProfileDiscovery] 'profile:switched' → mode "${modeKey}"`,
|
|
265
|
+
`adapter "${adapterId}"`
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Register on-the-fly if not already known (handles dynamically-created profiles)
|
|
269
|
+
if (!orchestrator._adapters[modeKey]) {
|
|
270
|
+
try {
|
|
271
|
+
const adapter = await adapterRegistry.load(adapterId);
|
|
272
|
+
orchestrator.register(modeKey, adapter, {
|
|
273
|
+
...(profile.adapter_config || {}),
|
|
274
|
+
serverUrl: this._serverUrl,
|
|
275
|
+
profileId: profile.id,
|
|
276
|
+
});
|
|
277
|
+
console.log(`[ProfileDiscovery] On-the-fly registered mode "${modeKey}"`);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(
|
|
280
|
+
`[ProfileDiscovery] Failed to register mode "${modeKey}" on-the-fly:`, err
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await orchestrator.switchMode(modeKey);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error(
|
|
290
|
+
`[ProfileDiscovery] Failed to switch orchestrator to "${modeKey}":`, err
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
297
|
+
// Singleton
|
|
298
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
export const profileDiscovery = new ProfileDiscovery();
|
|
301
|
+
|
|
302
|
+
// Also export class for testing
|
|
303
|
+
export { ProfileDiscovery };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sounds-bridge.js — Connect DJ/caller sounds to EventBridge (P6-T4)
|
|
3
|
+
*
|
|
4
|
+
* Routes PLAY_SOUND events to the DJ soundboard or caller sound player.
|
|
5
|
+
*
|
|
6
|
+
* Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AgentEvents } from '../core/EventBridge.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wire sound effects to the EventBridge.
|
|
13
|
+
* @param {import('../core/EventBridge.js').EventBridge} bridge
|
|
14
|
+
* @returns {Function[]} unsubscribe functions
|
|
15
|
+
*/
|
|
16
|
+
export function connectSounds(bridge) {
|
|
17
|
+
const unsubs = [
|
|
18
|
+
bridge.on(AgentEvents.PLAY_SOUND, ({ sound, type }) => {
|
|
19
|
+
if (type === 'dj') {
|
|
20
|
+
window.DJSoundboard?.play(sound);
|
|
21
|
+
} else if (type === 'caller') {
|
|
22
|
+
window.playCallerSound?.(sound);
|
|
23
|
+
}
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
return unsubs;
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transcript-bridge.js — Connect TranscriptPanel and ActionConsole to EventBridge (P6-T4)
|
|
3
|
+
*
|
|
4
|
+
* Routes message/transcript events to the transcript panel and
|
|
5
|
+
* tool/lifecycle events to the action console.
|
|
6
|
+
*
|
|
7
|
+
* Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AgentEvents } from '../core/EventBridge.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wire TranscriptPanel to the EventBridge.
|
|
14
|
+
* @param {import('../core/EventBridge.js').EventBridge} bridge
|
|
15
|
+
* @returns {Function[]} unsubscribe functions
|
|
16
|
+
*/
|
|
17
|
+
export function connectTranscript(bridge) {
|
|
18
|
+
const tp = () => window.TranscriptPanel;
|
|
19
|
+
|
|
20
|
+
const unsubs = [
|
|
21
|
+
bridge.on(AgentEvents.MESSAGE, ({ role, text, final }) => {
|
|
22
|
+
if (!tp()) return;
|
|
23
|
+
if (final) tp().addMessage(role, text);
|
|
24
|
+
else tp().updateStreaming?.(text);
|
|
25
|
+
}),
|
|
26
|
+
bridge.on(AgentEvents.TRANSCRIPT, ({ text }) => {
|
|
27
|
+
tp()?.updateStreaming?.(text);
|
|
28
|
+
}),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
return unsubs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wire ActionConsole to the EventBridge.
|
|
36
|
+
* @param {import('../core/EventBridge.js').EventBridge} bridge
|
|
37
|
+
* @returns {Function[]} unsubscribe functions
|
|
38
|
+
*/
|
|
39
|
+
export function connectActionConsole(bridge) {
|
|
40
|
+
const ac = () => window.ActionConsole;
|
|
41
|
+
|
|
42
|
+
const unsubs = [
|
|
43
|
+
bridge.on(AgentEvents.TOOL_CALLED, ({ name, result }) => {
|
|
44
|
+
ac()?.addEntry('tool', name, JSON.stringify(result ?? ''));
|
|
45
|
+
}),
|
|
46
|
+
bridge.on(AgentEvents.CONNECTED, () => {
|
|
47
|
+
ac()?.addEntry('lifecycle', 'Agent connected');
|
|
48
|
+
}),
|
|
49
|
+
bridge.on(AgentEvents.DISCONNECTED, () => {
|
|
50
|
+
ac()?.addEntry('lifecycle', 'Agent disconnected');
|
|
51
|
+
}),
|
|
52
|
+
bridge.on(AgentEvents.ERROR, ({ message }) => {
|
|
53
|
+
ac()?.addEntry('error', `Agent error: ${message}`);
|
|
54
|
+
}),
|
|
55
|
+
bridge.on(AgentEvents.STATE_CHANGED, ({ state }) => {
|
|
56
|
+
ac()?.addEntry('system', `State: ${state}`);
|
|
57
|
+
}),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return unsubs;
|
|
61
|
+
}
|