apteva 0.4.53 → 0.4.56
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/dist/ActivityPage.kxzzb4yc.js +3 -0
- package/dist/ApiDocsPage.zq998hbm.js +4 -0
- package/dist/App.55rea8mn.js +61 -0
- package/dist/App.5ywb23z4.js +53 -0
- package/dist/App.6thds120.js +4 -0
- package/dist/{App.jhb45d7r.js → App.9tctxzqm.js} +3 -3
- package/dist/App.a8r8ttaz.js +4 -0
- package/dist/App.agsv5bje.js +4 -0
- package/dist/App.cepapqmx.js +4 -0
- package/dist/App.dp041gb3.js +221 -0
- package/dist/App.fds72zb5.js +4 -0
- package/dist/App.fg9qj2dq.js +4 -0
- package/dist/App.ndfejbm9.js +4 -0
- package/dist/App.nxmfmq1h.js +13 -0
- package/dist/App.qdfyt8ba.js +4 -0
- package/dist/{App.9sryp183.js → App.x2d0ygt6.js} +2 -2
- package/dist/App.yt9p4nr3.js +20 -0
- package/dist/{App.wghtdzsk.js → App.zn4mw16t.js} +1 -1
- package/dist/ConnectionsPage.8r96ryw7.js +3 -0
- package/dist/McpPage.3cwh0gnd.js +3 -0
- package/dist/SettingsPage.ykgdh5ev.js +3 -0
- package/dist/SkillsPage.4np1s65b.js +3 -0
- package/dist/TasksPage.4g08t7p6.js +3 -0
- package/dist/TelemetryPage.72w9pwcp.js +3 -0
- package/dist/TestsPage.z4fk3r7r.js +3 -0
- package/dist/ThreadsPage.63tcajeh.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/crypto.ts +25 -4
- package/src/db.ts +24 -1
- package/src/mcp-platform.ts +273 -44
- package/src/providers.ts +125 -5
- package/src/routes/api/agent-utils.ts +105 -8
- package/src/routes/api/providers.ts +64 -0
- package/src/routes/api/telemetry.ts +0 -7
- package/src/routes/share.ts +3 -2
- package/src/server.ts +53 -7
- package/src/test-runner.ts +1 -1
- package/src/web/App.tsx +37 -22
- package/src/web/components/agents/AgentCard.tsx +12 -9
- package/src/web/components/agents/AgentPanel.tsx +126 -7
- package/src/web/components/agents/AgentsView.tsx +30 -8
- package/src/web/components/agents/CreateAgentModal.tsx +155 -5
- package/src/web/components/dashboard/Dashboard.tsx +9 -7
- package/src/web/components/layout/Sidebar.tsx +43 -32
- package/src/web/components/meta-agent/MetaAgent.tsx +6 -2
- package/src/web/components/settings/SettingsPage.tsx +172 -43
- package/src/web/components/telemetry/TelemetryPage.tsx +54 -46
- package/src/web/components/tests/TestsPage.tsx +91 -76
- package/src/web/context/TelemetryContext.tsx +4 -1
- package/src/web/context/UIModeContext.tsx +49 -0
- package/src/web/context/index.ts +3 -0
- package/src/web/types.ts +67 -3
- package/dist/ActivityPage.sw9p594m.js +0 -3
- package/dist/ApiDocsPage.90e03bz7.js +0 -4
- package/dist/App.3vnrera5.js +0 -4
- package/dist/App.94x6mh7f.js +0 -20
- package/dist/App.9t1zc5r7.js +0 -53
- package/dist/App.p7jjw1zf.js +0 -4
- package/dist/App.pfbdzrhh.js +0 -4
- package/dist/App.pse0pzar.js +0 -4
- package/dist/App.r43t58w6.js +0 -221
- package/dist/App.stgng5bx.js +0 -13
- package/dist/App.tm3k7h4b.js +0 -4
- package/dist/App.vkg121c6.js +0 -4
- package/dist/App.xva0tfzh.js +0 -4
- package/dist/App.ysxy7akk.js +0 -61
- package/dist/App.yzkh4gq2.js +0 -4
- package/dist/ConnectionsPage.q5f9fd37.js +0 -3
- package/dist/McpPage.f3ccrezb.js +0 -3
- package/dist/SettingsPage.zmzm1pp6.js +0 -3
- package/dist/SkillsPage.whxnez67.js +0 -3
- package/dist/TasksPage.zp4jfevw.js +0 -3
- package/dist/TelemetryPage.an0ky78c.js +0 -3
- package/dist/TestsPage.18krj0d1.js +0 -3
- package/dist/ThreadsPage.nnphgy98.js +0 -3
|
@@ -145,6 +145,72 @@ function buildOperatorConfig(features: Agent["features"], projectId: string | nu
|
|
|
145
145
|
return operatorResult;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function buildRealtimeConfig(features: Agent["features"], projectId: string | null) {
|
|
149
|
+
// Backwards compatible: handle both boolean and RealtimeConfig
|
|
150
|
+
const realtimeVal = features.realtime;
|
|
151
|
+
const isEnabled = typeof realtimeVal === "boolean" ? realtimeVal : (realtimeVal as any)?.enabled ?? false;
|
|
152
|
+
|
|
153
|
+
if (!isEnabled) {
|
|
154
|
+
return { enabled: false, provider: "standard", stt: {}, tts: {} };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const rtConfig = typeof realtimeVal === "object" ? realtimeVal as Record<string, unknown> : {};
|
|
158
|
+
const rtProvider = (rtConfig.provider as string) || "standard";
|
|
159
|
+
|
|
160
|
+
// Native OpenAI Realtime mode
|
|
161
|
+
if (rtProvider === "openai") {
|
|
162
|
+
return {
|
|
163
|
+
enabled: true,
|
|
164
|
+
provider: "openai",
|
|
165
|
+
model: (rtConfig.model as string) || "gpt-realtime",
|
|
166
|
+
voice: (rtConfig.voice as string) || "alloy",
|
|
167
|
+
vad_type: (rtConfig.vadType as string) || "semantic_vad",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Native Gemini Live mode
|
|
172
|
+
if (rtProvider === "gemini") {
|
|
173
|
+
return {
|
|
174
|
+
enabled: true,
|
|
175
|
+
provider: "gemini",
|
|
176
|
+
gemini_model: (rtConfig.geminiModel as string) || "",
|
|
177
|
+
gemini_voice: (rtConfig.geminiVoice as string) || "Kore",
|
|
178
|
+
google_search: (rtConfig.googleSearch as boolean) || false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Standard mode: STT → Core LLM → TTS pipeline
|
|
183
|
+
const sttProvider = (rtConfig.sttProvider as string) || "elevenlabs";
|
|
184
|
+
const sttModel = (rtConfig.sttModel as string) || (sttProvider === "elevenlabs" ? "scribe_v2_realtime" : undefined);
|
|
185
|
+
const ttsProvider = (rtConfig.ttsProvider as string) || "elevenlabs";
|
|
186
|
+
const ttsModel = (rtConfig.ttsModel as string) || undefined;
|
|
187
|
+
|
|
188
|
+
const sttProviderDef = PROVIDERS[sttProvider as ProviderId];
|
|
189
|
+
const ttsProviderDef = PROVIDERS[ttsProvider as ProviderId];
|
|
190
|
+
|
|
191
|
+
const sttConfig: Record<string, unknown> = { provider: sttProvider };
|
|
192
|
+
if (sttModel) sttConfig.model = sttModel;
|
|
193
|
+
// For local providers, include the base URL in the config
|
|
194
|
+
if (sttProviderDef && "isLocal" in sttProviderDef && sttProviderDef.isLocal) {
|
|
195
|
+
sttConfig.base_url = ProviderKeys.getDecryptedForProject(sttProvider, projectId) ||
|
|
196
|
+
("defaultBaseUrl" in sttProviderDef ? (sttProviderDef as any).defaultBaseUrl : "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ttsConfig: Record<string, unknown> = { provider: ttsProvider };
|
|
200
|
+
if (ttsModel) ttsConfig.model = ttsModel;
|
|
201
|
+
if (ttsProviderDef && "isLocal" in ttsProviderDef && ttsProviderDef.isLocal) {
|
|
202
|
+
ttsConfig.base_url = ProviderKeys.getDecryptedForProject(ttsProvider, projectId) ||
|
|
203
|
+
("defaultBaseUrl" in ttsProviderDef ? (ttsProviderDef as any).defaultBaseUrl : "");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
enabled: true,
|
|
208
|
+
provider: "standard",
|
|
209
|
+
stt: sttConfig,
|
|
210
|
+
tts: ttsConfig,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
148
214
|
// Build agent config from apteva agent data
|
|
149
215
|
// Note: POST /config expects flat structure WITHOUT "agent" wrapper
|
|
150
216
|
export function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
@@ -302,12 +368,7 @@ export function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
302
368
|
cache_ttl: "15m",
|
|
303
369
|
servers: mcpServers,
|
|
304
370
|
},
|
|
305
|
-
realtime:
|
|
306
|
-
enabled: features.realtime,
|
|
307
|
-
provider: "openai",
|
|
308
|
-
model: "gpt-4o-realtime-preview",
|
|
309
|
-
voice: "alloy",
|
|
310
|
-
},
|
|
371
|
+
realtime: buildRealtimeConfig(features, agent.project_id),
|
|
311
372
|
context: {
|
|
312
373
|
max_messages: 30,
|
|
313
374
|
max_tokens: 0,
|
|
@@ -565,6 +626,41 @@ export async function startAgentProcess(
|
|
|
565
626
|
}
|
|
566
627
|
}
|
|
567
628
|
|
|
629
|
+
// If realtime voice is enabled, pass voice provider keys/URLs for STT/TTS
|
|
630
|
+
const rtEnabled = typeof agent.features.realtime === "boolean"
|
|
631
|
+
? agent.features.realtime
|
|
632
|
+
: (agent.features.realtime as any)?.enabled ?? false;
|
|
633
|
+
if (rtEnabled) {
|
|
634
|
+
// Cloud voice provider keys (backwards compat — always pass if available)
|
|
635
|
+
const elevenlabsKey = ProviderKeys.getDecryptedForProject("elevenlabs", agent.project_id);
|
|
636
|
+
if (elevenlabsKey) env.ELEVENLABS_API_KEY = elevenlabsKey;
|
|
637
|
+
const deepgramKey = ProviderKeys.getDecryptedForProject("deepgram", agent.project_id);
|
|
638
|
+
if (deepgramKey) env.DEEPGRAM_API_KEY = deepgramKey;
|
|
639
|
+
|
|
640
|
+
// Local voice provider URLs
|
|
641
|
+
const localVoiceProviders = ["speaches", "whisper_cpp", "kokoro", "piper", "fish_speech"] as const;
|
|
642
|
+
for (const pvId of localVoiceProviders) {
|
|
643
|
+
const pv = PROVIDERS[pvId as ProviderId];
|
|
644
|
+
if (pv) {
|
|
645
|
+
const url = ProviderKeys.getDecryptedForProject(pvId, agent.project_id);
|
|
646
|
+
if (url) env[pv.envVar] = url;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Pass API keys for native realtime providers (OpenAI/Gemini)
|
|
651
|
+
// Agent may use a different main LLM provider (e.g., Claude) but needs these for voice
|
|
652
|
+
const rtConfig = typeof agent.features.realtime === "object" ? agent.features.realtime as Record<string, unknown> : {};
|
|
653
|
+
const rtProvider = rtConfig.provider as string;
|
|
654
|
+
if (rtProvider === "openai" && agent.provider !== "openai") {
|
|
655
|
+
const openaiKey = ProviderKeys.getDecryptedForProject("openai", agent.project_id);
|
|
656
|
+
if (openaiKey) env.OPENAI_API_KEY = openaiKey;
|
|
657
|
+
}
|
|
658
|
+
if (rtProvider === "gemini" && agent.provider !== "gemini") {
|
|
659
|
+
const geminiKey = ProviderKeys.getDecryptedForProject("gemini", agent.project_id);
|
|
660
|
+
if (geminiKey) env.GEMINI_API_KEY = geminiKey;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
568
664
|
// Get binary path dynamically (allows hot-reload of new binary versions)
|
|
569
665
|
const binaryPath = getBinaryPathForAgent();
|
|
570
666
|
|
|
@@ -618,8 +714,9 @@ export async function startAgentProcess(
|
|
|
618
714
|
console.log(` Pushing configuration...`);
|
|
619
715
|
}
|
|
620
716
|
const config = buildAgentConfig(agent, providerKey);
|
|
621
|
-
|
|
622
|
-
|
|
717
|
+
if (agent.features.realtime) {
|
|
718
|
+
console.log(`[DEBUG] realtime config being pushed:`, JSON.stringify(config.realtime, null, 2));
|
|
719
|
+
}
|
|
623
720
|
const configResult = await pushConfigToAgent(agent.id, port, config);
|
|
624
721
|
if (!configResult.success) {
|
|
625
722
|
if (!silent) {
|
|
@@ -172,6 +172,70 @@ export async function handleProviderRoutes(
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// ==================== LOCAL VOICE PROVIDERS ====================
|
|
176
|
+
|
|
177
|
+
// GET /api/providers/:id/status - Check if a local voice provider is running
|
|
178
|
+
const localVoiceStatusMatch = path.match(/^\/api\/providers\/(speaches|whisper_cpp|kokoro|piper|fish_speech)\/status$/);
|
|
179
|
+
if (localVoiceStatusMatch && method === "GET") {
|
|
180
|
+
const providerId = localVoiceStatusMatch[1];
|
|
181
|
+
const providerDef = PROVIDERS[providerId as ProviderId];
|
|
182
|
+
const baseUrl = ProviderKeys.getDecrypted(providerId) ||
|
|
183
|
+
("defaultBaseUrl" in providerDef ? (providerDef as any).defaultBaseUrl : "");
|
|
184
|
+
const isDocker = await isRunningInDocker();
|
|
185
|
+
|
|
186
|
+
const healthEndpoints: Record<string, string> = {
|
|
187
|
+
speaches: "/v1/models",
|
|
188
|
+
whisper_cpp: "/",
|
|
189
|
+
kokoro: "/v1/models",
|
|
190
|
+
piper: "/api/voices",
|
|
191
|
+
fish_speech: "/v1/models",
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(`${baseUrl}${healthEndpoints[providerId]}`, {
|
|
196
|
+
method: "GET",
|
|
197
|
+
signal: AbortSignal.timeout(3000),
|
|
198
|
+
});
|
|
199
|
+
if (response.ok) {
|
|
200
|
+
return json({ connected: true, url: baseUrl, isDocker });
|
|
201
|
+
}
|
|
202
|
+
return json({ connected: false, url: baseUrl, error: "Not responding", isDocker });
|
|
203
|
+
} catch {
|
|
204
|
+
return json({ connected: false, url: baseUrl, error: "Not reachable", isDocker });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// GET /api/providers/:id/models - Fetch available models from a local voice provider
|
|
209
|
+
const localVoiceModelsMatch = path.match(/^\/api\/providers\/(speaches|kokoro|fish_speech)\/models$/);
|
|
210
|
+
if (localVoiceModelsMatch && method === "GET") {
|
|
211
|
+
const providerId = localVoiceModelsMatch[1];
|
|
212
|
+
const providerDef = PROVIDERS[providerId as ProviderId];
|
|
213
|
+
const baseUrl = ProviderKeys.getDecrypted(providerId) ||
|
|
214
|
+
("defaultBaseUrl" in providerDef ? (providerDef as any).defaultBaseUrl : "");
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(`${baseUrl}/v1/models`, {
|
|
218
|
+
method: "GET",
|
|
219
|
+
headers: { "Accept": "application/json" },
|
|
220
|
+
signal: AbortSignal.timeout(5000),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
return json({ error: "Failed to connect", models: [], connected: false }, 200);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const data = await response.json() as { data?: Array<{ id: string }> };
|
|
228
|
+
const models = (data.data || []).map((m: { id: string }) => ({
|
|
229
|
+
value: m.id,
|
|
230
|
+
label: m.id,
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
return json({ models, connected: true });
|
|
234
|
+
} catch {
|
|
235
|
+
return json({ error: "Not reachable", models: [], connected: false }, 200);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
175
239
|
// ==================== ONBOARDING ====================
|
|
176
240
|
|
|
177
241
|
// GET /api/onboarding/status - Check onboarding status
|
|
@@ -34,13 +34,6 @@ export async function handleTelemetryRoutes(
|
|
|
34
34
|
return json({ error: "agent_id and events are required" }, 400);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Debug: log raw incoming events
|
|
38
|
-
for (const event of body.events) {
|
|
39
|
-
if (event.category === "LLM") {
|
|
40
|
-
console.log(`[telemetry] RAW LLM event from ${body.agent_id}: ${JSON.stringify(event)}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
37
|
// Filter out debug events - too noisy
|
|
45
38
|
const filteredEvents = body.events.filter(e => e.level !== "debug");
|
|
46
39
|
|
package/src/routes/share.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
-
import { AgentDB, type Agent } from "../db";
|
|
2
|
+
import { AgentDB, isRealtimeEnabled, type Agent } from "../db";
|
|
3
3
|
import { agentFetch } from "./api/agent-utils";
|
|
4
4
|
|
|
5
5
|
function deriveShareToken(apiKey: string, agentId: string): string {
|
|
@@ -9,7 +9,7 @@ function deriveShareToken(apiKey: string, agentId: string): string {
|
|
|
9
9
|
.substring(0, 32);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function findAgentByShareToken(token: string): Agent | null {
|
|
12
|
+
export function findAgentByShareToken(token: string): Agent | null {
|
|
13
13
|
const agents = AgentDB.findAll();
|
|
14
14
|
for (const agent of agents) {
|
|
15
15
|
const apiKey = AgentDB.getApiKey(agent.id);
|
|
@@ -46,6 +46,7 @@ export async function handleShareRequest(req: Request, path: string): Promise<Re
|
|
|
46
46
|
return Response.json({
|
|
47
47
|
name: agent.name,
|
|
48
48
|
status: agent.status,
|
|
49
|
+
voiceEnabled: isRealtimeEnabled(agent.features),
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
|
package/src/server.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type Server, type Subprocess } from "bun";
|
|
2
2
|
import { handleApiRequest } from "./routes/api";
|
|
3
3
|
import { handleAuthRequest } from "./routes/auth";
|
|
4
|
-
import { handleShareRequest } from "./routes/share";
|
|
4
|
+
import { handleShareRequest, findAgentByShareToken } from "./routes/share";
|
|
5
5
|
import { serveStatic } from "./routes/static";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { mkdirSync, existsSync } from "fs";
|
|
9
|
-
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, ChannelDB, TelemetryDB, type McpServer, type Agent } from "./db";
|
|
9
|
+
import { initDatabase, AgentDB, ApiKeyDB, ProviderKeysDB, McpServerDB, ChannelDB, TelemetryDB, UserDB, type McpServer, type Agent } from "./db";
|
|
10
10
|
import { authMiddleware, type AuthContext } from "./auth/middleware";
|
|
11
|
+
import { verifyAccessToken } from "./auth";
|
|
11
12
|
import { startMcpProcess } from "./mcp-client";
|
|
12
13
|
import {
|
|
13
14
|
ensureBinary,
|
|
@@ -371,12 +372,54 @@ const server = Bun.serve({
|
|
|
371
372
|
// WebSocket upgrade: /api/agents/:id/voice → proxy to agent binary
|
|
372
373
|
const voiceMatch = path.match(/^\/api\/agents\/([^/]+)\/voice$/);
|
|
373
374
|
if (voiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
375
|
+
// Validate auth via query param token (WebSocket can't send custom headers)
|
|
376
|
+
if (process.env.AUTH_ENABLED !== "false") {
|
|
377
|
+
const token = url.searchParams.get("token");
|
|
378
|
+
if (!token) {
|
|
379
|
+
return new Response("Authentication required", { status: 401 });
|
|
380
|
+
}
|
|
381
|
+
// Try API key first, then JWT
|
|
382
|
+
const isApiKey = token.startsWith("apt_");
|
|
383
|
+
if (isApiKey) {
|
|
384
|
+
const result = ApiKeyDB.validate(token);
|
|
385
|
+
if (!result) {
|
|
386
|
+
return new Response("Invalid API key", { status: 401 });
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
const payload = verifyAccessToken(token);
|
|
390
|
+
if (!payload) {
|
|
391
|
+
return new Response("Invalid or expired token", { status: 401 });
|
|
392
|
+
}
|
|
393
|
+
const user = UserDB.findById(payload.userId);
|
|
394
|
+
if (!user) {
|
|
395
|
+
return new Response("User not found", { status: 401 });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
374
400
|
const agentId = voiceMatch[1];
|
|
375
401
|
const agent = AgentDB.findById(agentId);
|
|
376
402
|
if (!agent || agent.status !== "running" || !agent.port) {
|
|
377
403
|
return new Response("Agent not available", { status: 400 });
|
|
378
404
|
}
|
|
379
|
-
const
|
|
405
|
+
const agentApiKey = AgentDB.getApiKey(agentId) || "";
|
|
406
|
+
console.log(`[WS] Voice upgrade: agent=${agentId} port=${agent.port} hasKey=${!!agentApiKey}`);
|
|
407
|
+
const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
|
|
408
|
+
if (upgraded) return undefined;
|
|
409
|
+
return new Response("WebSocket upgrade failed", { status: 500 });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// WebSocket upgrade: /share/<token>/voice → proxy to agent binary (public, share-token auth)
|
|
413
|
+
const shareVoiceMatch = path.match(/^\/share\/([a-f0-9]{32})\/voice$/);
|
|
414
|
+
if (shareVoiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
415
|
+
const shareToken = shareVoiceMatch[1];
|
|
416
|
+
const agent = findAgentByShareToken(shareToken);
|
|
417
|
+
if (!agent || agent.status !== "running" || !agent.port) {
|
|
418
|
+
return new Response("Agent not available", { status: 400 });
|
|
419
|
+
}
|
|
420
|
+
const agentApiKey = AgentDB.getApiKey(agent.id) || "";
|
|
421
|
+
console.log(`[WS] Share voice upgrade: agent=${agent.id} port=${agent.port}`);
|
|
422
|
+
const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
|
|
380
423
|
if (upgraded) return undefined;
|
|
381
424
|
return new Response("WebSocket upgrade failed", { status: 500 });
|
|
382
425
|
}
|
|
@@ -454,8 +497,10 @@ const server = Bun.serve({
|
|
|
454
497
|
// WebSocket proxy for agent voice/realtime
|
|
455
498
|
websocket: {
|
|
456
499
|
open(ws: any) {
|
|
457
|
-
const { agentId, agentPort } = ws.data;
|
|
458
|
-
const
|
|
500
|
+
const { agentId, agentPort, agentApiKey } = ws.data;
|
|
501
|
+
const wsTarget = `ws://localhost:${agentPort}/voice${agentApiKey ? `?api_key=${agentApiKey}` : ''}`;
|
|
502
|
+
console.log(`[WS] Voice proxy connecting: agent=${agentId} port=${agentPort}`);
|
|
503
|
+
const agentWs = new WebSocket(wsTarget);
|
|
459
504
|
|
|
460
505
|
agentWs.onopen = () => {
|
|
461
506
|
console.log(`[WS] Voice proxy connected: agent=${agentId} port=${agentPort}`);
|
|
@@ -464,10 +509,11 @@ const server = Bun.serve({
|
|
|
464
509
|
try { ws.send(event.data); } catch {}
|
|
465
510
|
};
|
|
466
511
|
agentWs.onclose = (event: CloseEvent) => {
|
|
467
|
-
console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code}`);
|
|
512
|
+
console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code} reason=${event.reason}`);
|
|
468
513
|
ws.close(event.code, event.reason);
|
|
469
514
|
};
|
|
470
|
-
agentWs.onerror = () => {
|
|
515
|
+
agentWs.onerror = (err: any) => {
|
|
516
|
+
console.log(`[WS] Agent WS error: agent=${agentId}`, err?.message || err);
|
|
471
517
|
ws.close(1011, "Agent WebSocket error");
|
|
472
518
|
};
|
|
473
519
|
|
package/src/test-runner.ts
CHANGED
|
@@ -241,7 +241,7 @@ export async function runTest(testCase: TestCase): Promise<TestRun> {
|
|
|
241
241
|
|
|
242
242
|
const chatRes = await agentFetch(runningAgent.id, runningAgent.port!, "/chat", {
|
|
243
243
|
method: "POST",
|
|
244
|
-
headers: { "Content-Type": "application/json" },
|
|
244
|
+
headers: { "Content-Type": "application/json", "X-Test-Mode": "true" },
|
|
245
245
|
body: JSON.stringify(chatBody),
|
|
246
246
|
});
|
|
247
247
|
|
package/src/web/App.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
-
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
+
import { Chat, Call } from "@apteva/apteva-kit";
|
|
4
4
|
import "@apteva/apteva-kit/styles.css";
|
|
5
5
|
|
|
6
6
|
// Types
|
|
@@ -8,7 +8,7 @@ import type { Agent, Provider, Route, NewAgentForm } from "./types";
|
|
|
8
8
|
import { DEFAULT_FEATURES } from "./types";
|
|
9
9
|
|
|
10
10
|
// Context
|
|
11
|
-
import { TelemetryProvider, AuthProvider, ProjectProvider, ThemeProvider, useTheme, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
|
|
11
|
+
import { TelemetryProvider, AuthProvider, ProjectProvider, ThemeProvider, UIModeProvider, useTheme, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
|
|
12
12
|
|
|
13
13
|
// Hooks
|
|
14
14
|
import { useAgents, useProviders, useOnboarding } from "./hooks";
|
|
@@ -348,6 +348,7 @@ function SharePage({ token }: { token: string }) {
|
|
|
348
348
|
const { theme } = useTheme();
|
|
349
349
|
const [status, setStatus] = useState<"checking" | "online" | "offline">("checking");
|
|
350
350
|
const [agentName, setAgentName] = useState("Agent");
|
|
351
|
+
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
351
352
|
|
|
352
353
|
useEffect(() => {
|
|
353
354
|
const check = async () => {
|
|
@@ -356,6 +357,7 @@ function SharePage({ token }: { token: string }) {
|
|
|
356
357
|
if (res.ok) {
|
|
357
358
|
const data = await res.json();
|
|
358
359
|
setAgentName(data.name || "Agent");
|
|
360
|
+
setVoiceEnabled(!!data.voiceEnabled);
|
|
359
361
|
setStatus(data.status === "running" ? "online" : "offline");
|
|
360
362
|
} else {
|
|
361
363
|
setStatus("offline");
|
|
@@ -392,17 +394,28 @@ function SharePage({ token }: { token: string }) {
|
|
|
392
394
|
return (
|
|
393
395
|
<div className="min-h-[100dvh] flex items-center justify-center p-0 md:p-4" style={{ backgroundColor: "var(--color-bg)" }}>
|
|
394
396
|
<div className="w-full max-w-[640px] h-[100dvh] md:h-[calc(100dvh-32px)] md:max-h-[800px] md:rounded-xl overflow-hidden md:border flex flex-col" style={{ backgroundColor: "var(--color-bg)", borderColor: "var(--color-border)" }}>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
397
|
+
{voiceEnabled ? (
|
|
398
|
+
<Call
|
|
399
|
+
agentId="default"
|
|
400
|
+
agentName={agentName}
|
|
401
|
+
apiUrl={`/share/${token}`}
|
|
402
|
+
variant="default"
|
|
403
|
+
theme={theme.id as "light" | "dark"}
|
|
404
|
+
showTranscript={false}
|
|
405
|
+
/>
|
|
406
|
+
) : (
|
|
407
|
+
<Chat
|
|
408
|
+
agentId="default"
|
|
409
|
+
apiUrl={`/share/${token}`}
|
|
410
|
+
placeholder="Type a message..."
|
|
411
|
+
variant="terminal"
|
|
412
|
+
theme={theme.id as "light" | "dark"}
|
|
413
|
+
headerTitle={agentName}
|
|
414
|
+
enableMarkdown
|
|
415
|
+
enableWidgets
|
|
416
|
+
availableWidgets={["form", "kpi"]}
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
406
419
|
</div>
|
|
407
420
|
</div>
|
|
408
421
|
);
|
|
@@ -422,15 +435,17 @@ function App() {
|
|
|
422
435
|
|
|
423
436
|
return (
|
|
424
437
|
<ThemeProvider>
|
|
425
|
-
<
|
|
426
|
-
<
|
|
427
|
-
<
|
|
428
|
-
<
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
438
|
+
<UIModeProvider>
|
|
439
|
+
<AuthProvider>
|
|
440
|
+
<ProjectProvider>
|
|
441
|
+
<MetaAgentProvider>
|
|
442
|
+
<TelemetryProvider>
|
|
443
|
+
<AppContent />
|
|
444
|
+
</TelemetryProvider>
|
|
445
|
+
</MetaAgentProvider>
|
|
446
|
+
</ProjectProvider>
|
|
447
|
+
</AuthProvider>
|
|
448
|
+
</UIModeProvider>
|
|
434
449
|
</ThemeProvider>
|
|
435
450
|
);
|
|
436
451
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, SkillsIcon, ActivityIcon } from "../common/Icons";
|
|
3
|
-
import { useAgentActivity, useProjects } from "../../context";
|
|
3
|
+
import { useAgentActivity, useProjects, useUIMode } from "../../context";
|
|
4
4
|
import type { Agent, AgentFeatures } from "../../types";
|
|
5
5
|
|
|
6
6
|
interface AgentCardProps {
|
|
@@ -28,6 +28,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
28
28
|
const skills = agent.skillDetails || [];
|
|
29
29
|
const { isActive, label: activityLabel } = useAgentActivity(agent.id);
|
|
30
30
|
const { projects } = useProjects();
|
|
31
|
+
const { isDev } = useUIMode();
|
|
31
32
|
const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
|
|
32
33
|
const subscriptions = agent.subscriptions || [];
|
|
33
34
|
|
|
@@ -43,10 +44,12 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
43
44
|
<div className="flex items-start justify-between mb-3">
|
|
44
45
|
<div>
|
|
45
46
|
<h3 className="font-semibold text-lg">{agent.name}</h3>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
{isDev && (
|
|
48
|
+
<p className="text-sm text-[var(--color-text-muted)]">
|
|
49
|
+
{agent.provider} / {agent.model}
|
|
50
|
+
{agent.port && <span className="text-[var(--color-text-faint)]"> · :{agent.port}</span>}
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
50
53
|
{showProject && project && (
|
|
51
54
|
<p className="text-sm text-[var(--color-text-muted)] flex items-center gap-1.5 mt-1">
|
|
52
55
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
|
|
@@ -57,7 +60,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
57
60
|
<StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityLabel={activityLabel} />
|
|
58
61
|
</div>
|
|
59
62
|
|
|
60
|
-
{enabledFeatures.length > 0 && (
|
|
63
|
+
{isDev && enabledFeatures.length > 0 && (
|
|
61
64
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
62
65
|
{enabledFeatures.map(({ key, icon: Icon, label }) => (
|
|
63
66
|
<span
|
|
@@ -74,7 +77,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
74
77
|
)}
|
|
75
78
|
|
|
76
79
|
{/* MCP Servers */}
|
|
77
|
-
{mcpServers.length > 0 && (
|
|
80
|
+
{isDev && mcpServers.length > 0 && (
|
|
78
81
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
79
82
|
{mcpServers.map((server) => {
|
|
80
83
|
// HTTP/remote servers are always available
|
|
@@ -98,7 +101,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
98
101
|
)}
|
|
99
102
|
|
|
100
103
|
{/* Skills */}
|
|
101
|
-
{skills.length > 0 && (
|
|
104
|
+
{isDev && skills.length > 0 && (
|
|
102
105
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
103
106
|
{skills.map((skill) => (
|
|
104
107
|
<span
|
|
@@ -118,7 +121,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
|
|
|
118
121
|
)}
|
|
119
122
|
|
|
120
123
|
{/* Subscriptions (triggers listening to) */}
|
|
121
|
-
{subscriptions.length > 0 && (
|
|
124
|
+
{isDev && subscriptions.length > 0 && (
|
|
122
125
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
123
126
|
{subscriptions.map((sub) => (
|
|
124
127
|
<span
|