@meetopenbot/openbot 0.2.7 → 1.0.1
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/auto-model.js +13 -0
- package/dist/build-tools.js +26 -0
- package/dist/history.js +9 -5
- package/dist/images.js +64 -0
- package/dist/index.js +66 -67
- package/dist/output-buffer.js +31 -0
- package/dist/runtime.js +385 -482
- package/dist/stream-error.js +6 -0
- package/dist/system-prompt.js +4 -0
- package/dist/tools/approval.js +137 -108
- package/dist/tools/ask-agent.js +39 -47
- package/dist/tools/memory.js +41 -73
- package/dist/tools/start-work.js +61 -141
- package/dist/tools/storage.js +62 -424
- package/dist/tools/thread-status.js +22 -38
- package/dist/tools/thread-title.js +57 -0
- package/dist/tools/todo.js +33 -52
- package/dist/types.js +0 -8
- package/package.json +2 -2
- package/dist/tools/bash.js +0 -432
- package/dist/tools/preview.js +0 -269
- package/dist/tools/ui.js +0 -120
- package/dist/utils/workspace-url.js +0 -6
package/dist/runtime.js
CHANGED
|
@@ -1,537 +1,440 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { streamText, stepCountIs } from 'ai';
|
|
2
2
|
import { eventsToModelMessages } from './history.js';
|
|
3
|
+
import { addImageParts, eventsHaveImages, pickVisionModel } from './images.js';
|
|
3
4
|
import { buildContext } from './context.js';
|
|
4
5
|
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
5
6
|
import { isAuthErrorMessage, isCreditsErrorMessage } from '@meetopenbot/plugin-sdk';
|
|
6
|
-
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, } from './auto-model.js';
|
|
7
|
+
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, missingByokApiKeyMessage, } from './auto-model.js';
|
|
7
8
|
import { includeReasoningInHistory, reasoningForModel, resolveModel } from './model.js';
|
|
8
9
|
import { fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
|
|
10
|
+
import { errorFromStreamPart } from './stream-error.js';
|
|
11
|
+
import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
|
|
9
12
|
async function buildSystemPrompt(state, storage) {
|
|
10
13
|
const context = await buildContext(state, storage);
|
|
11
14
|
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
12
|
-
// Hardcoded naming hint logic
|
|
13
15
|
const threadState = state.threadDetails?.state;
|
|
14
16
|
if (!threadState?.isSmartNamed) {
|
|
15
|
-
sections.push('', '## SYSTEM HINT', 'This thread is unnamed. Please use the `
|
|
17
|
+
sections.push('', '## SYSTEM HINT', 'This thread is unnamed. Please use the `set_thread_title` tool to set a concise, descriptive title (e.g., "Project Brainstorming" instead of "project-brainstorm"). Only do this once.');
|
|
16
18
|
}
|
|
17
19
|
return sections.join('\n');
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
21
|
+
function toAiTools(tools, args, flushPendingOutput) {
|
|
22
|
+
const mapped = {};
|
|
23
|
+
for (const [name, def] of Object.entries(tools)) {
|
|
24
|
+
if (!def.execute)
|
|
25
|
+
continue;
|
|
26
|
+
mapped[name] = {
|
|
27
|
+
description: def.description,
|
|
28
|
+
inputSchema: def.inputSchema,
|
|
29
|
+
execute: async (input, { toolCallId }) => {
|
|
30
|
+
await flushPendingOutput();
|
|
31
|
+
await args.emit({
|
|
32
|
+
type: `action:${name}`,
|
|
33
|
+
data: input,
|
|
34
|
+
meta: {
|
|
35
|
+
toolCallId,
|
|
36
|
+
agentId: args.context.agentId,
|
|
37
|
+
threadId: args.threadId,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
const result = await def.execute(input, {
|
|
42
|
+
agentId: args.context.agentId,
|
|
43
|
+
channelId: args.state.channelId,
|
|
44
|
+
threadId: args.threadId,
|
|
45
|
+
runId: args.state.runId,
|
|
46
|
+
toolCallId,
|
|
47
|
+
abortSignal: args.context.abortSignal,
|
|
48
|
+
storage: args.context.storage,
|
|
49
|
+
host: args.context.host,
|
|
50
|
+
publicBaseUrl: args.context.publicBaseUrl,
|
|
51
|
+
config: args.context.config,
|
|
52
|
+
state: args.state,
|
|
53
|
+
emit: args.emit,
|
|
54
|
+
});
|
|
55
|
+
const data = typeof result === 'string'
|
|
56
|
+
? { success: true, output: result }
|
|
57
|
+
: { success: result.success !== false, ...result };
|
|
58
|
+
await args.emit({
|
|
59
|
+
type: `action:${name}:result`,
|
|
60
|
+
data,
|
|
61
|
+
meta: {
|
|
62
|
+
toolCallId,
|
|
63
|
+
agentId: args.context.agentId,
|
|
64
|
+
threadId: args.threadId,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return data.output ?? JSON.stringify(data);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
await args.emit({
|
|
72
|
+
type: `action:${name}:result`,
|
|
73
|
+
data: { success: false, error: message, output: message },
|
|
74
|
+
meta: {
|
|
75
|
+
toolCallId,
|
|
76
|
+
agentId: args.context.agentId,
|
|
77
|
+
threadId: args.threadId,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return message;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return mapped;
|
|
63
86
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
* - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
|
|
68
|
-
* - Tool calls become `action:*` events; plugins emit `:result` when done.
|
|
69
|
-
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
70
|
-
*/
|
|
71
|
-
export const openbotRuntime = (options) => (builder) => {
|
|
72
|
-
const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', agentId, storage, toolDefinitions = {}, abortSignal, host, } = options;
|
|
87
|
+
export async function* runOpenbotTurn(args, options) {
|
|
88
|
+
const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', storage, toolDefinitions = {}, abortSignal, host, } = options;
|
|
73
89
|
let configuredModelString = modelString;
|
|
74
90
|
const isCreditsCloudAgent = (id) => host.isCloudSystemAgent(id ?? '') && authMode === 'credits';
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
91
|
+
if (!storage || abortSignal?.aborted)
|
|
92
|
+
return;
|
|
93
|
+
const state = args.state;
|
|
94
|
+
state.model = configuredModelString;
|
|
95
|
+
const threadId = args.threadId || state.threadId;
|
|
96
|
+
const systemPrompt = await buildSystemPrompt(state, storage);
|
|
97
|
+
const events = await storage.getEvents({
|
|
98
|
+
channelId: state.channelId ?? '',
|
|
99
|
+
threadId,
|
|
100
|
+
});
|
|
101
|
+
const hasImages = eventsHaveImages(events);
|
|
102
|
+
let candidates = isAutoModel(configuredModelString)
|
|
103
|
+
? listAutoModelCandidates({ authMode })
|
|
104
|
+
: [configuredModelString];
|
|
105
|
+
try {
|
|
106
|
+
if (hasImages && isAutoModel(configuredModelString)) {
|
|
107
|
+
const vision = pickVisionModel(authMode);
|
|
108
|
+
if (!vision) {
|
|
109
|
+
throw new Error('Looking at images needs an OpenAI, Google, or Anthropic API key. Auto\'s usual model cannot see pictures.');
|
|
110
|
+
}
|
|
111
|
+
candidates = [vision];
|
|
112
|
+
}
|
|
113
|
+
const missingKey = missingByokApiKeyMessage(candidates[0], { authMode });
|
|
114
|
+
if (missingKey)
|
|
115
|
+
throw new Error(missingKey);
|
|
116
|
+
let lastError;
|
|
117
|
+
let started = false;
|
|
118
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
119
|
+
const candidate = candidates[i];
|
|
120
|
+
const output = createAgentOutputBuffer({ agentId: state.agentId, threadId });
|
|
121
|
+
const flushPendingOutput = async () => {
|
|
122
|
+
const event = output.take();
|
|
123
|
+
if (event)
|
|
124
|
+
await args.emit(event);
|
|
125
|
+
};
|
|
126
|
+
const tools = toAiTools(toolDefinitions, args, flushPendingOutput);
|
|
127
|
+
try {
|
|
128
|
+
let messages = eventsToModelMessages(events, {
|
|
129
|
+
includeReasoning: includeReasoningInHistory(candidate),
|
|
130
|
+
});
|
|
131
|
+
if (hasImages) {
|
|
132
|
+
messages = await addImageParts(messages, events, async (name) => {
|
|
133
|
+
try {
|
|
134
|
+
const file = await storage.readThreadAttachment({
|
|
135
|
+
channelId: state.channelId,
|
|
136
|
+
threadId,
|
|
137
|
+
name,
|
|
138
|
+
});
|
|
139
|
+
return { data: new Uint8Array(file.body), mediaType: file.mimeType };
|
|
121
140
|
}
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.warn('[openbot] could not read attachment', name, error);
|
|
143
|
+
return null;
|
|
124
144
|
}
|
|
125
|
-
}
|
|
145
|
+
});
|
|
146
|
+
console.info('[openbot] attached images for', candidate);
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return [
|
|
165
|
-
{
|
|
166
|
-
text: part.text,
|
|
167
|
-
...(part.providerMetadata ? { providerOptions: part.providerMetadata } : {}),
|
|
168
|
-
},
|
|
169
|
-
];
|
|
170
|
-
});
|
|
171
|
-
const reasoning = result.reasoningText?.trim() || undefined;
|
|
172
|
-
if (result.text || reasoning || reasoningParts.length > 0) {
|
|
173
|
-
yield {
|
|
174
|
-
type: 'agent:output',
|
|
175
|
-
data: {
|
|
176
|
-
content: result.text ?? '',
|
|
177
|
-
...(reasoning ? { reasoning } : {}),
|
|
178
|
-
...(reasoningParts.length > 0 ? { reasoningParts } : {}),
|
|
179
|
-
},
|
|
180
|
-
meta: outputMeta,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
if (toolCalls.length > 0) {
|
|
184
|
-
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
185
|
-
await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
186
|
-
for (const toolCall of toolCalls) {
|
|
187
|
-
yield {
|
|
188
|
-
type: `action:${toolCall.toolName}`,
|
|
189
|
-
data: toolCall.input,
|
|
190
|
-
meta: {
|
|
191
|
-
toolCallId: toolCall.toolCallId,
|
|
192
|
-
...outputMeta,
|
|
193
|
-
},
|
|
194
|
-
};
|
|
148
|
+
const result = streamText({
|
|
149
|
+
model: resolveModel(candidate, { authMode }),
|
|
150
|
+
system: systemPrompt,
|
|
151
|
+
messages,
|
|
152
|
+
tools,
|
|
153
|
+
abortSignal,
|
|
154
|
+
reasoning: reasoningForModel(candidate),
|
|
155
|
+
stopWhen: stepCountIs(20),
|
|
156
|
+
allowSystemInMessages: true,
|
|
157
|
+
});
|
|
158
|
+
for await (const part of result.fullStream) {
|
|
159
|
+
if (abortSignal?.aborted)
|
|
160
|
+
return;
|
|
161
|
+
const streamError = errorFromStreamPart(part);
|
|
162
|
+
if (streamError)
|
|
163
|
+
throw streamError;
|
|
164
|
+
if (part.type === 'text-delta' && part.text) {
|
|
165
|
+
output.addText(part.text);
|
|
166
|
+
yield {
|
|
167
|
+
type: 'agent:output:delta',
|
|
168
|
+
data: { content: part.text },
|
|
169
|
+
meta: { agentId: state.agentId, threadId },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (part.type === 'reasoning-delta' && 'text' in part && part.text) {
|
|
173
|
+
output.addReasoning(part.text);
|
|
174
|
+
yield {
|
|
175
|
+
type: 'agent:output:delta',
|
|
176
|
+
data: { reasoning: part.text },
|
|
177
|
+
meta: { agentId: state.agentId, threadId },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (isOutputBoundaryPart(part.type)) {
|
|
181
|
+
const event = output.take();
|
|
182
|
+
if (event)
|
|
183
|
+
yield event;
|
|
184
|
+
}
|
|
195
185
|
}
|
|
186
|
+
const leftover = output.take();
|
|
187
|
+
if (leftover)
|
|
188
|
+
yield leftover;
|
|
189
|
+
started = true;
|
|
190
|
+
lastError = undefined;
|
|
191
|
+
break;
|
|
196
192
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
// Run was stopped — unwind quietly without surfacing an error.
|
|
204
|
-
if (abortSignal?.aborted)
|
|
205
|
-
return;
|
|
206
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
|
-
if (isCreditsCloudAgent(context.state.agentId)) {
|
|
208
|
-
if (isCreditsErrorMessage(errorMessage)) {
|
|
209
|
-
yield {
|
|
210
|
-
type: 'agent:output',
|
|
211
|
-
data: {
|
|
212
|
-
content: 'Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.',
|
|
213
|
-
},
|
|
214
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
215
|
-
};
|
|
193
|
+
catch (error) {
|
|
194
|
+
lastError = error;
|
|
195
|
+
if (abortSignal?.aborted)
|
|
216
196
|
return;
|
|
197
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
198
|
+
if (isCreditsErrorMessage(errorMessage) || isAuthErrorMessage(errorMessage)) {
|
|
199
|
+
throw error;
|
|
217
200
|
}
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
type: 'agent:output',
|
|
221
|
-
data: {
|
|
222
|
-
content: 'Could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.',
|
|
223
|
-
},
|
|
224
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
225
|
-
};
|
|
226
|
-
return;
|
|
201
|
+
if (!isRetryableModelError(error) || i === candidates.length - 1) {
|
|
202
|
+
throw error;
|
|
227
203
|
}
|
|
228
204
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
205
|
+
}
|
|
206
|
+
if (!started && lastError)
|
|
207
|
+
throw lastError;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (abortSignal?.aborted)
|
|
211
|
+
return;
|
|
212
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
|
+
if (isCreditsCloudAgent(state.agentId)) {
|
|
214
|
+
if (isCreditsErrorMessage(errorMessage)) {
|
|
236
215
|
yield {
|
|
237
|
-
type: '
|
|
216
|
+
type: 'agent:output',
|
|
238
217
|
data: {
|
|
239
|
-
|
|
240
|
-
widgetId: `api_provider_selection_${Date.now()}`,
|
|
241
|
-
title: `Setup AI Provider`,
|
|
242
|
-
description: `Select a provider to continue.`,
|
|
243
|
-
actions: providerActions,
|
|
244
|
-
metadata: {
|
|
245
|
-
type: 'api_provider_selection',
|
|
246
|
-
},
|
|
218
|
+
content: 'Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.',
|
|
247
219
|
},
|
|
248
|
-
meta: { agentId:
|
|
220
|
+
meta: { agentId: state.agentId, threadId },
|
|
249
221
|
};
|
|
250
222
|
return;
|
|
251
223
|
}
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
builder.on('agent:invoke', async function* (event, context) {
|
|
256
|
-
const routedTo = event.data?.agentId;
|
|
257
|
-
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Capture user info from meta if available
|
|
261
|
-
if (event.meta?.userName) {
|
|
262
|
-
context.state.currentUser = {
|
|
263
|
-
userName: event.meta.userName,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
267
|
-
// clear the tool batch if the agent is invoked
|
|
268
|
-
// this is to prevent the tool batch from being used for a new agent invocation
|
|
269
|
-
await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
|
|
270
|
-
yield* runLLM(context, threadId, event);
|
|
271
|
-
});
|
|
272
|
-
// this is to handle the tool results from the tool calls
|
|
273
|
-
// because Melony runtime handles them one by one, thats why we need to record the result
|
|
274
|
-
builder.on('*', async function* (event, context) {
|
|
275
|
-
if (!event.type.endsWith(':result'))
|
|
276
|
-
return;
|
|
277
|
-
if (event.meta?.agentId !== context.state.agentId)
|
|
278
|
-
return;
|
|
279
|
-
const toolCallId = event.meta?.toolCallId;
|
|
280
|
-
// record the result of the tool call
|
|
281
|
-
if (!toolCallId ||
|
|
282
|
-
!(await createToolBatchTracker(context.state, storage, context.state.channelId, event.meta?.threadId || context.state.threadId).recordResult(toolCallId)))
|
|
283
|
-
return;
|
|
284
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
285
|
-
yield* runLLM(context, threadId);
|
|
286
|
-
});
|
|
287
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
288
|
-
const { metadata, values, actionId } = event.data;
|
|
289
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
290
|
-
if (isCreditsCloudAgent(context.state.agentId))
|
|
291
|
-
return;
|
|
292
|
-
if (metadata?.type === 'api_provider_selection') {
|
|
293
|
-
const provider = actionId;
|
|
294
|
-
const configuredModelId = isAutoModel(configuredModelString)
|
|
295
|
-
? ''
|
|
296
|
-
: configuredModelString.split('/').slice(1).join('/');
|
|
297
|
-
const registry = await fetchModelRegistry();
|
|
298
|
-
const providerData = registry?.providers?.[provider];
|
|
299
|
-
const label = providerData?.label || provider;
|
|
300
|
-
const link = PROVIDER_API_KEY_LINKS[provider] || '';
|
|
301
|
-
const modelOptions = getProviderModelOptions(registry, provider);
|
|
302
|
-
yield {
|
|
303
|
-
type: 'client:ui:widget',
|
|
304
|
-
data: {
|
|
305
|
-
widgetId: event.data.widgetId,
|
|
306
|
-
kind: 'message',
|
|
307
|
-
title: 'Provider Selected',
|
|
308
|
-
body: `${label} provider was selected.`,
|
|
309
|
-
state: 'submitted',
|
|
310
|
-
display: 'collapsed',
|
|
311
|
-
disabled: true,
|
|
312
|
-
actions: [],
|
|
313
|
-
},
|
|
314
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
315
|
-
};
|
|
316
|
-
if (modelOptions && modelOptions.length > 0 && !isAutoModel(configuredModelString)) {
|
|
317
|
-
const defaultModelId = modelOptions.find((m) => m.value === configuredModelId)?.value || modelOptions[0].value;
|
|
224
|
+
if (isAuthErrorMessage(errorMessage)) {
|
|
318
225
|
yield {
|
|
319
|
-
type: '
|
|
226
|
+
type: 'agent:output',
|
|
320
227
|
data: {
|
|
321
|
-
|
|
322
|
-
widgetId: `api_model_selection_${Date.now()}`,
|
|
323
|
-
title: 'Select Model',
|
|
324
|
-
description: `Choose a ${label} model.`,
|
|
325
|
-
actions: modelOptions.map((option) => ({
|
|
326
|
-
id: option.value,
|
|
327
|
-
label: option.label,
|
|
328
|
-
variant: option.value === defaultModelId ? 'primary' : 'secondary',
|
|
329
|
-
})),
|
|
330
|
-
metadata: {
|
|
331
|
-
type: 'api_model_selection',
|
|
332
|
-
provider,
|
|
333
|
-
},
|
|
228
|
+
content: 'Could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.',
|
|
334
229
|
},
|
|
335
|
-
meta: { agentId:
|
|
230
|
+
meta: { agentId: state.agentId, threadId },
|
|
336
231
|
};
|
|
337
232
|
return;
|
|
338
233
|
}
|
|
339
|
-
yield {
|
|
340
|
-
type: 'client:ui:widget',
|
|
341
|
-
data: {
|
|
342
|
-
kind: 'form',
|
|
343
|
-
widgetId: `api_key_request_${Date.now()}`,
|
|
344
|
-
title: `${label} Setup`,
|
|
345
|
-
description: isAutoModel(configuredModelString)
|
|
346
|
-
? `Enter your API key to continue.`
|
|
347
|
-
: `Enter your API key and model ID.`,
|
|
348
|
-
fields: [
|
|
349
|
-
...(isAutoModel(configuredModelString)
|
|
350
|
-
? []
|
|
351
|
-
: [
|
|
352
|
-
{
|
|
353
|
-
id: 'model',
|
|
354
|
-
label: 'Model',
|
|
355
|
-
type: 'text',
|
|
356
|
-
description: 'Enter the model ID for this provider.',
|
|
357
|
-
placeholder: provider === 'openai'
|
|
358
|
-
? 'gpt-4o-mini'
|
|
359
|
-
: provider === 'anthropic'
|
|
360
|
-
? 'claude-3-5-sonnet-20241022'
|
|
361
|
-
: provider === 'deepseek'
|
|
362
|
-
? 'deepseek-chat'
|
|
363
|
-
: 'gemini-2.0-flash',
|
|
364
|
-
required: true,
|
|
365
|
-
defaultValue: configuredModelId || '',
|
|
366
|
-
},
|
|
367
|
-
]),
|
|
368
|
-
{
|
|
369
|
-
id: 'apiKey',
|
|
370
|
-
label: 'API Key',
|
|
371
|
-
type: 'password',
|
|
372
|
-
description: `Get your key here: [${link}](${link})`,
|
|
373
|
-
placeholder: `sk-...`,
|
|
374
|
-
required: true,
|
|
375
|
-
},
|
|
376
|
-
],
|
|
377
|
-
submitLabel: 'Save & Continue',
|
|
378
|
-
metadata: {
|
|
379
|
-
type: 'api_key_request',
|
|
380
|
-
provider,
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
384
|
-
};
|
|
385
|
-
return;
|
|
386
234
|
}
|
|
387
|
-
if (
|
|
388
|
-
const provider = String(metadata.provider);
|
|
389
|
-
const modelId = String(actionId).trim();
|
|
235
|
+
if (isAuthErrorMessage(errorMessage) && !isCreditsCloudAgent(state.agentId)) {
|
|
390
236
|
const registry = await fetchModelRegistry();
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
237
|
+
const providerActions = listApiKeyProviders(registry).map((provider) => ({
|
|
238
|
+
id: provider.id,
|
|
239
|
+
label: provider.label,
|
|
240
|
+
variant: 'primary',
|
|
241
|
+
}));
|
|
395
242
|
yield {
|
|
396
243
|
type: 'client:ui:widget',
|
|
397
244
|
data: {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
title:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
disabled: true,
|
|
405
|
-
actions: [],
|
|
245
|
+
kind: 'choice',
|
|
246
|
+
widgetId: `api_provider_selection_${Date.now()}`,
|
|
247
|
+
title: `Setup AI Provider`,
|
|
248
|
+
description: `Select a provider to continue.`,
|
|
249
|
+
actions: providerActions,
|
|
250
|
+
metadata: { type: 'api_provider_selection' },
|
|
406
251
|
},
|
|
407
|
-
meta: { agentId:
|
|
252
|
+
meta: { agentId: state.agentId, threadId },
|
|
408
253
|
};
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export async function* handleOpenbotWidgetResponse(args, options) {
|
|
260
|
+
const event = args.event;
|
|
261
|
+
const { metadata, values, actionId } = event.data;
|
|
262
|
+
const threadId = args.threadId;
|
|
263
|
+
const { host, storage } = options;
|
|
264
|
+
let configuredModelString = options.model ?? AUTO_MODEL_ID;
|
|
265
|
+
const state = args.state;
|
|
266
|
+
if (host.isCloudSystemAgent(state.agentId) && options.authMode === 'credits')
|
|
267
|
+
return;
|
|
268
|
+
if (metadata?.type === 'api_provider_selection') {
|
|
269
|
+
const provider = actionId;
|
|
270
|
+
const configuredModelId = isAutoModel(configuredModelString)
|
|
271
|
+
? ''
|
|
272
|
+
: configuredModelString.split('/').slice(1).join('/');
|
|
273
|
+
const registry = await fetchModelRegistry();
|
|
274
|
+
const providerData = registry?.providers?.[provider];
|
|
275
|
+
const label = providerData?.label || provider;
|
|
276
|
+
const link = PROVIDER_API_KEY_LINKS[provider] || '';
|
|
277
|
+
const modelOptions = getProviderModelOptions(registry, provider);
|
|
278
|
+
yield {
|
|
279
|
+
type: 'client:ui:widget',
|
|
280
|
+
data: {
|
|
281
|
+
widgetId: event.data.widgetId,
|
|
282
|
+
kind: 'message',
|
|
283
|
+
title: 'Provider Selected',
|
|
284
|
+
body: `${label} provider was selected.`,
|
|
285
|
+
state: 'submitted',
|
|
286
|
+
display: 'collapsed',
|
|
287
|
+
disabled: true,
|
|
288
|
+
actions: [],
|
|
289
|
+
},
|
|
290
|
+
meta: { agentId: state.agentId, threadId },
|
|
291
|
+
};
|
|
292
|
+
if (modelOptions && modelOptions.length > 0 && !isAutoModel(configuredModelString)) {
|
|
293
|
+
const defaultModelId = modelOptions.find((m) => m.value === configuredModelId)?.value || modelOptions[0].value;
|
|
409
294
|
yield {
|
|
410
295
|
type: 'client:ui:widget',
|
|
411
296
|
data: {
|
|
412
|
-
kind: '
|
|
413
|
-
widgetId: `
|
|
414
|
-
title:
|
|
415
|
-
description: `
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
placeholder: `sk-...`,
|
|
423
|
-
required: true,
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
submitLabel: 'Save & Continue',
|
|
427
|
-
metadata: {
|
|
428
|
-
type: 'api_key_request',
|
|
429
|
-
provider,
|
|
430
|
-
model: modelId,
|
|
431
|
-
},
|
|
297
|
+
kind: 'choice',
|
|
298
|
+
widgetId: `api_model_selection_${Date.now()}`,
|
|
299
|
+
title: 'Select Model',
|
|
300
|
+
description: `Choose a ${label} model.`,
|
|
301
|
+
actions: modelOptions.map((option) => ({
|
|
302
|
+
id: option.value,
|
|
303
|
+
label: option.label,
|
|
304
|
+
variant: option.value === defaultModelId ? 'primary' : 'secondary',
|
|
305
|
+
})),
|
|
306
|
+
metadata: { type: 'api_model_selection', provider },
|
|
432
307
|
},
|
|
433
|
-
meta: { agentId:
|
|
308
|
+
meta: { agentId: state.agentId, threadId },
|
|
434
309
|
};
|
|
435
310
|
return;
|
|
436
311
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
const envVar = provider === 'openai'
|
|
459
|
-
? 'OPENAI_API_KEY'
|
|
460
|
-
: provider === 'anthropic'
|
|
461
|
-
? 'ANTHROPIC_API_KEY'
|
|
462
|
-
: provider === 'deepseek'
|
|
463
|
-
? 'DEEPSEEK_API_KEY'
|
|
464
|
-
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
465
|
-
const newModelString = keepAuto ? AUTO_MODEL_ID : `${provider}/${modelId}`;
|
|
466
|
-
if (!storage)
|
|
467
|
-
return;
|
|
468
|
-
try {
|
|
469
|
-
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
470
|
-
process.env[envVar] = apiKey;
|
|
471
|
-
if (!keepAuto) {
|
|
472
|
-
configuredModelString = newModelString;
|
|
473
|
-
}
|
|
474
|
-
try {
|
|
475
|
-
if (!keepAuto) {
|
|
476
|
-
host.saveConfig({ model: configuredModelString });
|
|
477
|
-
}
|
|
478
|
-
const details = await storage.getAgentDetails({ agentId: context.state.agentId });
|
|
479
|
-
const updatedPlugins = details.pluginRefs.map((ref) => {
|
|
480
|
-
if (ref.id === host.openbotPluginId) {
|
|
481
|
-
return {
|
|
482
|
-
...ref,
|
|
483
|
-
config: {
|
|
484
|
-
...ref.config,
|
|
485
|
-
...(keepAuto ? {} : { model: configuredModelString }),
|
|
486
|
-
...(host.isCloudSystemAgent(context.state.agentId) ? { authMode: 'byok' } : {}),
|
|
312
|
+
yield {
|
|
313
|
+
type: 'client:ui:widget',
|
|
314
|
+
data: {
|
|
315
|
+
kind: 'form',
|
|
316
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
317
|
+
title: `${label} Setup`,
|
|
318
|
+
description: isAutoModel(configuredModelString)
|
|
319
|
+
? `Enter your API key to continue.`
|
|
320
|
+
: `Enter your API key and model ID.`,
|
|
321
|
+
fields: [
|
|
322
|
+
...(isAutoModel(configuredModelString)
|
|
323
|
+
? []
|
|
324
|
+
: [
|
|
325
|
+
{
|
|
326
|
+
id: 'model',
|
|
327
|
+
label: 'Model',
|
|
328
|
+
type: 'text',
|
|
329
|
+
required: true,
|
|
330
|
+
defaultValue: configuredModelId || '',
|
|
487
331
|
},
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
332
|
+
]),
|
|
333
|
+
{
|
|
334
|
+
id: 'apiKey',
|
|
335
|
+
label: 'API Key',
|
|
336
|
+
type: 'password',
|
|
337
|
+
description: `Get your key here: [${link}](${link})`,
|
|
338
|
+
required: true,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
submitLabel: 'Save & Continue',
|
|
342
|
+
metadata: { type: 'api_key_request', provider },
|
|
343
|
+
},
|
|
344
|
+
meta: { agentId: state.agentId, threadId },
|
|
345
|
+
};
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (metadata?.type === 'api_model_selection') {
|
|
349
|
+
const provider = String(metadata.provider);
|
|
350
|
+
const modelId = String(actionId).trim();
|
|
351
|
+
const registry = await fetchModelRegistry();
|
|
352
|
+
const providerData = registry?.providers?.[provider];
|
|
353
|
+
const label = providerData?.label || provider;
|
|
354
|
+
const link = PROVIDER_API_KEY_LINKS[provider] || '';
|
|
355
|
+
yield {
|
|
356
|
+
type: 'client:ui:widget',
|
|
357
|
+
data: {
|
|
358
|
+
widgetId: event.data.widgetId,
|
|
359
|
+
kind: 'message',
|
|
360
|
+
title: 'Model Selected',
|
|
361
|
+
body: `${modelId} was selected.`,
|
|
362
|
+
state: 'submitted',
|
|
363
|
+
display: 'collapsed',
|
|
364
|
+
disabled: true,
|
|
365
|
+
actions: [],
|
|
366
|
+
},
|
|
367
|
+
meta: { agentId: state.agentId, threadId },
|
|
368
|
+
};
|
|
369
|
+
yield {
|
|
370
|
+
type: 'client:ui:widget',
|
|
371
|
+
data: {
|
|
372
|
+
kind: 'form',
|
|
373
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
374
|
+
title: `${label} Setup`,
|
|
375
|
+
description: `Enter your API key to continue.`,
|
|
376
|
+
fields: [
|
|
377
|
+
{
|
|
378
|
+
id: 'apiKey',
|
|
379
|
+
label: 'API Key',
|
|
380
|
+
type: 'password',
|
|
381
|
+
description: `Get your key here: [${link}](${link})`,
|
|
382
|
+
required: true,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
submitLabel: 'Save & Continue',
|
|
386
|
+
metadata: { type: 'api_key_request', provider, model: modelId },
|
|
387
|
+
},
|
|
388
|
+
meta: { agentId: state.agentId, threadId },
|
|
389
|
+
};
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (metadata?.type !== 'api_key_request' || !values?.apiKey || !storage)
|
|
393
|
+
return;
|
|
394
|
+
const provider = String(values.provider || metadata.provider);
|
|
395
|
+
const keepAuto = isAutoModel(configuredModelString);
|
|
396
|
+
const modelId = String(values.model || metadata.model || '').trim();
|
|
397
|
+
if (!keepAuto && !modelId)
|
|
398
|
+
return;
|
|
399
|
+
const apiKey = String(values.apiKey);
|
|
400
|
+
if (provider !== 'openai' &&
|
|
401
|
+
provider !== 'anthropic' &&
|
|
402
|
+
provider !== 'google' &&
|
|
403
|
+
provider !== 'deepseek') {
|
|
404
|
+
yield {
|
|
405
|
+
type: 'agent:output',
|
|
406
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
407
|
+
meta: { agentId: state.agentId },
|
|
408
|
+
};
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const envVar = provider === 'openai'
|
|
412
|
+
? 'OPENAI_API_KEY'
|
|
413
|
+
: provider === 'anthropic'
|
|
414
|
+
? 'ANTHROPIC_API_KEY'
|
|
415
|
+
: provider === 'deepseek'
|
|
416
|
+
? 'DEEPSEEK_API_KEY'
|
|
417
|
+
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
418
|
+
const newModelString = keepAuto ? AUTO_MODEL_ID : `${provider}/${modelId}`;
|
|
419
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
420
|
+
process.env[envVar] = apiKey;
|
|
421
|
+
if (!keepAuto) {
|
|
422
|
+
configuredModelString = newModelString;
|
|
423
|
+
try {
|
|
424
|
+
host.saveConfig({ model: configuredModelString });
|
|
526
425
|
}
|
|
527
|
-
catch
|
|
528
|
-
|
|
529
|
-
type: 'agent:output',
|
|
530
|
-
data: {
|
|
531
|
-
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
532
|
-
},
|
|
533
|
-
meta: { agentId: context.state.agentId },
|
|
534
|
-
};
|
|
426
|
+
catch {
|
|
427
|
+
/* ignore */
|
|
535
428
|
}
|
|
536
|
-
}
|
|
537
|
-
|
|
429
|
+
}
|
|
430
|
+
yield {
|
|
431
|
+
type: 'agent:output',
|
|
432
|
+
data: {
|
|
433
|
+
content: keepAuto
|
|
434
|
+
? `Saved ${provider} API key. Auto will use it when that provider is the cheapest available.`
|
|
435
|
+
: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
436
|
+
},
|
|
437
|
+
meta: { agentId: state.agentId },
|
|
438
|
+
};
|
|
439
|
+
yield* runOpenbotTurn(args, { ...options, model: configuredModelString });
|
|
440
|
+
}
|