openbot 0.4.5 → 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/server.js +24 -2
- 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 +54 -14
- 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/server.ts +31 -3
- package/src/app/types.ts +2 -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 +78 -14
- package/src/services/plugins/domain.ts +7 -4
- 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
|
@@ -340,18 +340,16 @@ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArr
|
|
|
340
340
|
/** Display-oriented fields persisted in a channel's `state.json`. */
|
|
341
341
|
const readChannelStateFileFields = (parsed) => {
|
|
342
342
|
if (!isRecord(parsed)) {
|
|
343
|
-
return {
|
|
343
|
+
return {};
|
|
344
344
|
}
|
|
345
345
|
const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
|
|
346
346
|
const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
return { name, cwd, participants };
|
|
347
|
+
return { name, cwd };
|
|
348
|
+
};
|
|
349
|
+
const isChannelProvisioned = async (channelId) => {
|
|
350
|
+
const statePath = `${getConversationDir(channelId)}/state.json`;
|
|
351
|
+
const state = await readJsonFile(statePath, {});
|
|
352
|
+
return !!readChannelStateFileFields(state).cwd;
|
|
355
353
|
};
|
|
356
354
|
/**
|
|
357
355
|
* Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
|
|
@@ -412,23 +410,23 @@ export const storageService = {
|
|
|
412
410
|
const statePath = path.join(channelDir, 'state.json');
|
|
413
411
|
let cwd;
|
|
414
412
|
let displayName = name;
|
|
415
|
-
let participants = [];
|
|
416
413
|
try {
|
|
417
414
|
const parsed = await readJsonFile(statePath, {});
|
|
418
415
|
const fields = readChannelStateFileFields(parsed);
|
|
419
416
|
cwd = fields.cwd;
|
|
420
417
|
displayName = fields.name ?? name;
|
|
421
|
-
participants = fields.participants;
|
|
422
418
|
}
|
|
423
419
|
catch {
|
|
424
420
|
// ignore
|
|
425
421
|
}
|
|
422
|
+
if (!cwd) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
426
425
|
const channel = {
|
|
427
426
|
id: name,
|
|
428
427
|
name: displayName,
|
|
429
428
|
description: '',
|
|
430
429
|
cwd,
|
|
431
|
-
participants,
|
|
432
430
|
createdAt: stats.birthtime,
|
|
433
431
|
updatedAt: stats.mtime,
|
|
434
432
|
};
|
|
@@ -452,7 +450,9 @@ export const storageService = {
|
|
|
452
450
|
}
|
|
453
451
|
return channel;
|
|
454
452
|
}));
|
|
455
|
-
return channels
|
|
453
|
+
return channels
|
|
454
|
+
.filter((channel) => channel !== null)
|
|
455
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
456
456
|
},
|
|
457
457
|
createChannel: async ({ channelId, spec, initialState, cwd, }) => {
|
|
458
458
|
const normalizedChannelId = channelId.trim();
|
|
@@ -486,6 +486,44 @@ export const storageService = {
|
|
|
486
486
|
`# ${normalizedChannelId}\n\n`);
|
|
487
487
|
await writeJsonFileAtomically(statePath, finalState);
|
|
488
488
|
},
|
|
489
|
+
ensureChannel: async ({ channelId, spec, initialState, cwd, }) => {
|
|
490
|
+
const normalizedChannelId = channelId.trim();
|
|
491
|
+
if (!normalizedChannelId) {
|
|
492
|
+
throw new Error('channelId is required');
|
|
493
|
+
}
|
|
494
|
+
const channelDir = getConversationDir(normalizedChannelId);
|
|
495
|
+
const specPath = `${channelDir}/SPEC.md`;
|
|
496
|
+
const statePath = `${channelDir}/state.json`;
|
|
497
|
+
const existingState = await readJsonFile(statePath, {});
|
|
498
|
+
const existingFields = readChannelStateFileFields(existingState);
|
|
499
|
+
if (existingFields.cwd) {
|
|
500
|
+
await fs.mkdir(resolvePath(existingFields.cwd), { recursive: true });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const finalState = {
|
|
504
|
+
...existingState,
|
|
505
|
+
...(initialState || {}),
|
|
506
|
+
};
|
|
507
|
+
const rawCwd = (typeof cwd === 'string' && cwd.trim()) ||
|
|
508
|
+
(typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
|
|
509
|
+
getDefaultChannelCwd(normalizedChannelId);
|
|
510
|
+
const resolvedCwd = resolvePath(rawCwd);
|
|
511
|
+
finalState.cwd = resolvedCwd;
|
|
512
|
+
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
513
|
+
await fs.mkdir(channelDir, { recursive: true });
|
|
514
|
+
try {
|
|
515
|
+
await fs.access(specPath);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error?.code === 'ENOENT') {
|
|
519
|
+
await fs.writeFile(specPath, spec?.trim() || `# ${normalizedChannelId}\n\n`);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
await writeJsonFileAtomically(statePath, finalState);
|
|
526
|
+
},
|
|
489
527
|
deleteChannel: async ({ channelId }) => {
|
|
490
528
|
const normalizedChannelId = channelId.trim();
|
|
491
529
|
if (!normalizedChannelId) {
|
|
@@ -648,7 +686,6 @@ export const storageService = {
|
|
|
648
686
|
spec,
|
|
649
687
|
state,
|
|
650
688
|
cwd,
|
|
651
|
-
participants: diskFields.participants,
|
|
652
689
|
};
|
|
653
690
|
details.threads = await storageService.getThreads({ channelId });
|
|
654
691
|
return details;
|
|
@@ -1030,6 +1067,9 @@ export const storageService = {
|
|
|
1030
1067
|
},
|
|
1031
1068
|
storeEvent: async ({ channelId, threadId, event, }) => {
|
|
1032
1069
|
try {
|
|
1070
|
+
if (!(await isChannelProvisioned(channelId))) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1033
1073
|
const threadDir = getConversationDir(channelId, threadId);
|
|
1034
1074
|
if (threadId) {
|
|
1035
1075
|
let exists = false;
|
|
@@ -74,20 +74,16 @@ export function parseMarketplaceRegistryJson(data) {
|
|
|
74
74
|
const id = item.id;
|
|
75
75
|
const name = item.name;
|
|
76
76
|
const description = item.description;
|
|
77
|
-
const participants = item.participants;
|
|
78
77
|
if (typeof id !== 'string' || !id)
|
|
79
78
|
throw new Error(`channels[${i}].id must be a non-empty string`);
|
|
80
79
|
if (typeof name !== 'string')
|
|
81
80
|
throw new Error(`channels[${i}].name must be a string`);
|
|
82
81
|
if (typeof description !== 'string')
|
|
83
82
|
throw new Error(`channels[${i}].description must be a string`);
|
|
84
|
-
if (!Array.isArray(participants))
|
|
85
|
-
throw new Error(`channels[${i}].participants must be an array`);
|
|
86
83
|
const listing = {
|
|
87
84
|
id,
|
|
88
85
|
name,
|
|
89
86
|
description,
|
|
90
|
-
participants: participants.filter((p) => typeof p === 'string'),
|
|
91
87
|
};
|
|
92
88
|
if (typeof item.image === 'string')
|
|
93
89
|
listing.image = item.image;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@ai-sdk/anthropic": "^3.0.33",
|
|
19
|
+
"@ai-sdk/google": "^3.0.82",
|
|
19
20
|
"@ai-sdk/openai": "^3.0.13",
|
|
20
21
|
"@anthropic-ai/claude-agent-sdk": "^0.2.138",
|
|
21
22
|
"@types/cors": "^2.8.19",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Default channel when requests omit channelId (general-purpose conversation). */
|
|
2
|
+
export const UNCATEGORIZED_CHANNEL_ID = 'uncategorized';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_UNCATEGORIZED_SPEC =
|
|
5
|
+
'# Uncategorized\n\nGeneral-purpose channel for conversations without a dedicated channel.';
|
package/src/app/cli.ts
CHANGED
package/src/app/server.ts
CHANGED
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
23
23
|
import { abortRegistry, abortKey } from '../services/abort.js';
|
|
24
24
|
import { resolveRespondingAgentId } from './responding-agent.js';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_UNCATEGORIZED_SPEC,
|
|
27
|
+
UNCATEGORIZED_CHANNEL_ID,
|
|
28
|
+
} from './channel-ids.js';
|
|
25
29
|
|
|
26
30
|
type Bucket = { channelId: string; threadId?: string; activeCount: number; agentIds: Set<string> };
|
|
27
31
|
|
|
@@ -65,9 +69,18 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
65
69
|
storageService.getAgents().catch((err) => console.warn('[server] Failed to pre-warm agents cache', err));
|
|
66
70
|
storageService.getPlugins().catch((err) => console.warn('[server] Failed to pre-warm plugins cache', err));
|
|
67
71
|
|
|
72
|
+
const getRawChannelId = (req: express.Request): string | undefined => {
|
|
73
|
+
const raw =
|
|
74
|
+
req.get('x-openbot-channel-id') ||
|
|
75
|
+
req.query.channelId ||
|
|
76
|
+
(req.body && req.body.channelId);
|
|
77
|
+
if (typeof raw !== 'string') return undefined;
|
|
78
|
+
const trimmed = raw.trim();
|
|
79
|
+
return trimmed || undefined;
|
|
80
|
+
};
|
|
81
|
+
|
|
68
82
|
const getContext = (req: express.Request) => {
|
|
69
|
-
const
|
|
70
|
-
req.get('x-openbot-channel-id') || req.query.channelId || (req.body && req.body.channelId);
|
|
83
|
+
const rawChannelId = getRawChannelId(req);
|
|
71
84
|
const threadId =
|
|
72
85
|
req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
|
|
73
86
|
const agentId =
|
|
@@ -83,7 +96,8 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
83
96
|
(req.body && req.body.responseType);
|
|
84
97
|
|
|
85
98
|
return {
|
|
86
|
-
channelId: (
|
|
99
|
+
channelId: (rawChannelId || UNCATEGORIZED_CHANNEL_ID) as string,
|
|
100
|
+
rawChannelId,
|
|
87
101
|
threadId: threadId as string | undefined,
|
|
88
102
|
agentId: agentId as string | undefined,
|
|
89
103
|
runId: runId as string,
|
|
@@ -532,6 +546,20 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
532
546
|
try {
|
|
533
547
|
ensureEventId(event);
|
|
534
548
|
|
|
549
|
+
const isUserConversationStart =
|
|
550
|
+
event.type === 'agent:invoke' &&
|
|
551
|
+
event.data?.role === 'user' &&
|
|
552
|
+
typeof event.data.content === 'string' &&
|
|
553
|
+
event.data.content.trim().length > 0;
|
|
554
|
+
|
|
555
|
+
if (isUserConversationStart && channelId === UNCATEGORIZED_CHANNEL_ID) {
|
|
556
|
+
await storageService.ensureChannel({
|
|
557
|
+
channelId: UNCATEGORIZED_CHANNEL_ID,
|
|
558
|
+
spec: DEFAULT_UNCATEGORIZED_SPEC,
|
|
559
|
+
initialState: { name: 'Uncategorized' },
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
535
563
|
const bindIfUnbound = event.type === 'agent:invoke';
|
|
536
564
|
const resolved = await resolveRespondingAgentId({
|
|
537
565
|
channelId,
|
package/src/app/types.ts
CHANGED
|
@@ -301,8 +301,6 @@ export type PatchChannelDetailsEvent = BaseEvent & {
|
|
|
301
301
|
state?: Record<string, unknown>;
|
|
302
302
|
spec?: string;
|
|
303
303
|
cwd?: string;
|
|
304
|
-
/** When set, replaces `state.json` `participants` (merged after `state` if both are sent). */
|
|
305
|
-
participants?: string[];
|
|
306
304
|
};
|
|
307
305
|
};
|
|
308
306
|
|
|
@@ -310,7 +308,7 @@ export type PatchChannelDetailsResultEvent = BaseEvent & {
|
|
|
310
308
|
type: 'action:patch_channel_details:result';
|
|
311
309
|
data: {
|
|
312
310
|
success: boolean;
|
|
313
|
-
updatedFields: ('state' | 'spec' | 'cwd'
|
|
311
|
+
updatedFields: ('state' | 'spec' | 'cwd')[];
|
|
314
312
|
};
|
|
315
313
|
};
|
|
316
314
|
|
|
@@ -569,8 +567,6 @@ export type CreateChannelEvent = BaseEvent & {
|
|
|
569
567
|
spec?: string;
|
|
570
568
|
initialState?: Record<string, unknown>;
|
|
571
569
|
cwd?: string;
|
|
572
|
-
/** Initial channel agent ids; written into `state.json` (overrides `initialState.participants` if both are set). */
|
|
573
|
-
participants?: string[];
|
|
574
570
|
};
|
|
575
571
|
meta?: {
|
|
576
572
|
toolCallId?: string;
|
|
@@ -594,8 +590,6 @@ export type UpdateChannelEvent = BaseEvent & {
|
|
|
594
590
|
channelId?: string;
|
|
595
591
|
name?: string;
|
|
596
592
|
cwd?: string;
|
|
597
|
-
/** Replaces the channel participant list when provided. */
|
|
598
|
-
participants?: string[];
|
|
599
593
|
};
|
|
600
594
|
};
|
|
601
595
|
|
|
@@ -655,7 +649,7 @@ export type UIWidgetAction = {
|
|
|
655
649
|
export type UIWidgetField = {
|
|
656
650
|
id: string;
|
|
657
651
|
label: string;
|
|
658
|
-
type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'multiselect' | 'date';
|
|
652
|
+
type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'multiselect' | 'date' | 'password';
|
|
659
653
|
description?: string;
|
|
660
654
|
placeholder?: string;
|
|
661
655
|
required?: boolean;
|
|
@@ -851,7 +845,6 @@ export type ListMarketplaceRegistryResultEvent = BaseEvent & {
|
|
|
851
845
|
image?: string;
|
|
852
846
|
spec?: string;
|
|
853
847
|
initialState?: Record<string, unknown>;
|
|
854
|
-
participants: string[];
|
|
855
848
|
starterPrompts?: Array<{ label: string; prompt: string }>;
|
|
856
849
|
}>;
|
|
857
850
|
error?: string;
|
|
@@ -863,7 +856,6 @@ export type InstallChannelEvent = BaseEvent & {
|
|
|
863
856
|
data: {
|
|
864
857
|
channelId: string;
|
|
865
858
|
name?: string;
|
|
866
|
-
participants?: string[];
|
|
867
859
|
initialState?: Record<string, unknown>;
|
|
868
860
|
};
|
|
869
861
|
};
|
|
@@ -27,20 +27,11 @@ export const getContextBudgetForModel = (modelString: string): number => {
|
|
|
27
27
|
/** Built-in orchestrator agent id. */
|
|
28
28
|
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Check if a channel is a solo DM (only the agent is present).
|
|
32
|
-
*/
|
|
33
|
-
export function isDmSoloChannel(participants: string[], agentId: string): boolean {
|
|
34
|
-
return participants.length === 0 || (participants.length === 1 && participants[0] === agentId);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
/**
|
|
38
31
|
* Simplified context builder for MVP.
|
|
39
32
|
*/
|
|
40
33
|
export async function buildContext(state: OpenBotState, storage?: Storage): Promise<string> {
|
|
41
34
|
const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
|
|
42
|
-
const participants = channelDetails?.participants || [];
|
|
43
|
-
const isDm = isDmSoloChannel(participants, agentId);
|
|
44
35
|
|
|
45
36
|
const sections: string[] = [];
|
|
46
37
|
|
|
@@ -54,23 +45,13 @@ export async function buildContext(state: OpenBotState, storage?: Storage): Prom
|
|
|
54
45
|
|
|
55
46
|
// 2. Environment
|
|
56
47
|
let env = '## ENVIRONMENT\n';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (threadId) {
|
|
66
|
-
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
67
|
-
}
|
|
68
|
-
const peerIds = participants.filter((id: string) => id !== agentId);
|
|
69
|
-
const participantLabels = peerIds.map((id) => {
|
|
70
|
-
const agent = allAgents.find((a) => a.id === id);
|
|
71
|
-
return agent ? `${agent.name} (${id})` : id;
|
|
72
|
-
});
|
|
73
|
-
env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
|
|
48
|
+
const channelName = channelDetails?.name || channelId;
|
|
49
|
+
env += `- Mode: Channel (#${channelName})\n`;
|
|
50
|
+
if (channelDetails?.cwd) {
|
|
51
|
+
env += `- Workspace: ${channelDetails.cwd}\n`;
|
|
52
|
+
}
|
|
53
|
+
if (threadId) {
|
|
54
|
+
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
74
55
|
}
|
|
75
56
|
sections.push(env);
|
|
76
57
|
|
|
@@ -2,6 +2,7 @@ import { MelonyPlugin, RuntimeContext } from 'melony';
|
|
|
2
2
|
import { generateText, type LanguageModel } from 'ai';
|
|
3
3
|
import { openai } from '@ai-sdk/openai';
|
|
4
4
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { google } from '@ai-sdk/google';
|
|
5
6
|
import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
|
|
6
7
|
import { eventsToModelMessages } from './history.js';
|
|
7
8
|
import { Storage } from '../../services/plugins/domain.js';
|
|
@@ -10,8 +11,33 @@ import {
|
|
|
10
11
|
ORCHESTRATOR_AGENT_ID,
|
|
11
12
|
buildContext,
|
|
12
13
|
} from './context.js';
|
|
13
|
-
import { saveConfig } from '../../app/config.js';
|
|
14
|
-
import {
|
|
14
|
+
import { saveConfig, DEFAULT_MARKETPLACE_REGISTRY_URL } from '../../app/config.js';
|
|
15
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
16
|
+
|
|
17
|
+
interface ModelRegistry {
|
|
18
|
+
providers: Record<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
label: string;
|
|
22
|
+
models: Array<{ id: string; label: string; description: string }>;
|
|
23
|
+
}
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let cachedRegistry: ModelRegistry | null = null;
|
|
28
|
+
|
|
29
|
+
async function fetchRegistry(): Promise<ModelRegistry | null> {
|
|
30
|
+
if (cachedRegistry) return cachedRegistry;
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
|
|
33
|
+
if (!response.ok) throw new Error(`Failed to fetch registry: ${response.statusText}`);
|
|
34
|
+
cachedRegistry = (await response.json()) as ModelRegistry;
|
|
35
|
+
return cachedRegistry;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('[openbot] Failed to fetch model registry:', error);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
15
41
|
|
|
16
42
|
export interface OpenBotRuntimeOptions {
|
|
17
43
|
/** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
|
|
@@ -34,6 +60,8 @@ function resolveModel(modelString: string): LanguageModel {
|
|
|
34
60
|
return openai(modelId);
|
|
35
61
|
case 'anthropic':
|
|
36
62
|
return anthropic(modelId);
|
|
63
|
+
case 'google':
|
|
64
|
+
return google(modelId);
|
|
37
65
|
default:
|
|
38
66
|
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
39
67
|
}
|
|
@@ -246,49 +274,20 @@ export const openbotRuntime =
|
|
|
246
274
|
errorMessage.includes('authentication');
|
|
247
275
|
|
|
248
276
|
if (isApiKeyError) {
|
|
249
|
-
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
250
|
-
const currentModelId = rest.join('/');
|
|
251
|
-
|
|
252
277
|
yield {
|
|
253
278
|
type: 'client:ui:widget',
|
|
254
279
|
data: {
|
|
255
|
-
kind: '
|
|
256
|
-
widgetId: `
|
|
257
|
-
title: `AI Provider
|
|
258
|
-
description:
|
|
259
|
-
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
type: 'select',
|
|
264
|
-
required: true,
|
|
265
|
-
options: [
|
|
266
|
-
{ label: 'OpenAI', value: 'openai' },
|
|
267
|
-
{ label: 'Anthropic', value: 'anthropic' },
|
|
268
|
-
],
|
|
269
|
-
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
id: 'model',
|
|
273
|
-
label: 'Model',
|
|
274
|
-
type: 'text',
|
|
275
|
-
description:
|
|
276
|
-
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
277
|
-
placeholder: 'gpt-4o-mini',
|
|
278
|
-
required: true,
|
|
279
|
-
defaultValue: currentModelId,
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
id: 'apiKey',
|
|
283
|
-
label: 'API Key',
|
|
284
|
-
type: 'text',
|
|
285
|
-
placeholder: `sk-...`,
|
|
286
|
-
required: true,
|
|
287
|
-
},
|
|
280
|
+
kind: 'choice',
|
|
281
|
+
widgetId: `api_provider_selection_${Date.now()}`,
|
|
282
|
+
title: `Setup AI Provider`,
|
|
283
|
+
description: `Select a provider to continue.`,
|
|
284
|
+
actions: [
|
|
285
|
+
{ id: 'openai', label: 'OpenAI', variant: 'primary' },
|
|
286
|
+
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' },
|
|
287
|
+
{ id: 'google', label: 'Google', variant: 'primary' },
|
|
288
288
|
],
|
|
289
|
-
submitLabel: 'Save & Continue',
|
|
290
289
|
metadata: {
|
|
291
|
-
type: '
|
|
290
|
+
type: 'api_provider_selection',
|
|
292
291
|
},
|
|
293
292
|
},
|
|
294
293
|
meta: { agentId: context.state.agentId, threadId },
|
|
@@ -315,49 +314,6 @@ export const openbotRuntime =
|
|
|
315
314
|
|
|
316
315
|
const threadId = event.meta?.threadId || context.state.threadId;
|
|
317
316
|
|
|
318
|
-
// Auto-add participants if tagged in the prompt
|
|
319
|
-
const content = (event as AgentInvokeEvent).data?.content;
|
|
320
|
-
if (content && storage) {
|
|
321
|
-
try {
|
|
322
|
-
const allAgents = await storage.getAgents();
|
|
323
|
-
const tags = content.match(/@([\w-]+)/g);
|
|
324
|
-
if (tags) {
|
|
325
|
-
const taggedAgentIds = tags.map((t) => t.slice(1));
|
|
326
|
-
const validAgentIds = taggedAgentIds.filter((id) =>
|
|
327
|
-
allAgents.some((a) => a.id === id),
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
const currentParticipants = context.state.channelDetails?.participants || [];
|
|
331
|
-
const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
|
|
332
|
-
|
|
333
|
-
if (newParticipants.length > currentParticipants.length) {
|
|
334
|
-
// Update storage
|
|
335
|
-
await storage.patchChannelState({
|
|
336
|
-
channelId: context.state.channelId,
|
|
337
|
-
state: { participants: newParticipants },
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Refresh local state
|
|
341
|
-
context.state.channelDetails = await storage.getChannelDetails({
|
|
342
|
-
channelId: context.state.channelId,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Notify UI/others about the change
|
|
346
|
-
yield {
|
|
347
|
-
type: 'action:patch_channel_details:result',
|
|
348
|
-
data: { success: true, updatedFields: ['participants'] },
|
|
349
|
-
meta: {
|
|
350
|
-
agentId: context.state.agentId,
|
|
351
|
-
threadId,
|
|
352
|
-
},
|
|
353
|
-
} as OpenBotEvent;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} catch (error) {
|
|
357
|
-
console.warn('[openbot] Failed to auto-add participants from tags:', error);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
317
|
// clear the tool batch if the agent is invoked
|
|
362
318
|
// this is to prevent the tool batch from being used for a new agent invocation
|
|
363
319
|
await createToolBatchTracker(
|
|
@@ -394,15 +350,98 @@ export const openbotRuntime =
|
|
|
394
350
|
});
|
|
395
351
|
|
|
396
352
|
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
397
|
-
const { metadata, values } = event.data;
|
|
353
|
+
const { metadata, values, actionId } = event.data;
|
|
354
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
355
|
+
|
|
356
|
+
if (metadata?.type === 'api_provider_selection') {
|
|
357
|
+
const provider = actionId;
|
|
358
|
+
const [_, ...rest] = currentModelString.split('/');
|
|
359
|
+
const currentModelId = rest.join('/');
|
|
360
|
+
|
|
361
|
+
const registry = await fetchRegistry();
|
|
362
|
+
const providerData = registry?.providers[provider as string];
|
|
363
|
+
|
|
364
|
+
const providerLinks: Record<string, string> = {
|
|
365
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
366
|
+
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
367
|
+
google: 'https://aistudio.google.com/app/apikey',
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const label = providerData?.label || (provider as string);
|
|
371
|
+
const link = providerLinks[provider as string] || '';
|
|
372
|
+
|
|
373
|
+
const modelOptions = providerData?.models.map((m) => ({
|
|
374
|
+
label: m.label,
|
|
375
|
+
value: m.id,
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
const defaultModel = modelOptions?.[0]?.value || 'gpt-4o-mini';
|
|
379
|
+
const defaultValue =
|
|
380
|
+
modelOptions?.find((m) => m.value === currentModelId)?.value ||
|
|
381
|
+
currentModelId ||
|
|
382
|
+
defaultModel;
|
|
383
|
+
|
|
384
|
+
yield {
|
|
385
|
+
type: 'client:ui:widget',
|
|
386
|
+
data: {
|
|
387
|
+
widgetId: event.data.widgetId,
|
|
388
|
+
kind: 'message',
|
|
389
|
+
title: 'Provider Selected',
|
|
390
|
+
body: `${label} provider was selected.`,
|
|
391
|
+
state: 'submitted',
|
|
392
|
+
display: 'collapsed',
|
|
393
|
+
disabled: true,
|
|
394
|
+
actions: [],
|
|
395
|
+
},
|
|
396
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
397
|
+
} as OpenBotEvent;
|
|
398
|
+
|
|
399
|
+
yield {
|
|
400
|
+
type: 'client:ui:widget',
|
|
401
|
+
data: {
|
|
402
|
+
kind: 'form',
|
|
403
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
404
|
+
title: `${label} Setup`,
|
|
405
|
+
description: `Enter your API key and select a model.`,
|
|
406
|
+
fields: [
|
|
407
|
+
{
|
|
408
|
+
id: 'model',
|
|
409
|
+
label: 'Model',
|
|
410
|
+
type: modelOptions ? 'select' : 'text',
|
|
411
|
+
description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
|
|
412
|
+
options: modelOptions,
|
|
413
|
+
placeholder: defaultModel,
|
|
414
|
+
required: true,
|
|
415
|
+
defaultValue,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: 'apiKey',
|
|
419
|
+
label: 'API Key',
|
|
420
|
+
type: 'password',
|
|
421
|
+
description: `Get your key here: [${link}](${link})`,
|
|
422
|
+
placeholder: `sk-...`,
|
|
423
|
+
required: true,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
submitLabel: 'Save & Continue',
|
|
427
|
+
metadata: {
|
|
428
|
+
type: 'api_key_request',
|
|
429
|
+
provider,
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
433
|
+
} as OpenBotEvent;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
398
437
|
if (metadata?.type !== 'api_key_request') return;
|
|
399
|
-
if (!values?.apiKey || !values?.
|
|
438
|
+
if (!values?.apiKey || !values?.model) return;
|
|
400
439
|
|
|
401
|
-
const provider = String(values.provider);
|
|
440
|
+
const provider = String(values.provider || metadata.provider);
|
|
402
441
|
const modelId = String(values.model).trim();
|
|
403
442
|
const apiKey = String(values.apiKey);
|
|
404
443
|
|
|
405
|
-
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
444
|
+
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
|
|
406
445
|
yield {
|
|
407
446
|
type: 'agent:output',
|
|
408
447
|
data: { content: `Unsupported provider: ${provider}` },
|
|
@@ -411,7 +450,12 @@ export const openbotRuntime =
|
|
|
411
450
|
return;
|
|
412
451
|
}
|
|
413
452
|
|
|
414
|
-
const envVar =
|
|
453
|
+
const envVar =
|
|
454
|
+
provider === 'openai'
|
|
455
|
+
? 'OPENAI_API_KEY'
|
|
456
|
+
: provider === 'anthropic'
|
|
457
|
+
? 'ANTHROPIC_API_KEY'
|
|
458
|
+
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
415
459
|
const newModelString = `${provider}/${modelId}`;
|
|
416
460
|
|
|
417
461
|
if (!storage) return;
|
|
@@ -460,10 +504,14 @@ export const openbotRuntime =
|
|
|
460
504
|
title: 'API Key Saved',
|
|
461
505
|
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
462
506
|
state: 'submitted',
|
|
463
|
-
|
|
507
|
+
display: 'collapsed',
|
|
508
|
+
disabled: true,
|
|
509
|
+
actions: [],
|
|
464
510
|
},
|
|
465
511
|
meta: { agentId: context.state.agentId },
|
|
466
512
|
};
|
|
513
|
+
|
|
514
|
+
yield* runLLM(context, threadId);
|
|
467
515
|
} catch (error) {
|
|
468
516
|
yield {
|
|
469
517
|
type: 'agent:output',
|
|
@@ -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,7 +20,3 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
21
20
|
'# COMMUNICATION STYLE',
|
|
22
21
|
'- Be always concise, professional, and proactive.',
|
|
23
22
|
].join('\n');
|
|
24
|
-
|
|
25
|
-
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
26
|
-
export const API_KEY_SETUP_MESSAGE =
|
|
27
|
-
'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.';
|