openbot 0.4.4 → 0.4.6
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/app/channel-ids.js +3 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/responding-agent.js +48 -0
- package/dist/app/server.js +112 -6
- package/dist/plugins/openbot/context.js +6 -25
- package/dist/plugins/openbot/runtime.js +116 -81
- package/dist/plugins/openbot/system-prompt.js +3 -6
- package/dist/plugins/plugin-manager/index.js +3 -45
- package/dist/plugins/storage/index.js +2 -41
- package/dist/plugins/storage/service.js +55 -15
- package/dist/services/plugins/service.js +0 -4
- package/package.json +2 -1
- package/src/app/channel-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/responding-agent.ts +74 -0
- package/src/app/server.ts +137 -7
- package/src/app/types.ts +7 -10
- package/src/plugins/openbot/context.ts +7 -26
- package/src/plugins/openbot/runtime.ts +137 -89
- package/src/plugins/openbot/system-prompt.ts +3 -8
- package/src/plugins/plugin-manager/index.ts +2 -50
- package/src/plugins/storage/index.ts +4 -46
- package/src/plugins/storage/service.ts +80 -15
- package/src/services/plugins/domain.ts +22 -5
- package/src/services/plugins/service.ts +0 -6
- package/dist/plugins/thread-naming/generate-title.js +0 -44
- package/dist/plugins/thread-naming/index.js +0 -103
- package/dist/services/thread-naming.js +0 -81
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Default channel when requests omit channelId (general-purpose conversation). */
|
|
2
|
+
export const UNCATEGORIZED_CHANNEL_ID = 'uncategorized';
|
|
3
|
+
export const DEFAULT_UNCATEGORIZED_SPEC = '# Uncategorized\n\nGeneral-purpose channel for conversations without a dedicated channel.';
|
package/dist/app/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ function checkNodeVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
checkNodeVersion();
|
|
19
|
-
program.name('openbot').description('OpenBot CLI').version('0.4.
|
|
19
|
+
program.name('openbot').description('OpenBot CLI').version('0.4.6');
|
|
20
20
|
program
|
|
21
21
|
.command('start')
|
|
22
22
|
.description('Start the OpenBot harness')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
|
|
2
|
+
import { storageService } from '../plugins/storage/service.js';
|
|
3
|
+
/** Thread `state.json` key for the sticky responding agent id. */
|
|
4
|
+
export const THREAD_RESPONDING_AGENT_ID_KEY = 'respondingAgentId';
|
|
5
|
+
const readBoundAgentId = (state) => {
|
|
6
|
+
if (!state || typeof state !== 'object')
|
|
7
|
+
return undefined;
|
|
8
|
+
const value = state[THREAD_RESPONDING_AGENT_ID_KEY];
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return undefined;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed || undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolves which agent should handle a thread-scoped publish.
|
|
16
|
+
* Once `respondingAgentId` is stored on the thread, that value wins over request overrides.
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveRespondingAgentId(options) {
|
|
19
|
+
const { channelId, threadId, requestedAgentId, bindIfUnbound = false } = options;
|
|
20
|
+
const requested = requestedAgentId?.trim() || undefined;
|
|
21
|
+
const fallback = requested || ORCHESTRATOR_AGENT_ID;
|
|
22
|
+
if (!threadId) {
|
|
23
|
+
return { agentId: fallback, bound: false, overridden: false };
|
|
24
|
+
}
|
|
25
|
+
const details = await storageService.getThreadDetails({ channelId, threadId });
|
|
26
|
+
const bound = readBoundAgentId(details.state);
|
|
27
|
+
if (bound) {
|
|
28
|
+
const overridden = !!requested && requested !== bound;
|
|
29
|
+
if (overridden) {
|
|
30
|
+
console.warn('[publish] Ignoring agentId override; thread is bound to responding agent', {
|
|
31
|
+
threadId,
|
|
32
|
+
bound,
|
|
33
|
+
requested,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return { agentId: bound, bound: true, overridden };
|
|
37
|
+
}
|
|
38
|
+
if (!bindIfUnbound) {
|
|
39
|
+
return { agentId: fallback, bound: false, overridden: false };
|
|
40
|
+
}
|
|
41
|
+
await storageService.getAgentDetails({ agentId: fallback });
|
|
42
|
+
await storageService.patchThreadState({
|
|
43
|
+
channelId,
|
|
44
|
+
threadId,
|
|
45
|
+
state: { [THREAD_RESPONDING_AGENT_ID_KEY]: fallback },
|
|
46
|
+
});
|
|
47
|
+
return { agentId: fallback, bound: true, overridden: false };
|
|
48
|
+
}
|
package/dist/app/server.js
CHANGED
|
@@ -16,6 +16,8 @@ import { storageService } from '../plugins/storage/service.js';
|
|
|
16
16
|
import { buildWorkspaceFileUrl, getPublicBaseUrl, openChannelFileStream, } from '../plugins/storage/files.js';
|
|
17
17
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
18
18
|
import { abortRegistry, abortKey } from '../services/abort.js';
|
|
19
|
+
import { resolveRespondingAgentId } from './responding-agent.js';
|
|
20
|
+
import { DEFAULT_UNCATEGORIZED_SPEC, UNCATEGORIZED_CHANNEL_ID, } from './channel-ids.js';
|
|
19
21
|
export async function startServer(options = {}) {
|
|
20
22
|
const publishEventSchema = z
|
|
21
23
|
.object({
|
|
@@ -42,8 +44,17 @@ export async function startServer(options = {}) {
|
|
|
42
44
|
// Pre-warm caches for agents and plugins to speed up first UI load
|
|
43
45
|
storageService.getAgents().catch((err) => console.warn('[server] Failed to pre-warm agents cache', err));
|
|
44
46
|
storageService.getPlugins().catch((err) => console.warn('[server] Failed to pre-warm plugins cache', err));
|
|
47
|
+
const getRawChannelId = (req) => {
|
|
48
|
+
const raw = req.get('x-openbot-channel-id') ||
|
|
49
|
+
req.query.channelId ||
|
|
50
|
+
(req.body && req.body.channelId);
|
|
51
|
+
if (typeof raw !== 'string')
|
|
52
|
+
return undefined;
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
return trimmed || undefined;
|
|
55
|
+
};
|
|
45
56
|
const getContext = (req) => {
|
|
46
|
-
const
|
|
57
|
+
const rawChannelId = getRawChannelId(req);
|
|
47
58
|
const threadId = req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
|
|
48
59
|
const agentId = req.get('x-openbot-agent-id') || req.query.agentId || (req.body && req.body.agentId);
|
|
49
60
|
const runId = req.get('x-openbot-run-id') ||
|
|
@@ -54,7 +65,8 @@ export async function startServer(options = {}) {
|
|
|
54
65
|
req.query.responseType ||
|
|
55
66
|
(req.body && req.body.responseType);
|
|
56
67
|
return {
|
|
57
|
-
channelId: (
|
|
68
|
+
channelId: (rawChannelId || UNCATEGORIZED_CHANNEL_ID),
|
|
69
|
+
rawChannelId,
|
|
58
70
|
threadId: threadId,
|
|
59
71
|
agentId: agentId,
|
|
60
72
|
runId: runId,
|
|
@@ -116,18 +128,58 @@ export async function startServer(options = {}) {
|
|
|
116
128
|
data: { channels },
|
|
117
129
|
};
|
|
118
130
|
};
|
|
131
|
+
const broadcastActiveRunsSnapshot = () => {
|
|
132
|
+
const snapshot = buildActiveRunsSnapshot();
|
|
133
|
+
ensureEventId(snapshot);
|
|
134
|
+
sendToClientKey(GLOBAL_CHANNEL_ID, snapshot);
|
|
135
|
+
};
|
|
136
|
+
const persistLifecycleEvent = (event, targetChannelId, targetThreadId) => {
|
|
137
|
+
ensureEventId(event);
|
|
138
|
+
storageService
|
|
139
|
+
.storeEvent({
|
|
140
|
+
channelId: targetChannelId,
|
|
141
|
+
threadId: targetThreadId,
|
|
142
|
+
event,
|
|
143
|
+
})
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
console.error('[server] Failed to persist lifecycle event', {
|
|
146
|
+
type: event.type,
|
|
147
|
+
channelId: targetChannelId,
|
|
148
|
+
threadId: targetThreadId,
|
|
149
|
+
error,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
};
|
|
119
153
|
// Drop every tracked run for a channel/thread. A stop aborts the whole
|
|
120
154
|
// chain (parent + delegated sub-agents), but the sub-agents' `agent:run:end`
|
|
121
155
|
// events can be swallowed when the parent run loop breaks on abort, leaving
|
|
122
|
-
// orphaned entries that keep a channel falsely "active".
|
|
123
|
-
//
|
|
156
|
+
// orphaned entries that keep a channel falsely "active". Emit explicit
|
|
157
|
+
// `agent:run:end` for each purged run and refresh the global snapshot so
|
|
158
|
+
// clients stay in sync even when the parent harness stops yielding.
|
|
124
159
|
const purgeActiveRunsForThread = (channelId, threadId) => {
|
|
125
160
|
const target = threadId || undefined;
|
|
161
|
+
const removed = [];
|
|
126
162
|
for (const [key, run] of activeRuns) {
|
|
127
163
|
if (run.channelId === channelId && (run.threadId || undefined) === target) {
|
|
164
|
+
removed.push(run);
|
|
128
165
|
activeRuns.delete(key);
|
|
129
166
|
}
|
|
130
167
|
}
|
|
168
|
+
for (const run of removed) {
|
|
169
|
+
const endEvent = {
|
|
170
|
+
type: 'agent:run:end',
|
|
171
|
+
data: {
|
|
172
|
+
runId: run.runId,
|
|
173
|
+
agentId: run.agentId,
|
|
174
|
+
channelId: run.channelId,
|
|
175
|
+
threadId: run.threadId,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
ensureEventId(endEvent);
|
|
179
|
+
persistLifecycleEvent(endEvent, run.channelId, run.threadId);
|
|
180
|
+
sendToClientKey(getClientKey(run.channelId, run.threadId), endEvent);
|
|
181
|
+
sendToClientKey(GLOBAL_CHANNEL_ID, endEvent);
|
|
182
|
+
}
|
|
131
183
|
};
|
|
132
184
|
// Support for Chrome's Private Network Access (PNA)
|
|
133
185
|
// https://developer.chrome.com/blog/private-network-access-preflight/
|
|
@@ -328,19 +380,38 @@ export async function startServer(options = {}) {
|
|
|
328
380
|
const data = (event.data ?? {});
|
|
329
381
|
const targetChannelId = data.channelId || channelId;
|
|
330
382
|
const targetThreadId = data.threadId || threadId;
|
|
383
|
+
let resolvedStopAgentId = data.agentId || agentId || ORCHESTRATOR_AGENT_ID;
|
|
384
|
+
try {
|
|
385
|
+
const resolved = await resolveRespondingAgentId({
|
|
386
|
+
channelId: targetChannelId,
|
|
387
|
+
threadId: targetThreadId,
|
|
388
|
+
requestedAgentId: data.agentId || agentId,
|
|
389
|
+
});
|
|
390
|
+
resolvedStopAgentId = resolved.agentId;
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.warn('[publish] Failed to resolve responding agent for stop request', {
|
|
394
|
+
channelId: targetChannelId,
|
|
395
|
+
threadId: targetThreadId,
|
|
396
|
+
error,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
331
399
|
const stopped = abortRegistry.abort(abortKey(targetChannelId, targetThreadId));
|
|
332
400
|
purgeActiveRunsForThread(targetChannelId, targetThreadId);
|
|
401
|
+
// Resync global clients even when nothing was tracked server-side.
|
|
402
|
+
broadcastActiveRunsSnapshot();
|
|
333
403
|
const stoppedEvent = {
|
|
334
404
|
type: 'agent:run:stopped',
|
|
335
405
|
data: {
|
|
336
406
|
runId: data.runId || runId,
|
|
337
|
-
agentId:
|
|
407
|
+
agentId: resolvedStopAgentId,
|
|
338
408
|
channelId: targetChannelId,
|
|
339
409
|
threadId: targetThreadId,
|
|
340
410
|
reason: data.reason,
|
|
341
411
|
},
|
|
342
412
|
};
|
|
343
413
|
ensureEventId(stoppedEvent);
|
|
414
|
+
persistLifecycleEvent(stoppedEvent, targetChannelId, targetThreadId);
|
|
344
415
|
sendToClientKey(getClientKey(targetChannelId, targetThreadId), stoppedEvent);
|
|
345
416
|
sendToClientKey(GLOBAL_CHANNEL_ID, stoppedEvent);
|
|
346
417
|
res.json({ success: stopped });
|
|
@@ -363,6 +434,7 @@ export async function startServer(options = {}) {
|
|
|
363
434
|
}
|
|
364
435
|
else if (chunk.type === 'agent:run:stopped') {
|
|
365
436
|
purgeActiveRunsForThread(chunk.data.channelId, chunk.data.threadId);
|
|
437
|
+
broadcastActiveRunsSnapshot();
|
|
366
438
|
}
|
|
367
439
|
sendToClientKey(targetClientKey, chunk);
|
|
368
440
|
if (chunk.type === 'agent:run:start' ||
|
|
@@ -373,9 +445,27 @@ export async function startServer(options = {}) {
|
|
|
373
445
|
};
|
|
374
446
|
try {
|
|
375
447
|
ensureEventId(event);
|
|
448
|
+
const isUserConversationStart = event.type === 'agent:invoke' &&
|
|
449
|
+
event.data?.role === 'user' &&
|
|
450
|
+
typeof event.data.content === 'string' &&
|
|
451
|
+
event.data.content.trim().length > 0;
|
|
452
|
+
if (isUserConversationStart && channelId === UNCATEGORIZED_CHANNEL_ID) {
|
|
453
|
+
await storageService.ensureChannel({
|
|
454
|
+
channelId: UNCATEGORIZED_CHANNEL_ID,
|
|
455
|
+
spec: DEFAULT_UNCATEGORIZED_SPEC,
|
|
456
|
+
initialState: { name: 'Uncategorized' },
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const bindIfUnbound = event.type === 'agent:invoke';
|
|
460
|
+
const resolved = await resolveRespondingAgentId({
|
|
461
|
+
channelId,
|
|
462
|
+
threadId,
|
|
463
|
+
requestedAgentId: agentId,
|
|
464
|
+
bindIfUnbound,
|
|
465
|
+
});
|
|
376
466
|
await runAgent({
|
|
377
467
|
runId,
|
|
378
|
-
agentId: agentId
|
|
468
|
+
agentId: resolved.agentId,
|
|
379
469
|
event,
|
|
380
470
|
channelId,
|
|
381
471
|
threadId,
|
|
@@ -385,6 +475,11 @@ export async function startServer(options = {}) {
|
|
|
385
475
|
res.sendStatus(200);
|
|
386
476
|
}
|
|
387
477
|
catch (error) {
|
|
478
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
479
|
+
const isUnknownAgent = (error instanceof Error &&
|
|
480
|
+
(error.code === 'AGENT_NOT_FOUND' ||
|
|
481
|
+
error.message.includes('does not exist'))) ||
|
|
482
|
+
message.includes('does not exist');
|
|
388
483
|
console.error('[publish] Failed to dispatch event', {
|
|
389
484
|
runId,
|
|
390
485
|
channelId,
|
|
@@ -392,6 +487,10 @@ export async function startServer(options = {}) {
|
|
|
392
487
|
eventType: event.type,
|
|
393
488
|
error,
|
|
394
489
|
});
|
|
490
|
+
if (isUnknownAgent) {
|
|
491
|
+
res.status(400).json({ error: message });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
395
494
|
res.status(500).json({ error: 'Failed to process publish event' });
|
|
396
495
|
}
|
|
397
496
|
});
|
|
@@ -406,6 +505,13 @@ export async function startServer(options = {}) {
|
|
|
406
505
|
return;
|
|
407
506
|
}
|
|
408
507
|
const { channelId, threadId, agentId, runId } = getContext(req);
|
|
508
|
+
// In-memory active runs (not persisted). Mirrors the initial SSE frame on __global__.
|
|
509
|
+
if (channelId === GLOBAL_CHANNEL_ID && event.type === 'action:storage:get-active-runs') {
|
|
510
|
+
const snapshot = buildActiveRunsSnapshot();
|
|
511
|
+
ensureEventId(snapshot);
|
|
512
|
+
res.json({ events: [snapshot] });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
409
515
|
if (event.type === 'action:storage:serve-file') {
|
|
410
516
|
const filePath = event.data?.path;
|
|
411
517
|
if (!channelId?.trim()) {
|
|
@@ -20,19 +20,11 @@ export const getContextBudgetForModel = (modelString) => {
|
|
|
20
20
|
};
|
|
21
21
|
/** Built-in orchestrator agent id. */
|
|
22
22
|
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
23
|
-
/**
|
|
24
|
-
* Check if a channel is a solo DM (only the agent is present).
|
|
25
|
-
*/
|
|
26
|
-
export function isDmSoloChannel(participants, agentId) {
|
|
27
|
-
return participants.length === 0 || (participants.length === 1 && participants[0] === agentId);
|
|
28
|
-
}
|
|
29
23
|
/**
|
|
30
24
|
* Simplified context builder for MVP.
|
|
31
25
|
*/
|
|
32
26
|
export async function buildContext(state, storage) {
|
|
33
27
|
const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
|
|
34
|
-
const participants = channelDetails?.participants || [];
|
|
35
|
-
const isDm = isDmSoloChannel(participants, agentId);
|
|
36
28
|
const sections = [];
|
|
37
29
|
// Fetch agents once if storage is available
|
|
38
30
|
const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
|
|
@@ -42,24 +34,13 @@ export async function buildContext(state, storage) {
|
|
|
42
34
|
}
|
|
43
35
|
// 2. Environment
|
|
44
36
|
let env = '## ENVIRONMENT\n';
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const channelName = channelDetails?.name || channelId;
|
|
38
|
+
env += `- Mode: Channel (#${channelName})\n`;
|
|
39
|
+
if (channelDetails?.cwd) {
|
|
40
|
+
env += `- Workspace: ${channelDetails.cwd}\n`;
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
env += `- Mode: Channel (#${channelName})\n`;
|
|
51
|
-
if (channelDetails?.cwd) {
|
|
52
|
-
env += `- Workspace: ${channelDetails.cwd}\n`;
|
|
53
|
-
}
|
|
54
|
-
if (threadId) {
|
|
55
|
-
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
56
|
-
}
|
|
57
|
-
const peerIds = participants.filter((id) => id !== agentId);
|
|
58
|
-
const participantLabels = peerIds.map((id) => {
|
|
59
|
-
const agent = allAgents.find((a) => a.id === id);
|
|
60
|
-
return agent ? `${agent.name} (${id})` : id;
|
|
61
|
-
});
|
|
62
|
-
env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
|
|
42
|
+
if (threadId) {
|
|
43
|
+
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
63
44
|
}
|
|
64
45
|
sections.push(env);
|
|
65
46
|
// 2.5 Installed Agents
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { generateText } from 'ai';
|
|
2
2
|
import { openai } from '@ai-sdk/openai';
|
|
3
3
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
import { google } from '@ai-sdk/google';
|
|
4
5
|
import { eventsToModelMessages } from './history.js';
|
|
5
6
|
import { buildContext, } from './context.js';
|
|
6
|
-
import { saveConfig } from '../../app/config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { saveConfig, DEFAULT_MARKETPLACE_REGISTRY_URL } from '../../app/config.js';
|
|
8
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
9
|
+
let cachedRegistry = null;
|
|
10
|
+
async function fetchRegistry() {
|
|
11
|
+
if (cachedRegistry)
|
|
12
|
+
return cachedRegistry;
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
|
|
15
|
+
if (!response.ok)
|
|
16
|
+
throw new Error(`Failed to fetch registry: ${response.statusText}`);
|
|
17
|
+
cachedRegistry = (await response.json());
|
|
18
|
+
return cachedRegistry;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('[openbot] Failed to fetch model registry:', error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
8
25
|
function resolveModel(modelString) {
|
|
9
26
|
const [provider, ...rest] = modelString.split('/');
|
|
10
27
|
const modelId = rest.join('/');
|
|
@@ -16,6 +33,8 @@ function resolveModel(modelString) {
|
|
|
16
33
|
return openai(modelId);
|
|
17
34
|
case 'anthropic':
|
|
18
35
|
return anthropic(modelId);
|
|
36
|
+
case 'google':
|
|
37
|
+
return google(modelId);
|
|
19
38
|
default:
|
|
20
39
|
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
21
40
|
}
|
|
@@ -182,47 +201,20 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
182
201
|
errorMessage.includes('Unauthorized') ||
|
|
183
202
|
errorMessage.includes('authentication');
|
|
184
203
|
if (isApiKeyError) {
|
|
185
|
-
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
186
|
-
const currentModelId = rest.join('/');
|
|
187
204
|
yield {
|
|
188
205
|
type: 'client:ui:widget',
|
|
189
206
|
data: {
|
|
190
|
-
kind: '
|
|
191
|
-
widgetId: `
|
|
192
|
-
title: `AI Provider
|
|
193
|
-
description:
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
type: 'select',
|
|
199
|
-
required: true,
|
|
200
|
-
options: [
|
|
201
|
-
{ label: 'OpenAI', value: 'openai' },
|
|
202
|
-
{ label: 'Anthropic', value: 'anthropic' },
|
|
203
|
-
],
|
|
204
|
-
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
id: 'model',
|
|
208
|
-
label: 'Model',
|
|
209
|
-
type: 'text',
|
|
210
|
-
description: 'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
211
|
-
placeholder: 'gpt-4o-mini',
|
|
212
|
-
required: true,
|
|
213
|
-
defaultValue: currentModelId,
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
id: 'apiKey',
|
|
217
|
-
label: 'API Key',
|
|
218
|
-
type: 'text',
|
|
219
|
-
placeholder: `sk-...`,
|
|
220
|
-
required: true,
|
|
221
|
-
},
|
|
207
|
+
kind: 'choice',
|
|
208
|
+
widgetId: `api_provider_selection_${Date.now()}`,
|
|
209
|
+
title: `Setup AI Provider`,
|
|
210
|
+
description: `Select a provider to continue.`,
|
|
211
|
+
actions: [
|
|
212
|
+
{ id: 'openai', label: 'OpenAI', variant: 'primary' },
|
|
213
|
+
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' },
|
|
214
|
+
{ id: 'google', label: 'Google', variant: 'primary' },
|
|
222
215
|
],
|
|
223
|
-
submitLabel: 'Save & Continue',
|
|
224
216
|
metadata: {
|
|
225
|
-
type: '
|
|
217
|
+
type: 'api_provider_selection',
|
|
226
218
|
},
|
|
227
219
|
},
|
|
228
220
|
meta: { agentId: context.state.agentId, threadId },
|
|
@@ -244,43 +236,6 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
244
236
|
};
|
|
245
237
|
}
|
|
246
238
|
const threadId = event.meta?.threadId || context.state.threadId;
|
|
247
|
-
// Auto-add participants if tagged in the prompt
|
|
248
|
-
const content = event.data?.content;
|
|
249
|
-
if (content && storage) {
|
|
250
|
-
try {
|
|
251
|
-
const allAgents = await storage.getAgents();
|
|
252
|
-
const tags = content.match(/@([\w-]+)/g);
|
|
253
|
-
if (tags) {
|
|
254
|
-
const taggedAgentIds = tags.map((t) => t.slice(1));
|
|
255
|
-
const validAgentIds = taggedAgentIds.filter((id) => allAgents.some((a) => a.id === id));
|
|
256
|
-
const currentParticipants = context.state.channelDetails?.participants || [];
|
|
257
|
-
const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
|
|
258
|
-
if (newParticipants.length > currentParticipants.length) {
|
|
259
|
-
// Update storage
|
|
260
|
-
await storage.patchChannelState({
|
|
261
|
-
channelId: context.state.channelId,
|
|
262
|
-
state: { participants: newParticipants },
|
|
263
|
-
});
|
|
264
|
-
// Refresh local state
|
|
265
|
-
context.state.channelDetails = await storage.getChannelDetails({
|
|
266
|
-
channelId: context.state.channelId,
|
|
267
|
-
});
|
|
268
|
-
// Notify UI/others about the change
|
|
269
|
-
yield {
|
|
270
|
-
type: 'action:patch_channel_details:result',
|
|
271
|
-
data: { success: true, updatedFields: ['participants'] },
|
|
272
|
-
meta: {
|
|
273
|
-
agentId: context.state.agentId,
|
|
274
|
-
threadId,
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
console.warn('[openbot] Failed to auto-add participants from tags:', error);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
239
|
// clear the tool batch if the agent is invoked
|
|
285
240
|
// this is to prevent the tool batch from being used for a new agent invocation
|
|
286
241
|
await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
|
|
@@ -302,15 +257,88 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
302
257
|
yield* runLLM(context, threadId);
|
|
303
258
|
});
|
|
304
259
|
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
305
|
-
const { metadata, values } = event.data;
|
|
260
|
+
const { metadata, values, actionId } = event.data;
|
|
261
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
262
|
+
if (metadata?.type === 'api_provider_selection') {
|
|
263
|
+
const provider = actionId;
|
|
264
|
+
const [_, ...rest] = currentModelString.split('/');
|
|
265
|
+
const currentModelId = rest.join('/');
|
|
266
|
+
const registry = await fetchRegistry();
|
|
267
|
+
const providerData = registry?.providers[provider];
|
|
268
|
+
const providerLinks = {
|
|
269
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
270
|
+
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
271
|
+
google: 'https://aistudio.google.com/app/apikey',
|
|
272
|
+
};
|
|
273
|
+
const label = providerData?.label || provider;
|
|
274
|
+
const link = providerLinks[provider] || '';
|
|
275
|
+
const modelOptions = providerData?.models.map((m) => ({
|
|
276
|
+
label: m.label,
|
|
277
|
+
value: m.id,
|
|
278
|
+
}));
|
|
279
|
+
const defaultModel = modelOptions?.[0]?.value || 'gpt-4o-mini';
|
|
280
|
+
const defaultValue = modelOptions?.find((m) => m.value === currentModelId)?.value ||
|
|
281
|
+
currentModelId ||
|
|
282
|
+
defaultModel;
|
|
283
|
+
yield {
|
|
284
|
+
type: 'client:ui:widget',
|
|
285
|
+
data: {
|
|
286
|
+
widgetId: event.data.widgetId,
|
|
287
|
+
kind: 'message',
|
|
288
|
+
title: 'Provider Selected',
|
|
289
|
+
body: `${label} provider was selected.`,
|
|
290
|
+
state: 'submitted',
|
|
291
|
+
display: 'collapsed',
|
|
292
|
+
disabled: true,
|
|
293
|
+
actions: [],
|
|
294
|
+
},
|
|
295
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
296
|
+
};
|
|
297
|
+
yield {
|
|
298
|
+
type: 'client:ui:widget',
|
|
299
|
+
data: {
|
|
300
|
+
kind: 'form',
|
|
301
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
302
|
+
title: `${label} Setup`,
|
|
303
|
+
description: `Enter your API key and select a model.`,
|
|
304
|
+
fields: [
|
|
305
|
+
{
|
|
306
|
+
id: 'model',
|
|
307
|
+
label: 'Model',
|
|
308
|
+
type: modelOptions ? 'select' : 'text',
|
|
309
|
+
description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
|
|
310
|
+
options: modelOptions,
|
|
311
|
+
placeholder: defaultModel,
|
|
312
|
+
required: true,
|
|
313
|
+
defaultValue,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'apiKey',
|
|
317
|
+
label: 'API Key',
|
|
318
|
+
type: 'password',
|
|
319
|
+
description: `Get your key here: [${link}](${link})`,
|
|
320
|
+
placeholder: `sk-...`,
|
|
321
|
+
required: true,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
submitLabel: 'Save & Continue',
|
|
325
|
+
metadata: {
|
|
326
|
+
type: 'api_key_request',
|
|
327
|
+
provider,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
331
|
+
};
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
306
334
|
if (metadata?.type !== 'api_key_request')
|
|
307
335
|
return;
|
|
308
|
-
if (!values?.apiKey || !values?.
|
|
336
|
+
if (!values?.apiKey || !values?.model)
|
|
309
337
|
return;
|
|
310
|
-
const provider = String(values.provider);
|
|
338
|
+
const provider = String(values.provider || metadata.provider);
|
|
311
339
|
const modelId = String(values.model).trim();
|
|
312
340
|
const apiKey = String(values.apiKey);
|
|
313
|
-
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
341
|
+
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
|
|
314
342
|
yield {
|
|
315
343
|
type: 'agent:output',
|
|
316
344
|
data: { content: `Unsupported provider: ${provider}` },
|
|
@@ -318,7 +346,11 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
318
346
|
};
|
|
319
347
|
return;
|
|
320
348
|
}
|
|
321
|
-
const envVar = provider === 'openai'
|
|
349
|
+
const envVar = provider === 'openai'
|
|
350
|
+
? 'OPENAI_API_KEY'
|
|
351
|
+
: provider === 'anthropic'
|
|
352
|
+
? 'ANTHROPIC_API_KEY'
|
|
353
|
+
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
322
354
|
const newModelString = `${provider}/${modelId}`;
|
|
323
355
|
if (!storage)
|
|
324
356
|
return;
|
|
@@ -363,10 +395,13 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
363
395
|
title: 'API Key Saved',
|
|
364
396
|
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
365
397
|
state: 'submitted',
|
|
366
|
-
|
|
398
|
+
display: 'collapsed',
|
|
399
|
+
disabled: true,
|
|
400
|
+
actions: [],
|
|
367
401
|
},
|
|
368
402
|
meta: { agentId: context.state.agentId },
|
|
369
403
|
};
|
|
404
|
+
yield* runLLM(context, threadId);
|
|
370
405
|
}
|
|
371
406
|
catch (error) {
|
|
372
407
|
yield {
|
|
@@ -7,13 +7,12 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
7
7
|
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
8
|
'',
|
|
9
9
|
'# CORE MISSION',
|
|
10
|
-
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents
|
|
10
|
+
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents. You act as a high-level manager, ensuring the right agent is working on the right task.',
|
|
11
11
|
'',
|
|
12
12
|
'# OPERATIONAL GUIDELINES',
|
|
13
13
|
'- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
14
|
-
'- **
|
|
15
|
-
'- **
|
|
16
|
-
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other participant can handle.',
|
|
14
|
+
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list.',
|
|
15
|
+
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other agent can handle.',
|
|
17
16
|
'- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
|
|
18
17
|
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
19
18
|
'- **Structured Interaction**: Use the `render_widget` tool to collect information via forms, offer choices, or display lists. This is preferred over asking multiple separate questions in plain text.',
|
|
@@ -21,5 +20,3 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
21
20
|
'# COMMUNICATION STYLE',
|
|
22
21
|
'- Be always concise, professional, and proactive.',
|
|
23
22
|
].join('\n');
|
|
24
|
-
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
25
|
-
export const API_KEY_SETUP_MESSAGE = 'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';
|