bunnyquery 1.2.3 → 1.3.0
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/README.md +78 -8
- package/bunnyquery.css +222 -165
- package/bunnyquery.js +4556 -3545
- package/dist/engine.cjs +1942 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.mts +475 -0
- package/dist/engine.d.ts +475 -0
- package/dist/engine.mjs +1884 -0
- package/dist/engine.mjs.map +1 -0
- package/package.json +24 -3
- package/src/engine/budget.ts +76 -0
- package/src/engine/config.ts +49 -0
- package/src/engine/errors.ts +58 -0
- package/src/engine/history.ts +117 -0
- package/src/engine/host.ts +110 -0
- package/src/engine/index.ts +74 -0
- package/src/engine/links.ts +74 -0
- package/src/engine/office.ts +105 -0
- package/src/engine/prompts/chat_system_prompt.ts +51 -0
- package/src/engine/prompts/index.ts +14 -0
- package/src/engine/prompts/indexing_system_prompt.ts +37 -0
- package/src/engine/prompts/indexing_user_message.ts +62 -0
- package/src/engine/requests.ts +682 -0
- package/src/engine/session.ts +918 -0
- package/src/widget.css +874 -0
- package/styles/chat.css +194 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI request builders + dispatch transport (framework-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Ported from www.skapi.com/src/code/ai_agent.ts. The only changes vs the
|
|
5
|
+
* original are dependency-injection seams:
|
|
6
|
+
* - `skapi.clientSecretRequest*` -> chatEngineConfig().clientSecretRequest*
|
|
7
|
+
* - MCP endpoint URL -> chatEngineConfig().mcpBaseUrl
|
|
8
|
+
* - `poll` on each request -> pollOpt() (set per consumer; see config.ts)
|
|
9
|
+
* - Vue `reactive`/`ref` removed (bgTaskQueue/agentViewMounted are app-level
|
|
10
|
+
* state that stays in the consumer, not here;
|
|
11
|
+
* only the BgTaskEntry TYPE lives here)
|
|
12
|
+
*/
|
|
13
|
+
import { buildIndexingSystemPrompt, buildIndexingUserMessage } from './prompts';
|
|
14
|
+
import { isOfficeFile, makeExtractPlaceholder, type ExtractDirective } from './office';
|
|
15
|
+
import { chatEngineConfig, pollOpt } from './config';
|
|
16
|
+
|
|
17
|
+
export const ANTHROPIC_MESSAGES_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
18
|
+
const ANTHROPIC_MODELS_API_URL = 'https://api.anthropic.com/v1/models';
|
|
19
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
20
|
+
const ANTHROPIC_MCP_BETA = 'mcp-client-2025-11-20';
|
|
21
|
+
const ANTHROPIC_WEB_FETCH_BETA = 'web-fetch-2025-09-10';
|
|
22
|
+
const ANTHROPIC_PROMPT_CACHING_BETA = 'prompt-caching-2024-07-31';
|
|
23
|
+
const ANTHROPIC_BETA_HEADER = `${ANTHROPIC_MCP_BETA},${ANTHROPIC_WEB_FETCH_BETA},${ANTHROPIC_PROMPT_CACHING_BETA}`;
|
|
24
|
+
const WEB_FETCH_MAX_USES = 40;
|
|
25
|
+
const WEB_FETCH_MAX_CONTENT_TOKENS = 200000;
|
|
26
|
+
|
|
27
|
+
export const OPENAI_RESPONSES_API_URL = 'https://api.openai.com/v1/responses';
|
|
28
|
+
const OPENAI_MODELS_API_URL = 'https://api.openai.com/v1/models';
|
|
29
|
+
const MAX_TOKENS = 25000;
|
|
30
|
+
const DEFAULT_OPENAI_IMAGE_DETAIL = 'auto';
|
|
31
|
+
const OPENAI_WEB_SEARCH_ENABLED = true;
|
|
32
|
+
const OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
|
|
33
|
+
export const MCP_NAME = 'BunnyQuery';
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
36
|
+
export const DEFAULT_OPENAI_MODEL = 'gpt-5.4';
|
|
37
|
+
|
|
38
|
+
const mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
39
|
+
const clientSecretRequest = (opts: any) => chatEngineConfig().clientSecretRequest(opts);
|
|
40
|
+
|
|
41
|
+
const getOpenAIImageDetail = (model?: string) => {
|
|
42
|
+
const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
|
|
43
|
+
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
|
|
44
|
+
if (!match) {
|
|
45
|
+
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const major = Number(match[1]);
|
|
49
|
+
const minor = match[2] === undefined ? null : Number(match[2]);
|
|
50
|
+
|
|
51
|
+
if (major > 5) {
|
|
52
|
+
return 'original';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (major === 5 && minor !== null && minor >= 4) {
|
|
56
|
+
return 'original';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ClaudeRole = 'user' | 'assistant';
|
|
63
|
+
|
|
64
|
+
export type ClaudeMessage = {
|
|
65
|
+
role: ClaudeRole;
|
|
66
|
+
content: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type OpenAIMessage = {
|
|
70
|
+
role: ClaudeRole;
|
|
71
|
+
content: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type ClaudeMcpToolConfig = {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
defer_loading?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ClaudeMcpServerRequest = {
|
|
80
|
+
name: string;
|
|
81
|
+
url: string;
|
|
82
|
+
authorizationToken?: string;
|
|
83
|
+
defaultConfig?: ClaudeMcpToolConfig;
|
|
84
|
+
configs?: Record<string, ClaudeMcpToolConfig>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const IMAGE_URL_REGEX =
|
|
88
|
+
/\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
|
|
89
|
+
|
|
90
|
+
export function transformContentWithImages(
|
|
91
|
+
content: string,
|
|
92
|
+
): string | Array<Record<string, any>> {
|
|
93
|
+
if (typeof content !== 'string' || !content) {
|
|
94
|
+
return content;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const matches = content.match(IMAGE_URL_REGEX);
|
|
98
|
+
|
|
99
|
+
if (!matches || !matches.length) {
|
|
100
|
+
return content;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
const imageBlocks: Array<Record<string, any>> = [];
|
|
105
|
+
|
|
106
|
+
for (const url of matches) {
|
|
107
|
+
if (seen.has(url)) continue;
|
|
108
|
+
seen.add(url);
|
|
109
|
+
imageBlocks.push({
|
|
110
|
+
type: 'image',
|
|
111
|
+
source: { type: 'url', url },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [...imageBlocks, { type: 'text', text: content }];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function prepareClaudeMessages(messages: ClaudeMessage[]) {
|
|
119
|
+
if (!messages.length) return messages;
|
|
120
|
+
// Only transform the most recent user message. Historical user messages
|
|
121
|
+
// may reference image URLs that are now stale (deleted, moved, expired).
|
|
122
|
+
const lastIndex = messages.length - 1;
|
|
123
|
+
const last = messages[lastIndex];
|
|
124
|
+
if (last.role !== 'user') return messages;
|
|
125
|
+
const content = transformContentWithImages(last.content);
|
|
126
|
+
if (content === last.content) return messages;
|
|
127
|
+
const next = messages.slice();
|
|
128
|
+
next[lastIndex] = { role: last.role, content } as unknown as ClaudeMessage;
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function transformContentWithOpenAIImages(
|
|
133
|
+
content: string,
|
|
134
|
+
detail = DEFAULT_OPENAI_IMAGE_DETAIL,
|
|
135
|
+
): string | Array<Record<string, any>> {
|
|
136
|
+
if (typeof content !== 'string' || !content) {
|
|
137
|
+
return content;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const matches = content.match(IMAGE_URL_REGEX);
|
|
141
|
+
|
|
142
|
+
if (!matches || !matches.length) {
|
|
143
|
+
return content;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const seen = new Set<string>();
|
|
147
|
+
const imageBlocks: Array<Record<string, any>> = [];
|
|
148
|
+
|
|
149
|
+
for (const url of matches) {
|
|
150
|
+
if (seen.has(url)) continue;
|
|
151
|
+
seen.add(url);
|
|
152
|
+
imageBlocks.push({
|
|
153
|
+
type: 'input_image',
|
|
154
|
+
image_url: url,
|
|
155
|
+
detail,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [{ type: 'input_text', text: content }, ...imageBlocks];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function prepareOpenAIMessages(
|
|
163
|
+
messages: OpenAIMessage[],
|
|
164
|
+
detail = DEFAULT_OPENAI_IMAGE_DETAIL,
|
|
165
|
+
) {
|
|
166
|
+
if (!messages.length) return messages;
|
|
167
|
+
const lastIndex = messages.length - 1;
|
|
168
|
+
const last = messages[lastIndex];
|
|
169
|
+
if (last.role !== 'user') return messages;
|
|
170
|
+
const content = transformContentWithOpenAIImages(last.content, detail);
|
|
171
|
+
if (content === last.content) return messages;
|
|
172
|
+
const next = messages.slice();
|
|
173
|
+
next[lastIndex] = { role: last.role, content } as unknown as OpenAIMessage;
|
|
174
|
+
return next;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Attach a cache_control breakpoint to the last message of the stable history
|
|
178
|
+
// prefix (everything except the final user turn) so Anthropic re-uses it at
|
|
179
|
+
// ~10% input-token billing.
|
|
180
|
+
function applyHistoryCacheBreakpoint(messages: any[]): any[] {
|
|
181
|
+
if (messages.length < 2) return messages;
|
|
182
|
+
const breakpointIndex = messages.length - 2;
|
|
183
|
+
return messages.map((m, i) => {
|
|
184
|
+
if (i !== breakpointIndex) return m;
|
|
185
|
+
const blocks = Array.isArray(m.content)
|
|
186
|
+
? m.content.slice()
|
|
187
|
+
: [{ type: 'text', text: m.content }];
|
|
188
|
+
if (!blocks.length) return m;
|
|
189
|
+
const lastBlockIndex = blocks.length - 1;
|
|
190
|
+
blocks[lastBlockIndex] = {
|
|
191
|
+
...blocks[lastBlockIndex],
|
|
192
|
+
cache_control: { type: 'ephemeral' },
|
|
193
|
+
};
|
|
194
|
+
return { ...m, content: blocks };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type CallClaudeWithMcpParams = {
|
|
199
|
+
prompt: string;
|
|
200
|
+
messages?: ClaudeMessage[];
|
|
201
|
+
service: string;
|
|
202
|
+
owner: string;
|
|
203
|
+
userId?: string;
|
|
204
|
+
model?: string;
|
|
205
|
+
maxTokens?: number;
|
|
206
|
+
system?: string;
|
|
207
|
+
mcpServer: ClaudeMcpServerRequest;
|
|
208
|
+
extractContent?: ExtractDirective[];
|
|
209
|
+
onResponse?: (res: any) => void;
|
|
210
|
+
onError?: (err: any) => void;
|
|
211
|
+
};
|
|
212
|
+
export const POLL_INTERVAL = 1500;
|
|
213
|
+
export async function callClaudeWithMcp({
|
|
214
|
+
prompt,
|
|
215
|
+
messages,
|
|
216
|
+
service,
|
|
217
|
+
owner,
|
|
218
|
+
userId,
|
|
219
|
+
model = DEFAULT_CLAUDE_MODEL,
|
|
220
|
+
maxTokens = 1000,
|
|
221
|
+
system,
|
|
222
|
+
mcpServer,
|
|
223
|
+
extractContent,
|
|
224
|
+
}: CallClaudeWithMcpParams) {
|
|
225
|
+
const mcpServerDefinition: Record<string, any> = {
|
|
226
|
+
type: 'url',
|
|
227
|
+
name: mcpServer.name,
|
|
228
|
+
url: mcpServer.url,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (mcpServer.authorizationToken) {
|
|
232
|
+
mcpServerDefinition.authorization_token = mcpServer.authorizationToken;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return clientSecretRequest({
|
|
236
|
+
clientSecretName: 'claude',
|
|
237
|
+
queue: userId || service,
|
|
238
|
+
service,
|
|
239
|
+
owner,
|
|
240
|
+
...pollOpt(),
|
|
241
|
+
url: ANTHROPIC_MESSAGES_API_URL,
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'content-type': 'application/json',
|
|
245
|
+
'x-api-key': '$CLIENT_SECRET',
|
|
246
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
247
|
+
'anthropic-beta': ANTHROPIC_BETA_HEADER,
|
|
248
|
+
},
|
|
249
|
+
data: {
|
|
250
|
+
model,
|
|
251
|
+
max_tokens: maxTokens,
|
|
252
|
+
...(extractContent && extractContent.length
|
|
253
|
+
? { _skapi_extract: extractContent }
|
|
254
|
+
: {}),
|
|
255
|
+
...(system
|
|
256
|
+
? {
|
|
257
|
+
system: [
|
|
258
|
+
{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: system,
|
|
261
|
+
cache_control: { type: 'ephemeral' },
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
266
|
+
messages: (() => {
|
|
267
|
+
const prepared =
|
|
268
|
+
messages && messages.length
|
|
269
|
+
? prepareClaudeMessages(messages)
|
|
270
|
+
: [
|
|
271
|
+
{
|
|
272
|
+
role: 'user',
|
|
273
|
+
content: transformContentWithImages(prompt),
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
return applyHistoryCacheBreakpoint(prepared as any[]);
|
|
277
|
+
})(),
|
|
278
|
+
mcp_servers: [mcpServerDefinition],
|
|
279
|
+
tools: [
|
|
280
|
+
{
|
|
281
|
+
type: 'mcp_toolset',
|
|
282
|
+
mcp_server_name: mcpServer.name,
|
|
283
|
+
...(mcpServer.defaultConfig
|
|
284
|
+
? { default_config: mcpServer.defaultConfig }
|
|
285
|
+
: {}),
|
|
286
|
+
...(mcpServer.configs ? { configs: mcpServer.configs } : {}),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'web_fetch_20250910',
|
|
290
|
+
name: 'web_fetch',
|
|
291
|
+
max_uses: WEB_FETCH_MAX_USES,
|
|
292
|
+
citations: { enabled: true },
|
|
293
|
+
max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function callClaudeWithPublicMcp(
|
|
301
|
+
prompt: string,
|
|
302
|
+
service: string,
|
|
303
|
+
owner: string,
|
|
304
|
+
messages?: ClaudeMessage[],
|
|
305
|
+
system?: string,
|
|
306
|
+
model?: string,
|
|
307
|
+
userId?: string,
|
|
308
|
+
extractContent?: ExtractDirective[],
|
|
309
|
+
onResponse?: (res: any) => void,
|
|
310
|
+
onError?: (err: any) => void,
|
|
311
|
+
) {
|
|
312
|
+
return callClaudeWithMcp({
|
|
313
|
+
prompt,
|
|
314
|
+
messages,
|
|
315
|
+
service,
|
|
316
|
+
owner,
|
|
317
|
+
userId,
|
|
318
|
+
model: model || DEFAULT_CLAUDE_MODEL,
|
|
319
|
+
maxTokens: MAX_TOKENS,
|
|
320
|
+
system,
|
|
321
|
+
extractContent,
|
|
322
|
+
mcpServer: {
|
|
323
|
+
name: MCP_NAME,
|
|
324
|
+
url: mcpUrl(),
|
|
325
|
+
authorizationToken: '$ACCESS_TOKEN',
|
|
326
|
+
},
|
|
327
|
+
onResponse,
|
|
328
|
+
onError,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function callOpenAIWithPublicMcp(
|
|
333
|
+
prompt: string,
|
|
334
|
+
service: string,
|
|
335
|
+
owner: string,
|
|
336
|
+
messages?: OpenAIMessage[],
|
|
337
|
+
system?: string,
|
|
338
|
+
model?: string,
|
|
339
|
+
userId?: string,
|
|
340
|
+
extractContent?: ExtractDirective[],
|
|
341
|
+
onResponse?: (res: any) => void,
|
|
342
|
+
onError?: (err: any) => void,
|
|
343
|
+
) {
|
|
344
|
+
const resolvedModel = model || DEFAULT_OPENAI_MODEL;
|
|
345
|
+
const imageDetail = getOpenAIImageDetail(resolvedModel);
|
|
346
|
+
const messageList =
|
|
347
|
+
messages && messages.length
|
|
348
|
+
? prepareOpenAIMessages(messages, imageDetail)
|
|
349
|
+
: [
|
|
350
|
+
{
|
|
351
|
+
role: 'user' as const,
|
|
352
|
+
content: transformContentWithOpenAIImages(prompt, imageDetail),
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
const responseInput = [
|
|
357
|
+
...(system
|
|
358
|
+
? [
|
|
359
|
+
{
|
|
360
|
+
role: 'system',
|
|
361
|
+
content: system,
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
: []),
|
|
365
|
+
...messageList.map((m) => ({
|
|
366
|
+
role: m.role,
|
|
367
|
+
content: m.content,
|
|
368
|
+
})),
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
return clientSecretRequest({
|
|
372
|
+
clientSecretName: 'openai',
|
|
373
|
+
queue: userId || service,
|
|
374
|
+
service,
|
|
375
|
+
owner,
|
|
376
|
+
...pollOpt(),
|
|
377
|
+
url: OPENAI_RESPONSES_API_URL,
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: {
|
|
380
|
+
'content-type': 'application/json',
|
|
381
|
+
Authorization: 'Bearer $CLIENT_SECRET',
|
|
382
|
+
},
|
|
383
|
+
data: {
|
|
384
|
+
model: resolvedModel,
|
|
385
|
+
max_output_tokens: MAX_TOKENS,
|
|
386
|
+
...(extractContent && extractContent.length
|
|
387
|
+
? { _skapi_extract: extractContent }
|
|
388
|
+
: {}),
|
|
389
|
+
input: responseInput,
|
|
390
|
+
tools: [
|
|
391
|
+
{
|
|
392
|
+
type: 'mcp',
|
|
393
|
+
server_label: MCP_NAME,
|
|
394
|
+
server_url: mcpUrl(),
|
|
395
|
+
require_approval: 'never',
|
|
396
|
+
headers: {
|
|
397
|
+
Authorization: 'Bearer $ACCESS_TOKEN',
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
...(OPENAI_WEB_SEARCH_ENABLED
|
|
401
|
+
? [
|
|
402
|
+
{
|
|
403
|
+
type: 'web_search',
|
|
404
|
+
external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS,
|
|
405
|
+
},
|
|
406
|
+
]
|
|
407
|
+
: []),
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export type AttachmentSaveInfo = {
|
|
414
|
+
platform: 'claude' | 'openai';
|
|
415
|
+
model?: string;
|
|
416
|
+
service: string;
|
|
417
|
+
owner: string;
|
|
418
|
+
userId?: string;
|
|
419
|
+
serviceName?: string;
|
|
420
|
+
serviceDescription?: string;
|
|
421
|
+
attachment: {
|
|
422
|
+
name: string;
|
|
423
|
+
storagePath: string;
|
|
424
|
+
mime?: string;
|
|
425
|
+
size?: number;
|
|
426
|
+
url: string;
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Background "save into knowledge" call (not a chat turn). Office files get the
|
|
431
|
+
// _skapi_extract directive + an inline-content placeholder instead of a URL.
|
|
432
|
+
export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
|
|
433
|
+
const { platform, service, owner, attachment } = info;
|
|
434
|
+
|
|
435
|
+
const office = isOfficeFile(attachment.name, attachment.mime);
|
|
436
|
+
const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : undefined;
|
|
437
|
+
const extractContent: ExtractDirective[] | undefined =
|
|
438
|
+
office && placeholder
|
|
439
|
+
? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }]
|
|
440
|
+
: undefined;
|
|
441
|
+
const skapiExtract =
|
|
442
|
+
extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
|
|
443
|
+
|
|
444
|
+
const userMessage = buildIndexingUserMessage(
|
|
445
|
+
attachment,
|
|
446
|
+
placeholder ? { inlineContentPlaceholder: placeholder } : undefined,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const systemPrompt = buildIndexingSystemPrompt({
|
|
450
|
+
service,
|
|
451
|
+
serviceName: info.serviceName,
|
|
452
|
+
serviceDescription: info.serviceDescription,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (platform === 'openai') {
|
|
456
|
+
const resolvedModel = info.model || DEFAULT_OPENAI_MODEL;
|
|
457
|
+
const imageDetail = getOpenAIImageDetail(resolvedModel);
|
|
458
|
+
return clientSecretRequest({
|
|
459
|
+
clientSecretName: 'openai',
|
|
460
|
+
queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
|
|
461
|
+
service,
|
|
462
|
+
owner,
|
|
463
|
+
...pollOpt(),
|
|
464
|
+
url: OPENAI_RESPONSES_API_URL,
|
|
465
|
+
method: 'POST',
|
|
466
|
+
headers: {
|
|
467
|
+
'content-type': 'application/json',
|
|
468
|
+
Authorization: 'Bearer $CLIENT_SECRET',
|
|
469
|
+
},
|
|
470
|
+
data: {
|
|
471
|
+
model: resolvedModel,
|
|
472
|
+
max_output_tokens: MAX_TOKENS,
|
|
473
|
+
...skapiExtract,
|
|
474
|
+
input: [
|
|
475
|
+
{ role: 'system', content: systemPrompt },
|
|
476
|
+
{
|
|
477
|
+
role: 'user',
|
|
478
|
+
content: transformContentWithOpenAIImages(userMessage, imageDetail),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
tools: [
|
|
482
|
+
{
|
|
483
|
+
type: 'mcp',
|
|
484
|
+
server_label: MCP_NAME,
|
|
485
|
+
server_url: mcpUrl(),
|
|
486
|
+
require_approval: 'never',
|
|
487
|
+
headers: { Authorization: 'Bearer $ACCESS_TOKEN' },
|
|
488
|
+
},
|
|
489
|
+
...(OPENAI_WEB_SEARCH_ENABLED
|
|
490
|
+
? [
|
|
491
|
+
{
|
|
492
|
+
type: 'web_search',
|
|
493
|
+
external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS,
|
|
494
|
+
},
|
|
495
|
+
]
|
|
496
|
+
: []),
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
|
|
503
|
+
return clientSecretRequest({
|
|
504
|
+
clientSecretName: 'claude',
|
|
505
|
+
queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
|
|
506
|
+
service,
|
|
507
|
+
owner,
|
|
508
|
+
...pollOpt(),
|
|
509
|
+
url: ANTHROPIC_MESSAGES_API_URL,
|
|
510
|
+
method: 'POST',
|
|
511
|
+
headers: {
|
|
512
|
+
'content-type': 'application/json',
|
|
513
|
+
'x-api-key': '$CLIENT_SECRET',
|
|
514
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
515
|
+
'anthropic-beta': ANTHROPIC_BETA_HEADER,
|
|
516
|
+
},
|
|
517
|
+
data: {
|
|
518
|
+
model: resolvedModel,
|
|
519
|
+
max_tokens: MAX_TOKENS,
|
|
520
|
+
...skapiExtract,
|
|
521
|
+
system: [
|
|
522
|
+
{
|
|
523
|
+
type: 'text',
|
|
524
|
+
text: systemPrompt,
|
|
525
|
+
cache_control: { type: 'ephemeral' },
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
messages: [
|
|
529
|
+
{
|
|
530
|
+
role: 'user',
|
|
531
|
+
content: transformContentWithImages(userMessage),
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
mcp_servers: [
|
|
535
|
+
{
|
|
536
|
+
type: 'url',
|
|
537
|
+
name: MCP_NAME,
|
|
538
|
+
url: mcpUrl(),
|
|
539
|
+
authorization_token: '$ACCESS_TOKEN',
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
tools: [
|
|
543
|
+
{
|
|
544
|
+
type: 'mcp_toolset',
|
|
545
|
+
mcp_server_name: MCP_NAME,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
type: 'web_fetch_20250910',
|
|
549
|
+
name: 'web_fetch',
|
|
550
|
+
max_uses: WEB_FETCH_MAX_USES,
|
|
551
|
+
citations: { enabled: true },
|
|
552
|
+
max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function extractClaudeText(response: any) {
|
|
560
|
+
if (!Array.isArray(response?.content)) {
|
|
561
|
+
return '';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return response.content
|
|
565
|
+
.filter((block: any) => block?.type === 'text')
|
|
566
|
+
.map((block: any) => block.text)
|
|
567
|
+
.join('\n');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function extractOpenAIText(response: any) {
|
|
571
|
+
if (
|
|
572
|
+
typeof response?.output_text === 'string' &&
|
|
573
|
+
response.output_text.length
|
|
574
|
+
) {
|
|
575
|
+
return response.output_text;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (Array.isArray(response?.output)) {
|
|
579
|
+
const text = response.output
|
|
580
|
+
.flatMap((item: any) => item?.content || [])
|
|
581
|
+
.filter((part: any) => part?.type === 'output_text')
|
|
582
|
+
.map((part: any) => part.text || '')
|
|
583
|
+
.join('\n')
|
|
584
|
+
.trim();
|
|
585
|
+
|
|
586
|
+
if (text) {
|
|
587
|
+
return text;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const content = response?.choices?.[0]?.message?.content;
|
|
592
|
+
|
|
593
|
+
if (typeof content === 'string') {
|
|
594
|
+
return content;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (Array.isArray(content)) {
|
|
598
|
+
return content
|
|
599
|
+
.map((part: any) => {
|
|
600
|
+
if (typeof part === 'string') {
|
|
601
|
+
return part;
|
|
602
|
+
}
|
|
603
|
+
if (part?.type === 'text') {
|
|
604
|
+
return part.text || '';
|
|
605
|
+
}
|
|
606
|
+
return '';
|
|
607
|
+
})
|
|
608
|
+
.join('\n');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return '';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function listClaudeModels(service: string, owner: string) {
|
|
615
|
+
return clientSecretRequest({
|
|
616
|
+
clientSecretName: 'claude',
|
|
617
|
+
service,
|
|
618
|
+
owner,
|
|
619
|
+
url: ANTHROPIC_MODELS_API_URL,
|
|
620
|
+
method: 'GET',
|
|
621
|
+
headers: {
|
|
622
|
+
'x-api-key': '$CLIENT_SECRET',
|
|
623
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export async function listOpenAIModels(service: string, owner: string) {
|
|
629
|
+
return clientSecretRequest({
|
|
630
|
+
clientSecretName: 'openai',
|
|
631
|
+
service,
|
|
632
|
+
owner,
|
|
633
|
+
url: OPENAI_MODELS_API_URL,
|
|
634
|
+
method: 'GET',
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: 'Bearer $CLIENT_SECRET',
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Suffix for the background-indexing queue. Must sort *before* ':' (ASCII 58)
|
|
642
|
+
// so the chat-history BETWEEN query never includes bg-queue items. '-' (45) works.
|
|
643
|
+
export const BG_INDEXING_QUEUE_SUFFIX = '-bg';
|
|
644
|
+
|
|
645
|
+
// Pending background-indexing task descriptor. NOTE: the live mutable queue
|
|
646
|
+
// (a Vue `reactive([])` in agent.vue, a plain array in bunnyquery) is app-level
|
|
647
|
+
// state owned by the consumer — only the TYPE lives in the engine.
|
|
648
|
+
export type BgTaskEntry = {
|
|
649
|
+
serviceId: string;
|
|
650
|
+
platform: 'claude' | 'openai';
|
|
651
|
+
id: string;
|
|
652
|
+
filename: string;
|
|
653
|
+
storagePath?: string;
|
|
654
|
+
isReindex?: boolean;
|
|
655
|
+
mime?: string;
|
|
656
|
+
size?: number;
|
|
657
|
+
status: 'running' | 'pending';
|
|
658
|
+
poll: ((opts: { latency: number }) => Promise<any>) | undefined;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
export async function getChatHistory(
|
|
662
|
+
params: { service?: string; owner?: string; platform: 'claude' | 'openai'; queue?: string },
|
|
663
|
+
fetchOptions: Record<string, any>,
|
|
664
|
+
) {
|
|
665
|
+
const url =
|
|
666
|
+
params.platform === 'claude'
|
|
667
|
+
? ANTHROPIC_MESSAGES_API_URL
|
|
668
|
+
: OPENAI_RESPONSES_API_URL;
|
|
669
|
+
const p = Object.assign(
|
|
670
|
+
{
|
|
671
|
+
url,
|
|
672
|
+
method: 'POST',
|
|
673
|
+
},
|
|
674
|
+
{ service: params.service, owner: params.owner },
|
|
675
|
+
params.queue ? { queue: params.queue } : {},
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
return chatEngineConfig().clientSecretRequestHistory(
|
|
679
|
+
p as { url: string; method: 'POST'; queue?: string },
|
|
680
|
+
Object.assign({ ascending: false }, fetchOptions),
|
|
681
|
+
);
|
|
682
|
+
}
|