@meetopenbot/openbot 1.2.14 → 1.2.15
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/images.js +115 -22
- package/dist/runtime.js +41 -28
- package/dist/system-prompt.js +7 -0
- package/package.json +1 -1
package/dist/images.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { BYOK_ENV_KEYS, shouldUseCreditsAuth } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
import { expandModelString, isAutoModel, listAutoModelCandidates, } from './auto-model.js';
|
|
2
3
|
export function isImageAttachment(file) {
|
|
3
4
|
if (file.mimeType?.startsWith('image/'))
|
|
4
5
|
return true;
|
|
5
6
|
return /\.(png|jpe?g|gif|webp)$/i.test(file.name);
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
return events.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
function topLevelUserInvokes(events) {
|
|
9
|
+
return events.filter((event) => !event.meta?.parentToolCallId &&
|
|
10
|
+
event.type === 'agent:invoke' &&
|
|
11
|
+
Boolean(event.data?.role) &&
|
|
12
|
+
invokeRole(event.data.role) === 'user');
|
|
13
|
+
}
|
|
14
|
+
/** True when the newest top-level user message carries an image. */
|
|
15
|
+
export function latestUserMessageHasImages(events) {
|
|
16
|
+
const invokes = topLevelUserInvokes(events);
|
|
17
|
+
const latest = invokes[invokes.length - 1];
|
|
18
|
+
return Boolean(latest?.data?.attachments?.some(isImageAttachment));
|
|
19
|
+
}
|
|
20
|
+
/** True when any top-level user message carries an attachment. */
|
|
21
|
+
export function eventsHaveAttachments(events) {
|
|
22
|
+
return topLevelUserInvokes(events).some((event) => Boolean(event.data?.attachments?.length));
|
|
23
|
+
}
|
|
24
|
+
/** Providers whose models in the picker cannot take image input. */
|
|
25
|
+
const NO_IMAGE_INPUT_PROVIDERS = new Set(['deepseek']);
|
|
26
|
+
/** Whether the model for this turn can see images. Auto counts as its concrete pool model. */
|
|
27
|
+
export function modelSupportsImages(modelString, ctx) {
|
|
28
|
+
const provider = expandModelString(modelString, ctx).split('/')[0] ?? '';
|
|
29
|
+
return !NO_IMAGE_INPUT_PROVIDERS.has(provider);
|
|
15
30
|
}
|
|
16
31
|
function hasAnyKey(env, keys) {
|
|
17
32
|
return Boolean(keys?.some((key) => Boolean(env[key]?.trim())));
|
|
@@ -33,8 +48,57 @@ export function pickVisionModel(authMode, env = process.env) {
|
|
|
33
48
|
function invokeRole(role) {
|
|
34
49
|
return role === 'agent' ? 'user' : role;
|
|
35
50
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Models to try this turn. Auto switches to a cheap vision model only when the
|
|
53
|
+
* newest user message has an image and a vision model is available. Never throws.
|
|
54
|
+
*/
|
|
55
|
+
export function chooseTurnCandidates({ model, events, authMode, env = process.env, }) {
|
|
56
|
+
if (!isAutoModel(model))
|
|
57
|
+
return [model.trim()];
|
|
58
|
+
if (latestUserMessageHasImages(events)) {
|
|
59
|
+
const vision = pickVisionModel(authMode, env);
|
|
60
|
+
if (vision)
|
|
61
|
+
return [vision];
|
|
62
|
+
}
|
|
63
|
+
return listAutoModelCandidates({ authMode, env });
|
|
64
|
+
}
|
|
65
|
+
function formatSize(bytes) {
|
|
66
|
+
if (bytes < 1024)
|
|
67
|
+
return `${bytes} B`;
|
|
68
|
+
if (bytes < 1024 * 1024)
|
|
69
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
70
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
71
|
+
}
|
|
72
|
+
function fileType(file) {
|
|
73
|
+
if (file.mimeType)
|
|
74
|
+
return file.mimeType;
|
|
75
|
+
const match = /\.([a-z0-9]+)$/i.exec(file.name);
|
|
76
|
+
return match ? match[1].toLowerCase() : 'file';
|
|
77
|
+
}
|
|
78
|
+
/** Text note listing attached files with their full paths. */
|
|
79
|
+
export function attachmentNote(files) {
|
|
80
|
+
const lines = files.map((file) => {
|
|
81
|
+
const details = [fileType(file)];
|
|
82
|
+
if (typeof file.size === 'number' && file.size > 0)
|
|
83
|
+
details.push(formatSize(file.size));
|
|
84
|
+
const where = file.path ?? "in this thread's attachments";
|
|
85
|
+
const suffix = file.imageNotShown
|
|
86
|
+
? ' — not shown to you; the current model cannot view images'
|
|
87
|
+
: '';
|
|
88
|
+
return `- ${file.name} (${details.join(', ')}): ${where}${suffix}`;
|
|
89
|
+
});
|
|
90
|
+
return [
|
|
91
|
+
'[Attached files, saved on this computer:',
|
|
92
|
+
...lines,
|
|
93
|
+
'You cannot open these files yourself. If a coding agent should use one, give them its full path.]',
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Put attachments on matching user messages. Images go as AI SDK `file` parts
|
|
98
|
+
* when `includeImages` is set and the read succeeds; every other file (and any
|
|
99
|
+
* image not shown) becomes one text note with its full path.
|
|
100
|
+
*/
|
|
101
|
+
export async function addAttachmentParts(messages, events, { includeImages, read, locate, }) {
|
|
38
102
|
const result = [...messages];
|
|
39
103
|
let userCursor = 0;
|
|
40
104
|
const takeNextUserMessageIndex = () => {
|
|
@@ -46,6 +110,23 @@ export async function addImageParts(messages, events, read) {
|
|
|
46
110
|
}
|
|
47
111
|
return -1;
|
|
48
112
|
};
|
|
113
|
+
const safeRead = async (name) => {
|
|
114
|
+
try {
|
|
115
|
+
const loaded = await read(name);
|
|
116
|
+
return loaded?.data.byteLength ? loaded : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const safeLocate = async (name) => {
|
|
123
|
+
try {
|
|
124
|
+
return (await locate(name)) || null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
49
130
|
for (const event of events) {
|
|
50
131
|
if (event.meta?.parentToolCallId)
|
|
51
132
|
continue;
|
|
@@ -58,27 +139,39 @@ export async function addImageParts(messages, events, read) {
|
|
|
58
139
|
if (invokeRole(event.data.role) !== 'user')
|
|
59
140
|
continue;
|
|
60
141
|
const index = takeNextUserMessageIndex();
|
|
61
|
-
|
|
62
|
-
if (index === -1 || files.length === 0)
|
|
142
|
+
if (index === -1 || attachments.length === 0)
|
|
63
143
|
continue;
|
|
64
144
|
const current = result[index];
|
|
65
145
|
const parts = [];
|
|
66
146
|
if (typeof current.content === 'string' && current.content) {
|
|
67
147
|
parts.push({ type: 'text', text: current.content });
|
|
68
148
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
149
|
+
const noted = [];
|
|
150
|
+
for (const file of attachments) {
|
|
151
|
+
const isImage = isImageAttachment(file);
|
|
152
|
+
if (isImage && includeImages) {
|
|
153
|
+
const loaded = await safeRead(file.name);
|
|
154
|
+
if (loaded) {
|
|
155
|
+
parts.push({
|
|
156
|
+
type: 'file',
|
|
157
|
+
data: loaded.data,
|
|
158
|
+
mediaType: loaded.mediaType || 'image/png',
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
noted.push({
|
|
164
|
+
name: file.name,
|
|
165
|
+
mimeType: file.mimeType,
|
|
166
|
+
size: file.size,
|
|
167
|
+
path: await safeLocate(file.name),
|
|
168
|
+
imageNotShown: isImage,
|
|
77
169
|
});
|
|
78
170
|
}
|
|
79
|
-
if (
|
|
171
|
+
if (noted.length > 0)
|
|
172
|
+
parts.push({ type: 'text', text: attachmentNote(noted) });
|
|
173
|
+
if (parts.length > 0)
|
|
80
174
|
result[index] = { role: 'user', content: parts };
|
|
81
|
-
}
|
|
82
175
|
}
|
|
83
176
|
return result;
|
|
84
177
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { streamText, stepCountIs } from 'ai';
|
|
2
2
|
import { BYOK_ENV_KEYS, isAuthErrorMessage, isCreditsErrorMessage, fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
|
|
3
3
|
import { eventsToModelMessages } from './history.js';
|
|
4
|
-
import {
|
|
4
|
+
import { addAttachmentParts, chooseTurnCandidates, eventsHaveAttachments, modelSupportsImages, } from './images.js';
|
|
5
5
|
import { buildContext } from './context.js';
|
|
6
6
|
import { OPENBOT_SYSTEM_PROMPT, withGithubGitInstructions, withUserInstructions, } from './system-prompt.js';
|
|
7
|
-
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError,
|
|
7
|
+
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, missingByokApiKeyMessage, } from './auto-model.js';
|
|
8
8
|
import { reasoningForModel, resolveModel } from './model.js';
|
|
9
9
|
import { errorFromStreamPart } from './stream-error.js';
|
|
10
10
|
import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
|
|
@@ -100,18 +100,39 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
100
100
|
channelId: state.channelId ?? '',
|
|
101
101
|
threadId,
|
|
102
102
|
});
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
103
|
+
const hasAttachments = eventsHaveAttachments(events);
|
|
104
|
+
const candidates = chooseTurnCandidates({ model: configuredModelString, events, authMode });
|
|
105
|
+
const readImage = async (name) => {
|
|
106
|
+
try {
|
|
107
|
+
const file = await storage.readThreadAttachment({
|
|
108
|
+
channelId: state.channelId,
|
|
109
|
+
threadId,
|
|
110
|
+
name,
|
|
111
|
+
});
|
|
112
|
+
return { data: new Uint8Array(file.body), mediaType: file.mimeType };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn('[openbot] could not read attachment', name, error);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const locateAttachment = async (name) => {
|
|
120
|
+
if (typeof storage.getThreadAttachmentStat !== 'function')
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
const stat = await storage.getThreadAttachmentStat({
|
|
124
|
+
channelId: state.channelId,
|
|
125
|
+
threadId,
|
|
126
|
+
name,
|
|
127
|
+
});
|
|
128
|
+
return typeof stat?.abs === 'string' ? stat.abs : null;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.warn('[openbot] could not locate attachment', name, error);
|
|
132
|
+
return null;
|
|
114
133
|
}
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
115
136
|
const missingKey = missingByokApiKeyMessage(candidates[0], { authMode });
|
|
116
137
|
if (missingKey)
|
|
117
138
|
throw new Error(missingKey);
|
|
@@ -128,22 +149,14 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
128
149
|
const tools = toAiTools(toolDefinitions, args, flushPendingOutput);
|
|
129
150
|
try {
|
|
130
151
|
let messages = eventsToModelMessages(events);
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
name,
|
|
138
|
-
});
|
|
139
|
-
return { data: new Uint8Array(file.body), mediaType: file.mimeType };
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
console.warn('[openbot] could not read attachment', name, error);
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
152
|
+
if (hasAttachments) {
|
|
153
|
+
const includeImages = modelSupportsImages(candidate, { authMode });
|
|
154
|
+
messages = await addAttachmentParts(messages, events, {
|
|
155
|
+
includeImages,
|
|
156
|
+
read: readImage,
|
|
157
|
+
locate: locateAttachment,
|
|
145
158
|
});
|
|
146
|
-
console.info('[openbot] attached
|
|
159
|
+
console.info('[openbot] attached files for', candidate, { images: includeImages });
|
|
147
160
|
}
|
|
148
161
|
const result = streamText({
|
|
149
162
|
model: resolveModel(candidate, { authMode }),
|
package/dist/system-prompt.js
CHANGED
|
@@ -50,6 +50,13 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
50
50
|
"- **Hub-and-spoke**: Specialists cannot talk to each other. You pass context from one to the next.",
|
|
51
51
|
"- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.",
|
|
52
52
|
"",
|
|
53
|
+
"# ATTACHMENTS",
|
|
54
|
+
"- Files the human attaches are saved on this computer. Their message lists each one with its full path.",
|
|
55
|
+
"- You see an image only when it is shown to you. You cannot open other files (PDF, documents, spreadsheets, archives, code).",
|
|
56
|
+
"- Never claim you read a file or saw an image you were not shown.",
|
|
57
|
+
"- When a coding agent should use a file, `ask_agent` them with the exact full path(s) and ask them to open the file there. Do not paraphrase or shorten the path.",
|
|
58
|
+
"- If an image was not shown to you and no specialist fits, say the current model cannot view images and suggest picking a vision-capable model.",
|
|
59
|
+
"",
|
|
53
60
|
"# CLOUD COMPUTER",
|
|
54
61
|
"This runtime is a remote cloud computer. The human is in the browser at openbot.one and cannot open localhost, 127.0.0.1, or any port on this machine.",
|
|
55
62
|
"- Never tell them to visit localhost. They need a public HTTPS URL.",
|