@promptbook/remote-server 0.110.0-5 → 0.110.0-7

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/esm/index.es.js CHANGED
@@ -22,6 +22,7 @@ import moment from 'moment';
22
22
  import sha256 from 'crypto-js/sha256';
23
23
  import { lookup, extension } from 'mime-types';
24
24
  import { parse, unparse } from 'papaparse';
25
+ import { Agent as Agent$1, setDefaultOpenAIClient, setDefaultOpenAIKey, fileSearchTool, tool, run } from '@openai/agents';
25
26
  import Bottleneck from 'bottleneck';
26
27
  import OpenAI from 'openai';
27
28
 
@@ -39,7 +40,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
39
40
  * @generated
40
41
  * @see https://github.com/webgptorg/promptbook
41
42
  */
42
- const PROMPTBOOK_ENGINE_VERSION = '0.110.0-5';
43
+ const PROMPTBOOK_ENGINE_VERSION = '0.110.0-7';
43
44
  /**
44
45
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
45
46
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -19462,16 +19463,11 @@ class OpenAiCompatibleExecutionTools {
19462
19463
  const openAiOptions = { ...this.options };
19463
19464
  delete openAiOptions.isVerbose;
19464
19465
  delete openAiOptions.userId;
19465
- // Enhanced configuration for better ECONNRESET handling
19466
+ // Enhanced configuration with retries and timeouts.
19466
19467
  const enhancedOptions = {
19467
19468
  ...openAiOptions,
19468
19469
  timeout: API_REQUEST_TIMEOUT,
19469
19470
  maxRetries: CONNECTION_RETRIES_LIMIT,
19470
- defaultHeaders: {
19471
- Connection: 'keep-alive',
19472
- 'Keep-Alive': 'timeout=30, max=100',
19473
- ...openAiOptions.defaultHeaders,
19474
- },
19475
19471
  };
19476
19472
  this.client = new OpenAI(enhancedOptions);
19477
19473
  }
@@ -20394,644 +20390,197 @@ class OpenAiExecutionTools extends OpenAiCompatibleExecutionTools {
20394
20390
  }
20395
20391
  }
20396
20392
 
20397
- /**
20398
- * Uploads files to OpenAI and returns their IDs
20399
- *
20400
- * @private utility for `OpenAiAssistantExecutionTools` and `OpenAiCompatibleExecutionTools`
20401
- */
20402
- async function uploadFilesToOpenAi(client, files) {
20403
- const fileIds = [];
20404
- for (const file of files) {
20405
- // Note: OpenAI API expects a File object or a ReadStream
20406
- // In browser environment, we can pass the File object directly
20407
- // In Node.js environment, we might need to convert it or use a different approach
20408
- // But since `Prompt.files` already contains `File` objects, we try to pass them directly
20409
- const uploadedFile = await client.files.create({
20410
- file: file,
20411
- purpose: 'assistants',
20412
- });
20413
- fileIds.push(uploadedFile.id);
20414
- }
20415
- return fileIds;
20416
- }
20417
-
20418
20393
  const DEFAULT_KNOWLEDGE_SOURCE_DOWNLOAD_TIMEOUT_MS = 30000;
20419
20394
  const DEFAULT_KNOWLEDGE_SOURCE_UPLOAD_TIMEOUT_MS = 900000;
20420
20395
  const VECTOR_STORE_PROGRESS_LOG_INTERVAL_MIN_MS = 15000;
20421
20396
  const VECTOR_STORE_STALL_LOG_THRESHOLD_MS = 30000;
20422
20397
  /**
20423
- * Execution Tools for calling OpenAI API Assistants
20424
- *
20425
- * This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
20426
- *
20427
- * Note: [🦖] There are several different things in Promptbook:
20428
- * - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
20429
- * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
20430
- * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
20431
- * - `OpenAiAssistantExecutionTools` - which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities, recommended for usage in `Agent` or `AgentLlmExecutionTools`
20432
- * - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
20398
+ * Base class for OpenAI execution tools that need hosted vector stores.
20433
20399
  *
20434
20400
  * @public exported from `@promptbook/openai`
20435
20401
  */
20436
- class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
20402
+ class OpenAiVectorStoreHandler extends OpenAiExecutionTools {
20437
20403
  /**
20438
- * Creates OpenAI Execution Tools.
20439
- *
20440
- * @param options which are relevant are directly passed to the OpenAI client
20404
+ * Returns the per-knowledge-source download timeout in milliseconds.
20441
20405
  */
20442
- constructor(options) {
20406
+ getKnowledgeSourceDownloadTimeoutMs() {
20443
20407
  var _a;
20444
- if (options.isProxied) {
20445
- throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI assistants`);
20446
- }
20447
- super(options);
20448
- this.isCreatingNewAssistantsAllowed = false;
20449
- this.assistantId = options.assistantId;
20450
- this.isCreatingNewAssistantsAllowed = (_a = options.isCreatingNewAssistantsAllowed) !== null && _a !== void 0 ? _a : false;
20451
- if (this.assistantId === null && !this.isCreatingNewAssistantsAllowed) {
20452
- throw new NotAllowed(`Assistant ID is null and creating new assistants is not allowed - this configuration does not make sense`);
20453
- }
20454
- // <- TODO: !!! `OpenAiAssistantExecutionToolsOptions` - Allow `assistantId: null` together with `isCreatingNewAssistantsAllowed: true`
20455
- // TODO: [👱] Make limiter same as in `OpenAiExecutionTools`
20408
+ return (_a = this.vectorStoreOptions.knowledgeSourceDownloadTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_KNOWLEDGE_SOURCE_DOWNLOAD_TIMEOUT_MS;
20456
20409
  }
20457
- get title() {
20458
- return 'OpenAI Assistant';
20410
+ /**
20411
+ * Returns the max concurrency for knowledge source uploads.
20412
+ */
20413
+ getKnowledgeSourceUploadMaxConcurrency() {
20414
+ var _a;
20415
+ return (_a = this.vectorStoreOptions.knowledgeSourceUploadMaxConcurrency) !== null && _a !== void 0 ? _a : 5;
20459
20416
  }
20460
- get description() {
20461
- return 'Use single assistant provided by OpenAI';
20417
+ /**
20418
+ * Returns the polling interval in milliseconds for vector store uploads.
20419
+ */
20420
+ getKnowledgeSourceUploadPollIntervalMs() {
20421
+ var _a;
20422
+ return (_a = this.vectorStoreOptions.knowledgeSourceUploadPollIntervalMs) !== null && _a !== void 0 ? _a : 5000;
20462
20423
  }
20463
20424
  /**
20464
- * Calls OpenAI API to use a chat model.
20425
+ * Returns the overall upload timeout in milliseconds for vector store uploads.
20465
20426
  */
20466
- async callChatModel(prompt) {
20467
- return this.callChatModelStream(prompt, () => { });
20427
+ getKnowledgeSourceUploadTimeoutMs() {
20428
+ var _a;
20429
+ return (_a = this.vectorStoreOptions.knowledgeSourceUploadTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_KNOWLEDGE_SOURCE_UPLOAD_TIMEOUT_MS;
20468
20430
  }
20469
20431
  /**
20470
- * Calls OpenAI API to use a chat model with streaming.
20432
+ * Returns true if we should continue even if vector store ingestion stalls.
20471
20433
  */
20472
- async callChatModelStream(prompt, onProgress) {
20473
- var _a, _b, _c, _d, _e, _f;
20474
- if (this.options.isVerbose) {
20475
- console.info('💬 OpenAI callChatModel call', { prompt });
20434
+ shouldContinueOnVectorStoreStall() {
20435
+ var _a;
20436
+ return (_a = this.vectorStoreOptions.shouldContinueOnVectorStoreStall) !== null && _a !== void 0 ? _a : true;
20437
+ }
20438
+ /**
20439
+ * Returns vector-store-specific options with extended settings.
20440
+ */
20441
+ get vectorStoreOptions() {
20442
+ return this.options;
20443
+ }
20444
+ /**
20445
+ * Returns the OpenAI vector stores API surface, supporting stable and beta SDKs.
20446
+ */
20447
+ getVectorStoresApi(client) {
20448
+ var _a, _b;
20449
+ const vectorStores = (_a = client.vectorStores) !== null && _a !== void 0 ? _a : (_b = client.beta) === null || _b === void 0 ? void 0 : _b.vectorStores;
20450
+ if (!vectorStores) {
20451
+ throw new Error('OpenAI client does not support vector stores. Please ensure you are using a compatible version of the OpenAI SDK with vector store support.');
20476
20452
  }
20477
- const { content, parameters, modelRequirements /*, format*/ } = prompt;
20478
- const client = await this.getClient();
20479
- // TODO: [☂] Use here more modelRequirements
20480
- if (modelRequirements.modelVariant !== 'CHAT') {
20481
- throw new PipelineExecutionError('Use callChatModel only for CHAT variant');
20453
+ return vectorStores;
20454
+ }
20455
+ /**
20456
+ * Downloads a knowledge source URL into a File for vector store upload.
20457
+ */
20458
+ async downloadKnowledgeSourceFile(options) {
20459
+ var _a;
20460
+ const { source, timeoutMs, logLabel } = options;
20461
+ const startedAtMs = Date.now();
20462
+ const controller = new AbortController();
20463
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
20464
+ if (this.options.isVerbose) {
20465
+ console.info('[🤰]', 'Downloading knowledge source', {
20466
+ source,
20467
+ timeoutMs,
20468
+ logLabel,
20469
+ });
20482
20470
  }
20483
- // TODO: [👨‍👨‍👧‍👧] Remove:
20484
- for (const key of ['maxTokens', 'modelName', 'seed', 'temperature']) {
20485
- if (modelRequirements[key] !== undefined) {
20486
- throw new NotYetImplementedError(`In \`OpenAiAssistantExecutionTools\` you cannot specify \`${key}\``);
20471
+ try {
20472
+ const response = await fetch(source, { signal: controller.signal });
20473
+ const contentType = (_a = response.headers.get('content-type')) !== null && _a !== void 0 ? _a : undefined;
20474
+ if (!response.ok) {
20475
+ console.error('[🤰]', 'Failed to download knowledge source', {
20476
+ source,
20477
+ status: response.status,
20478
+ statusText: response.statusText,
20479
+ contentType,
20480
+ elapsedMs: Date.now() - startedAtMs,
20481
+ logLabel,
20482
+ });
20483
+ return null;
20484
+ }
20485
+ const buffer = await response.arrayBuffer();
20486
+ let filename = source.split('/').pop() || 'downloaded-file';
20487
+ try {
20488
+ const url = new URL(source);
20489
+ filename = url.pathname.split('/').pop() || filename;
20490
+ }
20491
+ catch (error) {
20492
+ // Keep default filename
20493
+ }
20494
+ const file = new File([buffer], filename, contentType ? { type: contentType } : undefined);
20495
+ const elapsedMs = Date.now() - startedAtMs;
20496
+ const sizeBytes = buffer.byteLength;
20497
+ if (this.options.isVerbose) {
20498
+ console.info('[🤰]', 'Downloaded knowledge source', {
20499
+ source,
20500
+ filename,
20501
+ sizeBytes,
20502
+ contentType,
20503
+ elapsedMs,
20504
+ logLabel,
20505
+ });
20487
20506
  }
20507
+ return { file, sizeBytes, filename, elapsedMs };
20488
20508
  }
20489
- /*
20490
- TODO: [👨‍👨‍👧‍👧] Implement all of this for Assistants
20491
- const modelName = modelRequirements.modelName || this.getDefaultChatModel().modelName;
20492
- const modelSettings = {
20493
- model: modelName,
20494
-
20495
- temperature: modelRequirements.temperature,
20496
-
20497
- // <- TODO: [🈁] Use `seed` here AND/OR use is `isDeterministic` for entire execution tools
20498
- // <- Note: [🧆]
20499
- } as OpenAI.Chat.Completions.CompletionCreateParamsNonStreaming; // <- TODO: Guard here types better
20500
-
20501
- if (format === 'JSON') {
20502
- modelSettings.response_format = {
20503
- type: 'json_object',
20504
- };
20509
+ catch (error) {
20510
+ assertsError(error);
20511
+ console.error('[🤰]', 'Error downloading knowledge source', {
20512
+ source,
20513
+ elapsedMs: Date.now() - startedAtMs,
20514
+ logLabel,
20515
+ error: serializeError(error),
20516
+ });
20517
+ return null;
20505
20518
  }
20506
- */
20507
- // <- TODO: [🚸] Not all models are compatible with JSON mode
20508
- // > 'response_format' of type 'json_object' is not supported with this model.
20509
- const rawPromptContent = templateParameters(content, {
20510
- ...parameters,
20511
- modelName: 'assistant',
20512
- // <- [🧠] What is the best value here
20513
- });
20514
- // Build thread messages: include previous thread messages + current user message
20515
- const threadMessages = [];
20516
- // TODO: [🈹] Maybe this should not be here but in other place, look at commit 39d705e75e5bcf7a818c3af36bc13e1c8475c30c
20517
- // Add previous messages from thread (if any)
20518
- if ('thread' in prompt && Array.isArray(prompt.thread)) {
20519
- const previousMessages = prompt.thread.map((msg) => ({
20520
- role: (msg.sender === 'assistant' ? 'assistant' : 'user'),
20521
- content: msg.content,
20522
- }));
20523
- threadMessages.push(...previousMessages);
20519
+ finally {
20520
+ clearTimeout(timeoutId);
20524
20521
  }
20525
- // Always add the current user message
20526
- const currentUserMessage = {
20527
- role: 'user',
20528
- content: rawPromptContent,
20529
- };
20530
- if ('files' in prompt && Array.isArray(prompt.files) && prompt.files.length > 0) {
20531
- const fileIds = await uploadFilesToOpenAi(client, prompt.files);
20532
- currentUserMessage.attachments = fileIds.map((fileId) => ({
20533
- file_id: fileId,
20534
- tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
20535
- }));
20522
+ }
20523
+ /**
20524
+ * Logs vector store file batch diagnostics to help trace ingestion stalls or failures.
20525
+ */
20526
+ async logVectorStoreFileBatchDiagnostics(options) {
20527
+ var _a, _b, _c, _d, _e;
20528
+ const { client, vectorStoreId, batchId, uploadedFiles, logLabel, reason } = options;
20529
+ if (reason === 'stalled' && !this.options.isVerbose) {
20530
+ return;
20536
20531
  }
20537
- threadMessages.push(currentUserMessage);
20538
- // Check if tools are being used - if so, use non-streaming mode
20539
- const hasTools = modelRequirements.tools !== undefined && modelRequirements.tools.length > 0;
20540
- const start = $getCurrentDate();
20541
- let complete;
20542
- // [🐱‍🚀] When tools are present, we need to use the non-streaming Runs API
20543
- // because streaming doesn't support tool execution flow properly
20544
- if (hasTools) {
20545
- onProgress({
20546
- content: '',
20547
- modelName: 'assistant',
20548
- timing: { start, complete: $getCurrentDate() },
20549
- usage: UNCERTAIN_USAGE,
20550
- rawPromptContent,
20551
- rawRequest: null,
20552
- rawResponse: null,
20532
+ if (!batchId.startsWith('vsfb_')) {
20533
+ console.error('[🤰]', 'Vector store file batch diagnostics skipped (invalid batch id)', {
20534
+ vectorStoreId,
20535
+ batchId,
20536
+ reason,
20537
+ logLabel,
20553
20538
  });
20554
- const rawRequest = {
20555
- assistant_id: this.assistantId,
20556
- thread: {
20557
- messages: threadMessages,
20558
- },
20559
- tools: mapToolsToOpenAi(modelRequirements.tools),
20560
- };
20561
- if (this.options.isVerbose) {
20562
- console.info(colors.bgWhite('rawRequest (non-streaming with tools)'), JSON.stringify(rawRequest, null, 4));
20563
- }
20564
- // Create thread and run
20565
- const threadAndRun = await client.beta.threads.createAndRun(rawRequest);
20566
- let run = threadAndRun;
20567
- const completedToolCalls = [];
20568
- const toolCallStartedAt = new Map();
20569
- // Poll until run completes or requires action
20570
- while (run.status === 'queued' || run.status === 'in_progress' || run.status === 'requires_action') {
20571
- if (run.status === 'requires_action' && ((_a = run.required_action) === null || _a === void 0 ? void 0 : _a.type) === 'submit_tool_outputs') {
20572
- // Execute tools
20573
- const toolCalls = run.required_action.submit_tool_outputs.tool_calls;
20574
- const toolOutputs = [];
20575
- for (const toolCall of toolCalls) {
20576
- if (toolCall.type === 'function') {
20577
- const functionName = toolCall.function.name;
20578
- const functionArgs = JSON.parse(toolCall.function.arguments);
20579
- const calledAt = $getCurrentDate();
20580
- if (toolCall.id) {
20581
- toolCallStartedAt.set(toolCall.id, calledAt);
20582
- }
20583
- onProgress({
20584
- content: '',
20585
- modelName: 'assistant',
20586
- timing: { start, complete: $getCurrentDate() },
20587
- usage: UNCERTAIN_USAGE,
20588
- rawPromptContent,
20589
- rawRequest: null,
20590
- rawResponse: null,
20591
- toolCalls: [
20592
- {
20593
- name: functionName,
20594
- arguments: toolCall.function.arguments,
20595
- result: '',
20596
- rawToolCall: toolCall,
20597
- createdAt: calledAt,
20598
- },
20599
- ],
20600
- });
20601
- if (this.options.isVerbose) {
20602
- console.info(`🔧 Executing tool: ${functionName}`, functionArgs);
20603
- }
20604
- // Get execution tools for script execution
20605
- const executionTools = this.options
20606
- .executionTools;
20607
- if (!executionTools || !executionTools.script) {
20608
- throw new PipelineExecutionError(`Model requested tool '${functionName}' but no executionTools.script were provided in OpenAiAssistantExecutionTools options`);
20609
- }
20610
- // TODO: [DRY] Use some common tool caller (similar to OpenAiCompatibleExecutionTools)
20611
- const scriptTools = Array.isArray(executionTools.script)
20612
- ? executionTools.script
20613
- : [executionTools.script];
20614
- let functionResponse;
20615
- let errors;
20616
- try {
20617
- const scriptTool = scriptTools[0]; // <- TODO: [🧠] Which script tool to use?
20618
- functionResponse = await scriptTool.execute({
20619
- scriptLanguage: 'javascript',
20620
- script: `
20621
- const args = ${JSON.stringify(functionArgs)};
20622
- return await ${functionName}(args);
20623
- `,
20624
- parameters: prompt.parameters,
20625
- });
20626
- if (this.options.isVerbose) {
20627
- console.info(`✅ Tool ${functionName} executed:`, functionResponse);
20628
- }
20629
- }
20630
- catch (error) {
20631
- assertsError(error);
20632
- const serializedError = serializeError(error);
20633
- errors = [serializedError];
20634
- functionResponse = spaceTrim$2((block) => `
20635
-
20636
- The invoked tool \`${functionName}\` failed with error:
20637
-
20638
- \`\`\`json
20639
- ${block(JSON.stringify(serializedError, null, 4))}
20640
- \`\`\`
20641
-
20642
- `);
20643
- console.error(colors.bgRed(`❌ Error executing tool ${functionName}:`));
20644
- console.error(error);
20645
- }
20646
- toolOutputs.push({
20647
- tool_call_id: toolCall.id,
20648
- output: functionResponse,
20649
- });
20650
- completedToolCalls.push({
20651
- name: functionName,
20652
- arguments: toolCall.function.arguments,
20653
- result: functionResponse,
20654
- rawToolCall: toolCall,
20655
- createdAt: toolCall.id ? toolCallStartedAt.get(toolCall.id) || calledAt : calledAt,
20656
- errors,
20657
- });
20658
- }
20659
- }
20660
- // Submit tool outputs
20661
- run = await client.beta.threads.runs.submitToolOutputs(run.thread_id, run.id, {
20662
- tool_outputs: toolOutputs,
20663
- });
20664
- }
20665
- else {
20666
- // Wait a bit before polling again
20667
- await new Promise((resolve) => setTimeout(resolve, 500));
20668
- run = await client.beta.threads.runs.retrieve(run.thread_id, run.id);
20669
- }
20670
- }
20671
- if (run.status !== 'completed') {
20672
- throw new PipelineExecutionError(`Assistant run failed with status: ${run.status}`);
20673
- }
20674
- // Get messages from the thread
20675
- const messages = await client.beta.threads.messages.list(run.thread_id);
20676
- const assistantMessages = messages.data.filter((msg) => msg.role === 'assistant');
20677
- if (assistantMessages.length === 0) {
20678
- throw new PipelineExecutionError('No assistant messages found after run completion');
20679
- }
20680
- const lastMessage = assistantMessages[0];
20681
- const textContent = lastMessage.content.find((c) => c.type === 'text');
20682
- if (!textContent || textContent.type !== 'text') {
20683
- throw new PipelineExecutionError('No text content in assistant response');
20684
- }
20685
- complete = $getCurrentDate();
20686
- const resultContent = textContent.text.value;
20687
- const usage = UNCERTAIN_USAGE;
20688
- // Progress callback with final result
20689
- const finalChunk = {
20690
- content: resultContent,
20691
- modelName: 'assistant',
20692
- timing: { start, complete },
20693
- usage,
20694
- rawPromptContent,
20695
- rawRequest,
20696
- rawResponse: { run, messages: messages.data },
20697
- toolCalls: completedToolCalls.length > 0 ? completedToolCalls : undefined,
20698
- };
20699
- onProgress(finalChunk);
20700
- return exportJson({
20701
- name: 'promptResult',
20702
- message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\` (with tools)`,
20703
- order: [],
20704
- value: finalChunk,
20705
- });
20706
- }
20707
- // Streaming mode (without tools)
20708
- const rawRequest = {
20709
- // TODO: [👨‍👨‍👧‍👧] ...modelSettings,
20710
- // TODO: [👨‍👨‍👧‍👧][🧠] What about system message for assistants, does it make sense - combination of OpenAI assistants with Promptbook Personas
20711
- assistant_id: this.assistantId,
20712
- thread: {
20713
- messages: threadMessages,
20714
- },
20715
- tools: modelRequirements.tools === undefined ? undefined : mapToolsToOpenAi(modelRequirements.tools),
20716
- // <- TODO: Add user identification here> user: this.options.user,
20717
- };
20718
- if (this.options.isVerbose) {
20719
- console.info(colors.bgWhite('rawRequest (streaming)'), JSON.stringify(rawRequest, null, 4));
20720
- }
20721
- const stream = await client.beta.threads.createAndRunStream(rawRequest);
20722
- stream.on('connect', () => {
20723
- if (this.options.isVerbose) {
20724
- console.info('connect', stream.currentEvent);
20725
- }
20726
- });
20727
- stream.on('textDelta', (textDelta, snapshot) => {
20728
- if (this.options.isVerbose && textDelta.value) {
20729
- console.info('textDelta', textDelta.value);
20730
- }
20731
- const chunk = {
20732
- content: snapshot.value,
20733
- modelName: 'assistant',
20734
- timing: {
20735
- start,
20736
- complete: $getCurrentDate(),
20737
- },
20738
- usage: UNCERTAIN_USAGE,
20739
- rawPromptContent,
20740
- rawRequest,
20741
- rawResponse: snapshot,
20742
- };
20743
- onProgress(chunk);
20744
- });
20745
- stream.on('messageCreated', (message) => {
20746
- if (this.options.isVerbose) {
20747
- console.info('messageCreated', message);
20748
- }
20749
- });
20750
- stream.on('messageDone', (message) => {
20751
- if (this.options.isVerbose) {
20752
- console.info('messageDone', message);
20753
- }
20754
- });
20755
- // TODO: [🐱‍🚀] Handle tool calls in assistants
20756
- // Note: OpenAI Assistant streaming with tool calls requires special handling.
20757
- // The stream will pause when a tool call is needed, and we need to:
20758
- // 1. Wait for the run to reach 'requires_action' status
20759
- // 2. Execute the tool calls
20760
- // 3. Submit tool outputs via a separate API call (not on the stream)
20761
- // 4. Continue the run
20762
- // This requires switching to non-streaming mode or using the Runs API directly.
20763
- // For now, tools with assistants should use the non-streaming chat completions API instead.
20764
- const rawResponse = await stream.finalMessages();
20765
- if (this.options.isVerbose) {
20766
- console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
20767
- }
20768
- if (rawResponse.length !== 1) {
20769
- throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse.length} finalMessages from OpenAI`);
20770
- }
20771
- if (rawResponse[0].content.length !== 1) {
20772
- throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse[0].content.length} finalMessages content from OpenAI`);
20773
- }
20774
- if (((_b = rawResponse[0].content[0]) === null || _b === void 0 ? void 0 : _b.type) !== 'text') {
20775
- throw new PipelineExecutionError(`There is NOT 'text' BUT ${(_c = rawResponse[0].content[0]) === null || _c === void 0 ? void 0 : _c.type} finalMessages content type from OpenAI`);
20776
- }
20777
- let resultContent = (_d = rawResponse[0].content[0]) === null || _d === void 0 ? void 0 : _d.text.value;
20778
- // Process annotations to replace file IDs with filenames
20779
- if ((_e = rawResponse[0].content[0]) === null || _e === void 0 ? void 0 : _e.text.annotations) {
20780
- const annotations = (_f = rawResponse[0].content[0]) === null || _f === void 0 ? void 0 : _f.text.annotations;
20781
- // Map to store file ID -> filename to avoid duplicate requests
20782
- const fileIdToName = new Map();
20783
- for (const annotation of annotations) {
20784
- if (annotation.type === 'file_citation') {
20785
- const fileId = annotation.file_citation.file_id;
20786
- let filename = fileIdToName.get(fileId);
20787
- if (!filename) {
20788
- try {
20789
- const file = await client.files.retrieve(fileId);
20790
- filename = file.filename;
20791
- fileIdToName.set(fileId, filename);
20792
- }
20793
- catch (error) {
20794
- console.error(`Failed to retrieve file info for ${fileId}`, error);
20795
- // Fallback to "Source" or keep original if fetch fails
20796
- filename = 'Source';
20797
- }
20798
- }
20799
- if (filename && resultContent) {
20800
- // Replace the citation marker with filename
20801
- // Regex to match the second part of the citation: 【id†source】 -> 【id†filename】
20802
- // Note: annotation.text contains the exact marker like 【4:0†source】
20803
- const newText = annotation.text.replace(/†.*?】/, `†${filename}】`);
20804
- resultContent = resultContent.replace(annotation.text, newText);
20805
- }
20806
- }
20807
- }
20808
- }
20809
- // eslint-disable-next-line prefer-const
20810
- complete = $getCurrentDate();
20811
- const usage = UNCERTAIN_USAGE;
20812
- // <- TODO: [🥘] Compute real usage for assistant
20813
- // ?> const usage = computeOpenAiUsage(content, resultContent || '', rawResponse);
20814
- if (resultContent === null) {
20815
- throw new PipelineExecutionError('No response message from OpenAI');
20816
- }
20817
- return exportJson({
20818
- name: 'promptResult',
20819
- message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\``,
20820
- order: [],
20821
- value: {
20822
- content: resultContent,
20823
- modelName: 'assistant',
20824
- // <- TODO: [🥘] Detect used model in assistant
20825
- // ?> model: rawResponse.model || modelName,
20826
- timing: {
20827
- start,
20828
- complete,
20829
- },
20830
- usage,
20831
- rawPromptContent,
20832
- rawRequest,
20833
- rawResponse,
20834
- // <- [🗯]
20835
- },
20836
- });
20837
- }
20838
- /*
20839
- public async playground() {
20840
- const client = await this.getClient();
20841
-
20842
- // List all assistants
20843
- const assistants = await client.beta.assistants.list();
20844
-
20845
- // Get details of a specific assistant
20846
- const assistantId = 'asst_MO8fhZf4dGloCfXSHeLcIik0';
20847
- const assistant = await client.beta.assistants.retrieve(assistantId);
20848
-
20849
- // Update an assistant
20850
- const updatedAssistant = await client.beta.assistants.update(assistantId, {
20851
- name: assistant.name + '(M)',
20852
- description: 'Updated description via Promptbook',
20853
- metadata: {
20854
- [Math.random().toString(36).substring(2, 15)]: new Date().toISOString(),
20855
- },
20856
- });
20857
-
20858
- await forEver();
20859
- }
20860
- */
20861
- /**
20862
- * Get an existing assistant tool wrapper
20863
- */
20864
- getAssistant(assistantId) {
20865
- return new OpenAiAssistantExecutionTools({
20866
- ...this.options,
20867
- isCreatingNewAssistantsAllowed: this.isCreatingNewAssistantsAllowed,
20868
- assistantId,
20869
- });
20870
- }
20871
- /**
20872
- * Returns the per-knowledge-source download timeout in milliseconds.
20873
- */
20874
- getKnowledgeSourceDownloadTimeoutMs() {
20875
- var _a;
20876
- return (_a = this.assistantOptions.knowledgeSourceDownloadTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_KNOWLEDGE_SOURCE_DOWNLOAD_TIMEOUT_MS;
20877
- }
20878
- /**
20879
- * Returns the max concurrency for knowledge source uploads.
20880
- */
20881
- getKnowledgeSourceUploadMaxConcurrency() {
20882
- var _a;
20883
- return (_a = this.assistantOptions.knowledgeSourceUploadMaxConcurrency) !== null && _a !== void 0 ? _a : 5;
20884
- }
20885
- /**
20886
- * Returns the polling interval in milliseconds for vector store uploads.
20887
- */
20888
- getKnowledgeSourceUploadPollIntervalMs() {
20889
- var _a;
20890
- return (_a = this.assistantOptions.knowledgeSourceUploadPollIntervalMs) !== null && _a !== void 0 ? _a : 5000;
20891
- }
20892
- /**
20893
- * Returns the overall upload timeout in milliseconds for vector store uploads.
20894
- */
20895
- getKnowledgeSourceUploadTimeoutMs() {
20896
- var _a;
20897
- return (_a = this.assistantOptions.knowledgeSourceUploadTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_KNOWLEDGE_SOURCE_UPLOAD_TIMEOUT_MS;
20898
- }
20899
- /**
20900
- * Returns true if we should continue even if vector store ingestion stalls.
20901
- */
20902
- shouldContinueOnVectorStoreStall() {
20903
- var _a;
20904
- return (_a = this.assistantOptions.shouldContinueOnVectorStoreStall) !== null && _a !== void 0 ? _a : true;
20905
- }
20906
- /**
20907
- * Returns assistant-specific options with extended settings.
20908
- */
20909
- get assistantOptions() {
20910
- return this.options;
20911
- }
20912
- /**
20913
- * Downloads a knowledge source URL into a File for vector store upload.
20914
- */
20915
- async downloadKnowledgeSourceFile(options) {
20916
- var _a;
20917
- const { source, timeoutMs, logLabel } = options;
20918
- const startedAtMs = Date.now();
20919
- const controller = new AbortController();
20920
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
20921
- if (this.options.isVerbose) {
20922
- console.info('[🤰]', 'Downloading knowledge source', {
20923
- source,
20924
- timeoutMs,
20925
- logLabel,
20926
- });
20927
- }
20928
- try {
20929
- const response = await fetch(source, { signal: controller.signal });
20930
- const contentType = (_a = response.headers.get('content-type')) !== null && _a !== void 0 ? _a : undefined;
20931
- if (!response.ok) {
20932
- console.error('[🤰]', 'Failed to download knowledge source', {
20933
- source,
20934
- status: response.status,
20935
- statusText: response.statusText,
20936
- contentType,
20937
- elapsedMs: Date.now() - startedAtMs,
20938
- logLabel,
20939
- });
20940
- return null;
20941
- }
20942
- const buffer = await response.arrayBuffer();
20943
- let filename = source.split('/').pop() || 'downloaded-file';
20944
- try {
20945
- const url = new URL(source);
20946
- filename = url.pathname.split('/').pop() || filename;
20947
- }
20948
- catch (error) {
20949
- // Keep default filename
20950
- }
20951
- const file = new File([buffer], filename, contentType ? { type: contentType } : undefined);
20952
- const elapsedMs = Date.now() - startedAtMs;
20953
- const sizeBytes = buffer.byteLength;
20954
- if (this.options.isVerbose) {
20955
- console.info('[🤰]', 'Downloaded knowledge source', {
20956
- source,
20957
- filename,
20958
- sizeBytes,
20959
- contentType,
20960
- elapsedMs,
20961
- logLabel,
20962
- });
20963
- }
20964
- return { file, sizeBytes, filename, elapsedMs };
20965
- }
20966
- catch (error) {
20967
- assertsError(error);
20968
- console.error('[🤰]', 'Error downloading knowledge source', {
20969
- source,
20970
- elapsedMs: Date.now() - startedAtMs,
20971
- logLabel,
20972
- error: serializeError(error),
20973
- });
20974
- return null;
20975
- }
20976
- finally {
20977
- clearTimeout(timeoutId);
20978
- }
20979
- }
20980
- /**
20981
- * Logs vector store file batch diagnostics to help trace ingestion stalls or failures.
20982
- */
20983
- async logVectorStoreFileBatchDiagnostics(options) {
20984
- var _a, _b;
20985
- const { client, vectorStoreId, batchId, uploadedFiles, logLabel, reason } = options;
20986
- if (reason === 'stalled' && !this.options.isVerbose) {
20987
- return;
20988
- }
20989
- if (!batchId.startsWith('vsfb_')) {
20990
- console.error('[🤰]', 'Vector store file batch diagnostics skipped (invalid batch id)', {
20991
- vectorStoreId,
20992
- batchId,
20993
- reason,
20994
- logLabel,
20995
- });
20996
- return;
20997
- }
20998
- const fileIdToMetadata = new Map();
20999
- for (const file of uploadedFiles) {
21000
- fileIdToMetadata.set(file.fileId, file);
21001
- }
21002
- try {
21003
- const limit = Math.min(100, Math.max(10, uploadedFiles.length));
21004
- const batchFilesPage = await client.beta.vectorStores.fileBatches.listFiles(vectorStoreId, batchId, {
21005
- limit,
21006
- });
21007
- const batchFiles = (_a = batchFilesPage.data) !== null && _a !== void 0 ? _a : [];
21008
- const statusCounts = {
21009
- in_progress: 0,
21010
- completed: 0,
21011
- failed: 0,
21012
- cancelled: 0,
20539
+ return;
20540
+ }
20541
+ const fileIdToMetadata = new Map();
20542
+ for (const file of uploadedFiles) {
20543
+ fileIdToMetadata.set(file.fileId, file);
20544
+ }
20545
+ try {
20546
+ const vectorStores = this.getVectorStoresApi(client);
20547
+ const limit = Math.min(100, Math.max(10, uploadedFiles.length));
20548
+ const batchFilesPage = await vectorStores.fileBatches.listFiles(batchId, {
20549
+ vector_store_id: vectorStoreId,
20550
+ limit,
20551
+ });
20552
+ const batchFiles = (_a = batchFilesPage.data) !== null && _a !== void 0 ? _a : [];
20553
+ const statusCounts = {
20554
+ in_progress: 0,
20555
+ completed: 0,
20556
+ failed: 0,
20557
+ cancelled: 0,
21013
20558
  };
21014
20559
  const errorSamples = [];
21015
20560
  const inProgressSamples = [];
21016
20561
  const batchFileIds = new Set();
21017
20562
  for (const file of batchFiles) {
21018
- batchFileIds.add(file.id);
21019
- statusCounts[file.status] = ((_b = statusCounts[file.status]) !== null && _b !== void 0 ? _b : 0) + 1;
21020
- const metadata = fileIdToMetadata.get(file.id);
21021
- if (file.last_error) {
20563
+ const status = (_b = file.status) !== null && _b !== void 0 ? _b : 'unknown';
20564
+ statusCounts[status] = ((_c = statusCounts[status]) !== null && _c !== void 0 ? _c : 0) + 1;
20565
+ const vectorStoreFileId = file.id;
20566
+ const uploadedFileId = (_d = file.file_id) !== null && _d !== void 0 ? _d : file.fileId;
20567
+ const fileId = uploadedFileId !== null && uploadedFileId !== void 0 ? uploadedFileId : vectorStoreFileId;
20568
+ batchFileIds.add(fileId);
20569
+ const metadata = fileIdToMetadata.get(fileId);
20570
+ if (status === 'failed') {
21022
20571
  errorSamples.push({
21023
- fileId: file.id,
20572
+ fileId,
20573
+ status,
20574
+ error: (_e = file.last_error) === null || _e === void 0 ? void 0 : _e.message,
21024
20575
  filename: metadata === null || metadata === void 0 ? void 0 : metadata.filename,
21025
- sizeBytes: metadata === null || metadata === void 0 ? void 0 : metadata.sizeBytes,
21026
- status: file.status,
21027
- lastError: file.last_error,
20576
+ vectorStoreFileId: uploadedFileId ? vectorStoreFileId : undefined,
21028
20577
  });
21029
20578
  }
21030
- else if (file.status === 'in_progress' && inProgressSamples.length < 5) {
20579
+ if (status === 'in_progress') {
21031
20580
  inProgressSamples.push({
21032
- fileId: file.id,
20581
+ fileId,
21033
20582
  filename: metadata === null || metadata === void 0 ? void 0 : metadata.filename,
21034
- sizeBytes: metadata === null || metadata === void 0 ? void 0 : metadata.sizeBytes,
20583
+ vectorStoreFileId: uploadedFileId ? vectorStoreFileId : undefined,
21035
20584
  });
21036
20585
  }
21037
20586
  }
@@ -21043,7 +20592,7 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
21043
20592
  filename: file.filename,
21044
20593
  sizeBytes: file.sizeBytes,
21045
20594
  }));
21046
- const vectorStore = await client.beta.vectorStores.retrieve(vectorStoreId);
20595
+ const vectorStore = await vectorStores.retrieve(vectorStoreId);
21047
20596
  const logPayload = {
21048
20597
  vectorStoreId,
21049
20598
  batchId,
@@ -21077,8 +20626,9 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
21077
20626
  * Uploads knowledge source files to the vector store and polls until processing completes.
21078
20627
  */
21079
20628
  async uploadKnowledgeSourceFilesToVectorStore(options) {
21080
- var _a, _b, _c, _d;
20629
+ var _a, _b, _c, _d, _e, _f;
21081
20630
  const { client, vectorStoreId, files, totalBytes, logLabel } = options;
20631
+ const vectorStores = this.getVectorStoresApi(client);
21082
20632
  const uploadStartedAtMs = Date.now();
21083
20633
  const maxConcurrency = Math.max(1, this.getKnowledgeSourceUploadMaxConcurrency());
21084
20634
  const pollIntervalMs = Math.max(1000, this.getKnowledgeSourceUploadPollIntervalMs());
@@ -21195,373 +20745,1288 @@ class OpenAiAssistantExecutionTools extends OpenAiExecutionTools {
21195
20745
  failedCount: failedUploads.length,
21196
20746
  logLabel,
21197
20747
  });
21198
- return null;
20748
+ return null;
20749
+ }
20750
+ const batch = await vectorStores.fileBatches.create(vectorStoreId, {
20751
+ file_ids: fileIds,
20752
+ });
20753
+ const expectedBatchId = batch.id;
20754
+ const expectedBatchIdValid = expectedBatchId.startsWith('vsfb_');
20755
+ if (!expectedBatchIdValid) {
20756
+ console.error('[🤰]', 'Vector store file batch id looks invalid', {
20757
+ vectorStoreId,
20758
+ batchId: expectedBatchId,
20759
+ batchVectorStoreId: batch.vector_store_id,
20760
+ logLabel,
20761
+ });
20762
+ }
20763
+ else if (batch.vector_store_id !== vectorStoreId) {
20764
+ console.error('[🤰]', 'Vector store file batch vector store id mismatch', {
20765
+ vectorStoreId,
20766
+ batchId: expectedBatchId,
20767
+ batchVectorStoreId: batch.vector_store_id,
20768
+ logLabel,
20769
+ });
20770
+ }
20771
+ if (this.options.isVerbose) {
20772
+ console.info('[🤰]', 'Created vector store file batch', {
20773
+ vectorStoreId,
20774
+ batchId: expectedBatchId,
20775
+ fileCount: fileIds.length,
20776
+ logLabel,
20777
+ });
20778
+ }
20779
+ const pollStartedAtMs = Date.now();
20780
+ const progressLogIntervalMs = Math.max(VECTOR_STORE_PROGRESS_LOG_INTERVAL_MIN_MS, pollIntervalMs);
20781
+ const diagnosticsIntervalMs = Math.max(60000, pollIntervalMs * 5);
20782
+ // let lastStatus: string | undefined;
20783
+ let lastCountsKey = '';
20784
+ let lastProgressKey = '';
20785
+ let lastLogAtMs = 0;
20786
+ let lastProgressAtMs = pollStartedAtMs;
20787
+ let lastDiagnosticsAtMs = pollStartedAtMs;
20788
+ let latestBatch = batch;
20789
+ let loggedBatchIdMismatch = false;
20790
+ let loggedBatchIdFallback = false;
20791
+ let loggedBatchIdInvalid = false;
20792
+ let shouldPoll = true;
20793
+ while (shouldPoll) {
20794
+ const nowMs = Date.now();
20795
+ // [🤰] Note: Sometimes OpenAI returns Vector Store object instead of Batch object, or IDs get swapped.
20796
+ const rawBatchId = typeof latestBatch.id === 'string' ? latestBatch.id : '';
20797
+ const rawVectorStoreId = latestBatch.vector_store_id;
20798
+ let returnedBatchId = rawBatchId;
20799
+ let returnedBatchIdValid = typeof returnedBatchId === 'string' && returnedBatchId.startsWith('vsfb_');
20800
+ if (!returnedBatchIdValid && expectedBatchIdValid) {
20801
+ if (!loggedBatchIdFallback) {
20802
+ console.error('[🤰]', 'Vector store file batch id missing from response; falling back to expected', {
20803
+ vectorStoreId,
20804
+ expectedBatchId,
20805
+ returnedBatchId,
20806
+ rawVectorStoreId,
20807
+ logLabel,
20808
+ });
20809
+ loggedBatchIdFallback = true;
20810
+ }
20811
+ returnedBatchId = expectedBatchId;
20812
+ returnedBatchIdValid = true;
20813
+ }
20814
+ if (!returnedBatchIdValid && !loggedBatchIdInvalid) {
20815
+ console.error('[🤰]', 'Vector store file batch id is invalid; stopping polling', {
20816
+ vectorStoreId,
20817
+ expectedBatchId,
20818
+ returnedBatchId,
20819
+ rawVectorStoreId,
20820
+ logLabel,
20821
+ });
20822
+ loggedBatchIdInvalid = true;
20823
+ }
20824
+ const batchIdMismatch = expectedBatchIdValid && returnedBatchIdValid && returnedBatchId !== expectedBatchId;
20825
+ if (batchIdMismatch && !loggedBatchIdMismatch) {
20826
+ console.error('[🤰]', 'Vector store file batch id mismatch', {
20827
+ vectorStoreId,
20828
+ expectedBatchId,
20829
+ returnedBatchId,
20830
+ logLabel,
20831
+ });
20832
+ loggedBatchIdMismatch = true;
20833
+ }
20834
+ if (returnedBatchIdValid) {
20835
+ latestBatch = await vectorStores.fileBatches.retrieve(returnedBatchId, {
20836
+ vector_store_id: vectorStoreId,
20837
+ });
20838
+ }
20839
+ else {
20840
+ shouldPoll = false;
20841
+ continue;
20842
+ }
20843
+ const status = (_e = latestBatch.status) !== null && _e !== void 0 ? _e : 'unknown';
20844
+ const fileCounts = (_f = latestBatch.file_counts) !== null && _f !== void 0 ? _f : {};
20845
+ const progressKey = JSON.stringify(fileCounts);
20846
+ const statusCountsKey = `${status}-${progressKey}`;
20847
+ const isProgressing = progressKey !== lastProgressKey;
20848
+ if (isProgressing) {
20849
+ lastProgressAtMs = nowMs;
20850
+ lastProgressKey = progressKey;
20851
+ }
20852
+ if (this.options.isVerbose &&
20853
+ (statusCountsKey !== lastCountsKey || nowMs - lastLogAtMs >= progressLogIntervalMs)) {
20854
+ console.info('[🤰]', 'Vector store file batch status', {
20855
+ vectorStoreId,
20856
+ batchId: returnedBatchId,
20857
+ status,
20858
+ fileCounts,
20859
+ elapsedMs: nowMs - pollStartedAtMs,
20860
+ logLabel,
20861
+ });
20862
+ lastCountsKey = statusCountsKey;
20863
+ lastLogAtMs = nowMs;
20864
+ }
20865
+ if (status === 'in_progress' &&
20866
+ nowMs - lastProgressAtMs >= VECTOR_STORE_STALL_LOG_THRESHOLD_MS &&
20867
+ nowMs - lastDiagnosticsAtMs >= diagnosticsIntervalMs) {
20868
+ lastDiagnosticsAtMs = nowMs;
20869
+ await this.logVectorStoreFileBatchDiagnostics({
20870
+ client,
20871
+ vectorStoreId,
20872
+ batchId: returnedBatchId,
20873
+ uploadedFiles,
20874
+ logLabel,
20875
+ reason: 'stalled',
20876
+ });
20877
+ }
20878
+ if (status === 'completed') {
20879
+ if (this.options.isVerbose) {
20880
+ console.info('[🤰]', 'Vector store file batch completed', {
20881
+ vectorStoreId,
20882
+ batchId: returnedBatchId,
20883
+ fileCounts,
20884
+ elapsedMs: nowMs - pollStartedAtMs,
20885
+ logLabel,
20886
+ });
20887
+ }
20888
+ shouldPoll = false;
20889
+ continue;
20890
+ }
20891
+ if (status === 'failed') {
20892
+ console.error('[🤰]', 'Vector store file batch completed with failures', {
20893
+ vectorStoreId,
20894
+ batchId: returnedBatchId,
20895
+ fileCounts,
20896
+ elapsedMs: nowMs - pollStartedAtMs,
20897
+ logLabel,
20898
+ });
20899
+ await this.logVectorStoreFileBatchDiagnostics({
20900
+ client,
20901
+ vectorStoreId,
20902
+ batchId: returnedBatchId,
20903
+ uploadedFiles,
20904
+ logLabel,
20905
+ reason: 'failed',
20906
+ });
20907
+ shouldPoll = false;
20908
+ continue;
20909
+ }
20910
+ if (status === 'cancelled') {
20911
+ console.error('[🤰]', 'Vector store file batch did not complete', {
20912
+ vectorStoreId,
20913
+ batchId: returnedBatchId,
20914
+ status,
20915
+ fileCounts,
20916
+ elapsedMs: nowMs - pollStartedAtMs,
20917
+ logLabel,
20918
+ });
20919
+ await this.logVectorStoreFileBatchDiagnostics({
20920
+ client,
20921
+ vectorStoreId,
20922
+ batchId: returnedBatchId,
20923
+ uploadedFiles,
20924
+ logLabel,
20925
+ reason: 'failed',
20926
+ });
20927
+ shouldPoll = false;
20928
+ continue;
20929
+ }
20930
+ if (nowMs - pollStartedAtMs >= uploadTimeoutMs) {
20931
+ console.error('[🤰]', 'Timed out waiting for vector store file batch', {
20932
+ vectorStoreId,
20933
+ batchId: returnedBatchId,
20934
+ fileCounts,
20935
+ elapsedMs: nowMs - pollStartedAtMs,
20936
+ uploadTimeoutMs,
20937
+ logLabel,
20938
+ });
20939
+ await this.logVectorStoreFileBatchDiagnostics({
20940
+ client,
20941
+ vectorStoreId,
20942
+ batchId: returnedBatchId,
20943
+ uploadedFiles,
20944
+ logLabel,
20945
+ reason: 'timeout',
20946
+ });
20947
+ if (this.shouldContinueOnVectorStoreStall()) {
20948
+ console.warn('[🤰]', 'Continuing despite vector store timeout as requested', {
20949
+ vectorStoreId,
20950
+ logLabel,
20951
+ });
20952
+ shouldPoll = false;
20953
+ continue;
20954
+ }
20955
+ try {
20956
+ const cancelBatchId = batchIdMismatch && returnedBatchId.startsWith('vsfb_') ? returnedBatchId : expectedBatchId;
20957
+ if (!cancelBatchId.startsWith('vsfb_')) {
20958
+ console.error('[🤰]', 'Skipping vector store file batch cancel (invalid batch id)', {
20959
+ vectorStoreId,
20960
+ batchId: cancelBatchId,
20961
+ logLabel,
20962
+ });
20963
+ }
20964
+ else {
20965
+ await vectorStores.fileBatches.cancel(cancelBatchId, {
20966
+ vector_store_id: vectorStoreId,
20967
+ });
20968
+ }
20969
+ if (this.options.isVerbose) {
20970
+ console.info('[🤰]', 'Cancelled vector store file batch after timeout', {
20971
+ vectorStoreId,
20972
+ batchId: batchIdMismatch && returnedBatchId.startsWith('vsfb_')
20973
+ ? returnedBatchId
20974
+ : expectedBatchId,
20975
+ ...(batchIdMismatch ? { returnedBatchId } : {}),
20976
+ logLabel,
20977
+ });
20978
+ }
20979
+ }
20980
+ catch (error) {
20981
+ assertsError(error);
20982
+ console.error('[🤰]', 'Failed to cancel vector store file batch after timeout', {
20983
+ vectorStoreId,
20984
+ batchId: expectedBatchId,
20985
+ ...(batchIdMismatch ? { returnedBatchId } : {}),
20986
+ logLabel,
20987
+ error: serializeError(error),
20988
+ });
20989
+ }
20990
+ shouldPoll = false;
20991
+ continue;
20992
+ }
20993
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
20994
+ }
20995
+ return latestBatch;
20996
+ }
20997
+ /**
20998
+ * Creates a vector store and uploads knowledge sources, returning its ID.
20999
+ */
21000
+ async createVectorStoreWithKnowledgeSources(options) {
21001
+ const { client, name, knowledgeSources, logLabel } = options;
21002
+ const vectorStores = this.getVectorStoresApi(client);
21003
+ const knowledgeSourcesCount = knowledgeSources.length;
21004
+ const downloadTimeoutMs = this.getKnowledgeSourceDownloadTimeoutMs();
21005
+ if (this.options.isVerbose) {
21006
+ console.info('[🤰]', 'Creating vector store with knowledge sources', {
21007
+ name,
21008
+ knowledgeSourcesCount,
21009
+ downloadTimeoutMs,
21010
+ logLabel,
21011
+ });
21199
21012
  }
21200
- const batch = await client.beta.vectorStores.fileBatches.create(vectorStoreId, {
21201
- file_ids: fileIds,
21013
+ const vectorStore = await vectorStores.create({
21014
+ name: `${name} Knowledge Base`,
21202
21015
  });
21203
- const expectedBatchId = batch.id;
21204
- const expectedBatchIdValid = expectedBatchId.startsWith('vsfb_');
21205
- if (!expectedBatchIdValid) {
21206
- console.error('[🤰]', 'Vector store file batch id looks invalid', {
21016
+ const vectorStoreId = vectorStore.id;
21017
+ if (this.options.isVerbose) {
21018
+ console.info('[🤰]', 'Vector store created', {
21207
21019
  vectorStoreId,
21208
- batchId: expectedBatchId,
21209
- batchVectorStoreId: batch.vector_store_id,
21210
21020
  logLabel,
21211
21021
  });
21212
21022
  }
21213
- else if (batch.vector_store_id !== vectorStoreId) {
21214
- console.error('[🤰]', 'Vector store file batch vector store id mismatch', {
21215
- vectorStoreId,
21216
- batchId: expectedBatchId,
21217
- batchVectorStoreId: batch.vector_store_id,
21218
- logLabel,
21219
- });
21023
+ const fileStreams = [];
21024
+ const skippedSources = [];
21025
+ let totalBytes = 0;
21026
+ const processingStartedAtMs = Date.now();
21027
+ for (const [index, source] of knowledgeSources.entries()) {
21028
+ try {
21029
+ const sourceType = source.startsWith('http') || source.startsWith('https') ? 'url' : 'file';
21030
+ if (this.options.isVerbose) {
21031
+ console.info('[🤰]', 'Processing knowledge source', {
21032
+ index: index + 1,
21033
+ total: knowledgeSourcesCount,
21034
+ source,
21035
+ sourceType,
21036
+ logLabel,
21037
+ });
21038
+ }
21039
+ // Check if it's a URL
21040
+ if (source.startsWith('http://') || source.startsWith('https://')) {
21041
+ const downloadResult = await this.downloadKnowledgeSourceFile({
21042
+ source,
21043
+ timeoutMs: downloadTimeoutMs,
21044
+ logLabel,
21045
+ });
21046
+ if (downloadResult) {
21047
+ fileStreams.push(downloadResult.file);
21048
+ totalBytes += downloadResult.sizeBytes;
21049
+ }
21050
+ else {
21051
+ skippedSources.push({ source, reason: 'download_failed' });
21052
+ }
21053
+ }
21054
+ else {
21055
+ skippedSources.push({ source, reason: 'unsupported_source_type' });
21056
+ if (this.options.isVerbose) {
21057
+ console.info('[🤰]', 'Skipping knowledge source (unsupported type)', {
21058
+ source,
21059
+ sourceType,
21060
+ logLabel,
21061
+ });
21062
+ }
21063
+ /*
21064
+ TODO: [🤰] Resolve problem with browser environment
21065
+ // Assume it's a local file path
21066
+ // Note: This will work in Node.js environment
21067
+ // For browser environments, this would need different handling
21068
+ const fs = await import('fs');
21069
+ const fileStream = fs.createReadStream(source);
21070
+ fileStreams.push(fileStream);
21071
+ */
21072
+ }
21073
+ }
21074
+ catch (error) {
21075
+ assertsError(error);
21076
+ skippedSources.push({ source, reason: 'processing_error' });
21077
+ console.error('[🤰]', 'Error processing knowledge source', {
21078
+ source,
21079
+ logLabel,
21080
+ error: serializeError(error),
21081
+ });
21082
+ }
21220
21083
  }
21221
21084
  if (this.options.isVerbose) {
21222
- console.info('[🤰]', 'Created vector store file batch', {
21223
- vectorStoreId,
21224
- batchId: expectedBatchId,
21225
- fileCount: fileIds.length,
21085
+ console.info('[🤰]', 'Finished processing knowledge sources', {
21086
+ total: knowledgeSourcesCount,
21087
+ downloadedCount: fileStreams.length,
21088
+ skippedCount: skippedSources.length,
21089
+ totalBytes,
21090
+ elapsedMs: Date.now() - processingStartedAtMs,
21091
+ skippedSamples: skippedSources.slice(0, 3),
21226
21092
  logLabel,
21227
21093
  });
21228
21094
  }
21229
- const pollStartedAtMs = Date.now();
21230
- const progressLogIntervalMs = Math.max(VECTOR_STORE_PROGRESS_LOG_INTERVAL_MIN_MS, pollIntervalMs);
21231
- const diagnosticsIntervalMs = Math.max(60000, pollIntervalMs * 5);
21232
- let lastStatus;
21233
- let lastCountsKey = '';
21234
- let lastProgressKey = '';
21235
- let lastLogAtMs = 0;
21236
- let lastProgressAtMs = pollStartedAtMs;
21237
- let lastDiagnosticsAtMs = pollStartedAtMs;
21238
- let latestBatch = batch;
21239
- let loggedBatchIdMismatch = false;
21240
- let shouldPoll = true;
21241
- while (shouldPoll) {
21242
- latestBatch = await client.beta.vectorStores.fileBatches.retrieve(vectorStoreId, expectedBatchId);
21243
- const counts = latestBatch.file_counts;
21244
- const countsKey = `${counts.completed}/${counts.failed}/${counts.in_progress}/${counts.cancelled}/${counts.total}`;
21245
- const nowMs = Date.now();
21246
- const returnedBatchId = latestBatch.id;
21247
- // [🤰] Note: Sometimes OpenAI returns Vector Store object instead of Batch object, or IDs get swapped.
21248
- // We only consider it a mismatch if the returned ID looks like a Batch ID.
21249
- const batchIdMismatch = returnedBatchId !== expectedBatchId && returnedBatchId.startsWith('vsfb_');
21250
- const diagnosticsBatchId = batchIdMismatch && returnedBatchId.startsWith('vsfb_') ? returnedBatchId : expectedBatchId;
21251
- const shouldLog = this.options.isVerbose &&
21252
- (latestBatch.status !== lastStatus ||
21253
- countsKey !== lastCountsKey ||
21254
- nowMs - lastLogAtMs >= progressLogIntervalMs);
21255
- if (batchIdMismatch && !loggedBatchIdMismatch) {
21256
- console.error('[🤰]', 'Vector store file batch id mismatch', {
21095
+ if (fileStreams.length > 0) {
21096
+ if (this.options.isVerbose) {
21097
+ console.info('[🤰]', 'Uploading files to vector store', {
21257
21098
  vectorStoreId,
21258
- expectedBatchId,
21259
- returnedBatchId,
21260
- status: latestBatch.status,
21261
- fileCounts: counts,
21099
+ fileCount: fileStreams.length,
21100
+ totalBytes,
21101
+ maxConcurrency: this.getKnowledgeSourceUploadMaxConcurrency(),
21102
+ pollIntervalMs: this.getKnowledgeSourceUploadPollIntervalMs(),
21103
+ uploadTimeoutMs: this.getKnowledgeSourceUploadTimeoutMs(),
21262
21104
  logLabel,
21263
21105
  });
21264
- loggedBatchIdMismatch = true;
21265
- }
21266
- if (countsKey !== lastProgressKey) {
21267
- lastProgressKey = countsKey;
21268
- lastProgressAtMs = nowMs;
21269
21106
  }
21270
- if (shouldLog) {
21271
- console.info('[🤰]', 'Vector store file batch status', {
21107
+ try {
21108
+ await this.uploadKnowledgeSourceFilesToVectorStore({
21109
+ client,
21272
21110
  vectorStoreId,
21273
- batchId: expectedBatchId,
21274
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21275
- status: latestBatch.status,
21276
- fileCounts: counts,
21277
- elapsedMs: nowMs - pollStartedAtMs,
21111
+ files: fileStreams,
21112
+ totalBytes,
21278
21113
  logLabel,
21279
21114
  });
21280
- // [🤰] If there are in-progress files for a long time, log their details
21281
- if (counts.in_progress > 0 && nowMs - lastProgressAtMs > VECTOR_STORE_STALL_LOG_THRESHOLD_MS) {
21282
- await this.logVectorStoreFileBatchDiagnostics({
21283
- client,
21284
- vectorStoreId,
21285
- batchId: diagnosticsBatchId,
21286
- uploadedFiles,
21287
- logLabel,
21288
- reason: 'stalled',
21289
- });
21290
- }
21291
- lastStatus = latestBatch.status;
21292
- lastCountsKey = countsKey;
21293
- lastLogAtMs = nowMs;
21294
21115
  }
21295
- if (nowMs - lastProgressAtMs >= diagnosticsIntervalMs &&
21296
- nowMs - lastDiagnosticsAtMs >= diagnosticsIntervalMs) {
21297
- lastDiagnosticsAtMs = nowMs;
21298
- await this.logVectorStoreFileBatchDiagnostics({
21299
- client,
21116
+ catch (error) {
21117
+ assertsError(error);
21118
+ console.error('[🤰]', 'Error uploading files to vector store', {
21300
21119
  vectorStoreId,
21301
- batchId: diagnosticsBatchId,
21302
- uploadedFiles,
21303
21120
  logLabel,
21304
- reason: 'stalled',
21121
+ error: serializeError(error),
21305
21122
  });
21306
21123
  }
21307
- if (latestBatch.status === 'completed') {
21308
- if (this.options.isVerbose) {
21309
- console.info('[🤰]', 'Vector store file batch completed', {
21310
- vectorStoreId,
21311
- batchId: expectedBatchId,
21312
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21313
- fileCounts: latestBatch.file_counts,
21314
- elapsedMs: Date.now() - uploadStartedAtMs,
21315
- logLabel,
21316
- });
21317
- }
21318
- if (latestBatch.file_counts.failed > 0) {
21319
- console.error('[🤰]', 'Vector store file batch completed with failures', {
21320
- vectorStoreId,
21321
- batchId: expectedBatchId,
21322
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21323
- fileCounts: latestBatch.file_counts,
21324
- logLabel,
21325
- });
21326
- await this.logVectorStoreFileBatchDiagnostics({
21327
- client,
21328
- vectorStoreId,
21329
- batchId: diagnosticsBatchId,
21330
- uploadedFiles,
21331
- logLabel,
21332
- reason: 'failed',
21333
- });
21334
- }
21335
- shouldPoll = false;
21336
- continue;
21124
+ }
21125
+ else if (this.options.isVerbose) {
21126
+ console.info('[🤰]', 'No knowledge source files to upload', {
21127
+ vectorStoreId,
21128
+ skippedCount: skippedSources.length,
21129
+ logLabel,
21130
+ });
21131
+ }
21132
+ return {
21133
+ vectorStoreId,
21134
+ uploadedFileCount: fileStreams.length,
21135
+ skippedCount: skippedSources.length,
21136
+ totalBytes,
21137
+ };
21138
+ }
21139
+ }
21140
+
21141
+ const DEFAULT_AGENT_KIT_MODEL_NAME = 'gpt-5.2';
21142
+ /**
21143
+ * Execution tools for OpenAI AgentKit (Agents SDK).
21144
+ *
21145
+ * @public exported from `@promptbook/openai`
21146
+ */
21147
+ class OpenAiAgentKitExecutionTools extends OpenAiVectorStoreHandler {
21148
+ /**
21149
+ * Creates OpenAI AgentKit execution tools.
21150
+ */
21151
+ constructor(options) {
21152
+ var _a;
21153
+ if (options.isProxied) {
21154
+ throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI AgentKit`);
21155
+ }
21156
+ super(options);
21157
+ this.preparedAgentKitAgent = null;
21158
+ this.agentKitModelName = (_a = options.agentKitModelName) !== null && _a !== void 0 ? _a : DEFAULT_AGENT_KIT_MODEL_NAME;
21159
+ }
21160
+ get title() {
21161
+ return 'OpenAI AgentKit';
21162
+ }
21163
+ get description() {
21164
+ return 'Use OpenAI AgentKit for agent-style chat with tools and knowledge';
21165
+ }
21166
+ /**
21167
+ * Calls OpenAI AgentKit with a chat prompt (non-streaming).
21168
+ */
21169
+ async callChatModel(prompt) {
21170
+ return this.callChatModelStream(prompt, () => { });
21171
+ }
21172
+ /**
21173
+ * Calls OpenAI AgentKit with a chat prompt (streaming).
21174
+ */
21175
+ async callChatModelStream(prompt, onProgress) {
21176
+ const { content, parameters, modelRequirements } = prompt;
21177
+ if (modelRequirements.modelVariant !== 'CHAT') {
21178
+ throw new PipelineExecutionError('Use callChatModel only for CHAT variant');
21179
+ }
21180
+ for (const key of ['maxTokens', 'modelName', 'seed', 'temperature']) {
21181
+ if (modelRequirements[key] !== undefined) {
21182
+ throw new NotYetImplementedError(`In \`OpenAiAgentKitExecutionTools\` you cannot specify \`${key}\``);
21183
+ }
21184
+ }
21185
+ const rawPromptContent = templateParameters(content, {
21186
+ ...parameters,
21187
+ modelName: this.agentKitModelName,
21188
+ });
21189
+ const preparedAgentKitAgent = await this.prepareAgentKitAgent({
21190
+ name: (prompt.title || 'Agent'),
21191
+ instructions: modelRequirements.systemMessage || '',
21192
+ knowledgeSources: modelRequirements.knowledgeSources,
21193
+ tools: 'tools' in prompt && Array.isArray(prompt.tools) ? prompt.tools : modelRequirements.tools,
21194
+ });
21195
+ return this.callChatModelStreamWithPreparedAgent({
21196
+ openAiAgentKitAgent: preparedAgentKitAgent.agent,
21197
+ prompt,
21198
+ rawPromptContent,
21199
+ onProgress,
21200
+ });
21201
+ }
21202
+ /**
21203
+ * Returns a prepared AgentKit agent when the server wants to manage caching externally.
21204
+ */
21205
+ getPreparedAgentKitAgent() {
21206
+ return this.preparedAgentKitAgent;
21207
+ }
21208
+ /**
21209
+ * Stores a prepared AgentKit agent for later reuse by external cache managers.
21210
+ */
21211
+ setPreparedAgentKitAgent(preparedAgent) {
21212
+ this.preparedAgentKitAgent = preparedAgent;
21213
+ }
21214
+ /**
21215
+ * Creates a new tools instance bound to a prepared AgentKit agent.
21216
+ */
21217
+ getPreparedAgentTools(preparedAgent) {
21218
+ const tools = new OpenAiAgentKitExecutionTools(this.agentKitOptions);
21219
+ tools.setPreparedAgentKitAgent(preparedAgent);
21220
+ return tools;
21221
+ }
21222
+ /**
21223
+ * Prepares an AgentKit agent with optional knowledge sources and tool definitions.
21224
+ */
21225
+ async prepareAgentKitAgent(options) {
21226
+ var _a, _b;
21227
+ const { name, instructions, knowledgeSources, tools, vectorStoreId: cachedVectorStoreId, storeAsPrepared, } = options;
21228
+ await this.ensureAgentKitDefaults();
21229
+ if (this.options.isVerbose) {
21230
+ console.info('[🤰]', 'Preparing OpenAI AgentKit agent', {
21231
+ name,
21232
+ instructionsLength: instructions.length,
21233
+ knowledgeSourcesCount: (_a = knowledgeSources === null || knowledgeSources === void 0 ? void 0 : knowledgeSources.length) !== null && _a !== void 0 ? _a : 0,
21234
+ toolsCount: (_b = tools === null || tools === void 0 ? void 0 : tools.length) !== null && _b !== void 0 ? _b : 0,
21235
+ });
21236
+ }
21237
+ let vectorStoreId = cachedVectorStoreId;
21238
+ if (!vectorStoreId && knowledgeSources && knowledgeSources.length > 0) {
21239
+ const vectorStoreResult = await this.createVectorStoreWithKnowledgeSources({
21240
+ client: await this.getClient(),
21241
+ name,
21242
+ knowledgeSources,
21243
+ logLabel: 'agentkit preparation',
21244
+ });
21245
+ vectorStoreId = vectorStoreResult.vectorStoreId;
21246
+ }
21247
+ else if (vectorStoreId && this.options.isVerbose) {
21248
+ console.info('[🤰]', 'Using cached vector store for AgentKit agent', {
21249
+ name,
21250
+ vectorStoreId,
21251
+ });
21252
+ }
21253
+ const agentKitTools = this.buildAgentKitTools({ tools, vectorStoreId });
21254
+ const openAiAgentKitAgent = new Agent$1({
21255
+ name,
21256
+ model: this.agentKitModelName,
21257
+ instructions: instructions || 'You are a helpful assistant.',
21258
+ tools: agentKitTools,
21259
+ });
21260
+ const preparedAgent = {
21261
+ agent: openAiAgentKitAgent,
21262
+ vectorStoreId,
21263
+ };
21264
+ if (storeAsPrepared) {
21265
+ this.setPreparedAgentKitAgent(preparedAgent);
21266
+ }
21267
+ if (this.options.isVerbose) {
21268
+ console.info('[🤰]', 'OpenAI AgentKit agent ready', {
21269
+ name,
21270
+ model: this.agentKitModelName,
21271
+ toolCount: agentKitTools.length,
21272
+ hasVectorStore: Boolean(vectorStoreId),
21273
+ });
21274
+ }
21275
+ return preparedAgent;
21276
+ }
21277
+ /**
21278
+ * Ensures the AgentKit SDK is wired to the OpenAI client and API key.
21279
+ */
21280
+ async ensureAgentKitDefaults() {
21281
+ const client = await this.getClient();
21282
+ setDefaultOpenAIClient(client);
21283
+ const apiKey = this.agentKitOptions.apiKey;
21284
+ if (apiKey && typeof apiKey === 'string') {
21285
+ setDefaultOpenAIKey(apiKey);
21286
+ }
21287
+ }
21288
+ /**
21289
+ * Builds the tool list for AgentKit, including hosted file search when applicable.
21290
+ */
21291
+ buildAgentKitTools(options) {
21292
+ var _a;
21293
+ const { tools, vectorStoreId } = options;
21294
+ const agentKitTools = [];
21295
+ if (vectorStoreId) {
21296
+ agentKitTools.push(fileSearchTool(vectorStoreId));
21297
+ }
21298
+ if (tools && tools.length > 0) {
21299
+ const scriptTools = this.resolveScriptTools();
21300
+ for (const toolDefinition of tools) {
21301
+ agentKitTools.push(tool({
21302
+ name: toolDefinition.name,
21303
+ description: toolDefinition.description,
21304
+ parameters: toolDefinition.parameters
21305
+ ? {
21306
+ ...toolDefinition.parameters,
21307
+ additionalProperties: false,
21308
+ required: (_a = toolDefinition.parameters.required) !== null && _a !== void 0 ? _a : [],
21309
+ }
21310
+ : undefined,
21311
+ strict: false,
21312
+ execute: async (input, runContext, details) => {
21313
+ var _a, _b, _c;
21314
+ const scriptTool = scriptTools[0];
21315
+ const functionName = toolDefinition.name;
21316
+ const calledAt = $getCurrentDate();
21317
+ const callId = (_a = details === null || details === void 0 ? void 0 : details.toolCall) === null || _a === void 0 ? void 0 : _a.callId;
21318
+ const functionArgs = input !== null && input !== void 0 ? input : {};
21319
+ if (this.options.isVerbose) {
21320
+ console.info('[🤰]', 'Executing AgentKit tool', {
21321
+ functionName,
21322
+ callId,
21323
+ calledAt,
21324
+ });
21325
+ }
21326
+ try {
21327
+ return await scriptTool.execute({
21328
+ scriptLanguage: 'javascript',
21329
+ script: `
21330
+ const args = ${JSON.stringify(functionArgs)};
21331
+ return await ${functionName}(args);
21332
+ `,
21333
+ parameters: (_c = (_b = runContext === null || runContext === void 0 ? void 0 : runContext.context) === null || _b === void 0 ? void 0 : _b.parameters) !== null && _c !== void 0 ? _c : {},
21334
+ });
21335
+ }
21336
+ catch (error) {
21337
+ assertsError(error);
21338
+ const serializedError = serializeError(error);
21339
+ const errorMessage = spaceTrim$2((block) => `
21340
+
21341
+ The invoked tool \`${functionName}\` failed with error:
21342
+
21343
+ \`\`\`json
21344
+ ${block(JSON.stringify(serializedError, null, 4))}
21345
+ \`\`\`
21346
+
21347
+ `);
21348
+ console.error('[🤰]', 'AgentKit tool execution failed', {
21349
+ functionName,
21350
+ callId,
21351
+ error: serializedError,
21352
+ });
21353
+ return errorMessage;
21354
+ }
21355
+ },
21356
+ }));
21337
21357
  }
21338
- if (latestBatch.status === 'failed' || latestBatch.status === 'cancelled') {
21339
- console.error('[🤰]', 'Vector store file batch did not complete', {
21340
- vectorStoreId,
21341
- batchId: expectedBatchId,
21342
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21343
- status: latestBatch.status,
21344
- fileCounts: latestBatch.file_counts,
21345
- elapsedMs: Date.now() - uploadStartedAtMs,
21346
- logLabel,
21347
- });
21348
- await this.logVectorStoreFileBatchDiagnostics({
21349
- client,
21350
- vectorStoreId,
21351
- batchId: diagnosticsBatchId,
21352
- uploadedFiles,
21353
- logLabel,
21354
- reason: 'failed',
21358
+ }
21359
+ return agentKitTools;
21360
+ }
21361
+ /**
21362
+ * Resolves the configured script tools for tool execution.
21363
+ */
21364
+ resolveScriptTools() {
21365
+ const executionTools = this.options.executionTools;
21366
+ if (!executionTools || !executionTools.script) {
21367
+ throw new PipelineExecutionError(`Model requested tools but no executionTools.script were provided in OpenAiAgentKitExecutionTools options`);
21368
+ }
21369
+ return Array.isArray(executionTools.script) ? executionTools.script : [executionTools.script];
21370
+ }
21371
+ /**
21372
+ * Runs a prepared AgentKit agent and streams results back to the caller.
21373
+ */
21374
+ async callChatModelStreamWithPreparedAgent(options) {
21375
+ var _a, _b, _c, _d;
21376
+ const { openAiAgentKitAgent, prompt, onProgress } = options;
21377
+ const rawPromptContent = (_a = options.rawPromptContent) !== null && _a !== void 0 ? _a : templateParameters(prompt.content, {
21378
+ ...prompt.parameters,
21379
+ modelName: this.agentKitModelName,
21380
+ });
21381
+ const start = $getCurrentDate();
21382
+ let latestContent = '';
21383
+ const toolCalls = [];
21384
+ const toolCallIndexById = new Map();
21385
+ const inputItems = await this.buildAgentKitInputItems(prompt, rawPromptContent);
21386
+ const rawRequest = {
21387
+ agentName: openAiAgentKitAgent.name,
21388
+ input: inputItems,
21389
+ };
21390
+ const streamResult = await run(openAiAgentKitAgent, inputItems, {
21391
+ stream: true,
21392
+ context: { parameters: prompt.parameters },
21393
+ });
21394
+ for await (const event of streamResult) {
21395
+ if (event.type === 'raw_model_stream_event' && ((_b = event.data) === null || _b === void 0 ? void 0 : _b.type) === 'output_text_delta') {
21396
+ latestContent += event.data.delta;
21397
+ onProgress({
21398
+ content: latestContent,
21399
+ modelName: this.agentKitModelName,
21400
+ timing: { start, complete: $getCurrentDate() },
21401
+ usage: UNCERTAIN_USAGE,
21402
+ rawPromptContent: rawPromptContent,
21403
+ rawRequest: null,
21404
+ rawResponse: {},
21355
21405
  });
21356
- shouldPoll = false;
21357
21406
  continue;
21358
21407
  }
21359
- if (nowMs - pollStartedAtMs >= uploadTimeoutMs) {
21360
- console.error('[🤰]', 'Timed out waiting for vector store file batch', {
21361
- vectorStoreId,
21362
- batchId: expectedBatchId,
21363
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21364
- fileCounts: latestBatch.file_counts,
21365
- elapsedMs: nowMs - pollStartedAtMs,
21366
- uploadTimeoutMs,
21367
- logLabel,
21368
- });
21369
- await this.logVectorStoreFileBatchDiagnostics({
21370
- client,
21371
- vectorStoreId,
21372
- batchId: diagnosticsBatchId,
21373
- uploadedFiles,
21374
- logLabel,
21375
- reason: 'timeout',
21376
- });
21377
- if (this.shouldContinueOnVectorStoreStall()) {
21378
- console.warn('[🤰]', 'Continuing despite vector store timeout as requested', {
21379
- vectorStoreId,
21380
- logLabel,
21408
+ if (event.type === 'run_item_stream_event') {
21409
+ const rawItem = (_c = event.item) === null || _c === void 0 ? void 0 : _c.rawItem;
21410
+ if (event.name === 'tool_called' && (rawItem === null || rawItem === void 0 ? void 0 : rawItem.type) === 'function_call') {
21411
+ const toolCall = {
21412
+ name: rawItem.name,
21413
+ arguments: rawItem.arguments,
21414
+ rawToolCall: rawItem,
21415
+ createdAt: $getCurrentDate(),
21416
+ };
21417
+ toolCallIndexById.set(rawItem.callId, toolCalls.length);
21418
+ toolCalls.push(toolCall);
21419
+ onProgress({
21420
+ content: latestContent,
21421
+ modelName: this.agentKitModelName,
21422
+ timing: { start, complete: $getCurrentDate() },
21423
+ usage: UNCERTAIN_USAGE,
21424
+ rawPromptContent: rawPromptContent,
21425
+ rawRequest: null,
21426
+ rawResponse: {},
21427
+ toolCalls: [toolCall],
21381
21428
  });
21382
- shouldPoll = false;
21383
- continue;
21384
21429
  }
21385
- try {
21386
- const cancelBatchId = batchIdMismatch && returnedBatchId.startsWith('vsfb_') ? returnedBatchId : expectedBatchId;
21387
- if (!cancelBatchId.startsWith('vsfb_')) {
21388
- console.error('[🤰]', 'Skipping vector store file batch cancel (invalid batch id)', {
21389
- vectorStoreId,
21390
- batchId: cancelBatchId,
21391
- logLabel,
21392
- });
21393
- }
21394
- else {
21395
- await client.beta.vectorStores.fileBatches.cancel(vectorStoreId, cancelBatchId);
21396
- }
21397
- if (this.options.isVerbose) {
21398
- console.info('[🤰]', 'Cancelled vector store file batch after timeout', {
21399
- vectorStoreId,
21400
- batchId: batchIdMismatch && returnedBatchId.startsWith('vsfb_')
21401
- ? returnedBatchId
21402
- : expectedBatchId,
21403
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21404
- logLabel,
21430
+ if (event.name === 'tool_output' && (rawItem === null || rawItem === void 0 ? void 0 : rawItem.type) === 'function_call_result') {
21431
+ const index = toolCallIndexById.get(rawItem.callId);
21432
+ const result = this.formatAgentKitToolOutput(rawItem.output);
21433
+ if (index !== undefined) {
21434
+ const existingToolCall = toolCalls[index];
21435
+ const completedToolCall = {
21436
+ ...existingToolCall,
21437
+ result,
21438
+ rawToolCall: rawItem,
21439
+ };
21440
+ toolCalls[index] = completedToolCall;
21441
+ onProgress({
21442
+ content: latestContent,
21443
+ modelName: this.agentKitModelName,
21444
+ timing: { start, complete: $getCurrentDate() },
21445
+ usage: UNCERTAIN_USAGE,
21446
+ rawPromptContent: rawPromptContent,
21447
+ rawRequest: null,
21448
+ rawResponse: {},
21449
+ toolCalls: [completedToolCall],
21405
21450
  });
21406
21451
  }
21407
21452
  }
21408
- catch (error) {
21409
- assertsError(error);
21410
- console.error('[🤰]', 'Failed to cancel vector store file batch after timeout', {
21411
- vectorStoreId,
21412
- batchId: expectedBatchId,
21413
- ...(batchIdMismatch ? { returnedBatchId } : {}),
21414
- logLabel,
21415
- error: serializeError(error),
21453
+ }
21454
+ }
21455
+ await streamResult.completed;
21456
+ const complete = $getCurrentDate();
21457
+ const finalContent = ((_d = streamResult.finalOutput) !== null && _d !== void 0 ? _d : latestContent);
21458
+ const finalResult = {
21459
+ content: finalContent,
21460
+ modelName: this.agentKitModelName,
21461
+ timing: { start, complete },
21462
+ usage: UNCERTAIN_USAGE,
21463
+ rawPromptContent: rawPromptContent,
21464
+ rawRequest,
21465
+ rawResponse: { runResult: streamResult },
21466
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
21467
+ };
21468
+ onProgress(finalResult);
21469
+ return finalResult;
21470
+ }
21471
+ /**
21472
+ * Builds AgentKit input items from the prompt and optional thread.
21473
+ */
21474
+ async buildAgentKitInputItems(prompt, rawPromptContent) {
21475
+ var _a;
21476
+ const inputItems = [];
21477
+ if ('thread' in prompt && Array.isArray(prompt.thread)) {
21478
+ for (const message of prompt.thread) {
21479
+ const sender = message.sender;
21480
+ const content = (_a = message.content) !== null && _a !== void 0 ? _a : '';
21481
+ if (sender === 'assistant' || sender === 'agent') {
21482
+ inputItems.push({
21483
+ role: 'assistant',
21484
+ status: 'completed',
21485
+ content: [{ type: 'output_text', text: content }],
21486
+ });
21487
+ }
21488
+ else {
21489
+ inputItems.push({
21490
+ role: 'user',
21491
+ content,
21416
21492
  });
21417
21493
  }
21418
- shouldPoll = false;
21419
- continue;
21420
21494
  }
21421
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
21422
21495
  }
21423
- return latestBatch;
21496
+ const userContent = await this.buildAgentKitUserContent(prompt, rawPromptContent);
21497
+ inputItems.push({
21498
+ role: 'user',
21499
+ content: userContent,
21500
+ });
21501
+ return inputItems;
21424
21502
  }
21425
21503
  /**
21426
- * Creates a vector store and uploads knowledge sources, returning its ID.
21504
+ * Builds the user message content for AgentKit runs, including file inputs when provided.
21427
21505
  */
21428
- async createVectorStoreWithKnowledgeSources(options) {
21429
- const { client, name, knowledgeSources, logLabel } = options;
21430
- const knowledgeSourcesCount = knowledgeSources.length;
21431
- const downloadTimeoutMs = this.getKnowledgeSourceDownloadTimeoutMs();
21506
+ async buildAgentKitUserContent(prompt, rawPromptContent) {
21507
+ if ('files' in prompt && Array.isArray(prompt.files) && prompt.files.length > 0) {
21508
+ const fileItems = await Promise.all(prompt.files.map(async (file) => {
21509
+ const arrayBuffer = await file.arrayBuffer();
21510
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
21511
+ return {
21512
+ type: 'input_image',
21513
+ image: `data:${file.type};base64,${base64}`,
21514
+ };
21515
+ }));
21516
+ return [{ type: 'input_text', text: rawPromptContent }, ...fileItems];
21517
+ }
21518
+ return rawPromptContent;
21519
+ }
21520
+ /**
21521
+ * Normalizes AgentKit tool outputs into a string for Promptbook tool call results.
21522
+ */
21523
+ formatAgentKitToolOutput(output) {
21524
+ if (typeof output === 'string') {
21525
+ return output;
21526
+ }
21527
+ if (output && typeof output === 'object') {
21528
+ const textOutput = output;
21529
+ if (textOutput.type === 'text' && typeof textOutput.text === 'string') {
21530
+ return textOutput.text;
21531
+ }
21532
+ }
21533
+ return JSON.stringify(output !== null && output !== void 0 ? output : null);
21534
+ }
21535
+ /**
21536
+ * Returns AgentKit-specific options.
21537
+ */
21538
+ get agentKitOptions() {
21539
+ return this.options;
21540
+ }
21541
+ /**
21542
+ * Discriminant for type guards.
21543
+ */
21544
+ get discriminant() {
21545
+ return DISCRIMINANT$1;
21546
+ }
21547
+ /**
21548
+ * Type guard to check if given `LlmExecutionTools` are instanceof `OpenAiAgentKitExecutionTools`.
21549
+ */
21550
+ static isOpenAiAgentKitExecutionTools(llmExecutionTools) {
21551
+ return llmExecutionTools.discriminant === DISCRIMINANT$1;
21552
+ }
21553
+ }
21554
+ /**
21555
+ * Discriminant for type guards.
21556
+ *
21557
+ * @private const of `OpenAiAgentKitExecutionTools`
21558
+ */
21559
+ const DISCRIMINANT$1 = 'OPEN_AI_AGENT_KIT_V1';
21560
+
21561
+ /**
21562
+ * Uploads files to OpenAI and returns their IDs
21563
+ *
21564
+ * @private utility for `OpenAiAssistantExecutionTools` and `OpenAiCompatibleExecutionTools`
21565
+ */
21566
+ async function uploadFilesToOpenAi(client, files) {
21567
+ const fileIds = [];
21568
+ for (const file of files) {
21569
+ // Note: OpenAI API expects a File object or a ReadStream
21570
+ // In browser environment, we can pass the File object directly
21571
+ // In Node.js environment, we might need to convert it or use a different approach
21572
+ // But since `Prompt.files` already contains `File` objects, we try to pass them directly
21573
+ const uploadedFile = await client.files.create({
21574
+ file: file,
21575
+ purpose: 'assistants',
21576
+ });
21577
+ fileIds.push(uploadedFile.id);
21578
+ }
21579
+ return fileIds;
21580
+ }
21581
+
21582
+ /**
21583
+ * Execution Tools for calling OpenAI API Assistants
21584
+ *
21585
+ * This is useful for calling OpenAI API with a single assistant, for more wide usage use `OpenAiExecutionTools`.
21586
+ *
21587
+ * Note: [🦖] There are several different things in Promptbook:
21588
+ * - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
21589
+ * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
21590
+ * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
21591
+ * - `OpenAiAssistantExecutionTools` - which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities, recommended for usage in `Agent` or `AgentLlmExecutionTools`
21592
+ * - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
21593
+ *
21594
+ * @deprecated Use `OpenAiAgentKitExecutionTools` instead.
21595
+ * @public exported from `@promptbook/openai`
21596
+ */
21597
+ class OpenAiAssistantExecutionTools extends OpenAiVectorStoreHandler {
21598
+ /**
21599
+ * Creates OpenAI Execution Tools.
21600
+ *
21601
+ * @param options which are relevant are directly passed to the OpenAI client
21602
+ */
21603
+ constructor(options) {
21604
+ var _a;
21605
+ if (options.isProxied) {
21606
+ throw new NotYetImplementedError(`Proxy mode is not yet implemented for OpenAI assistants`);
21607
+ }
21608
+ super(options);
21609
+ this.isCreatingNewAssistantsAllowed = false;
21610
+ this.assistantId = options.assistantId;
21611
+ this.isCreatingNewAssistantsAllowed = (_a = options.isCreatingNewAssistantsAllowed) !== null && _a !== void 0 ? _a : false;
21612
+ if (this.assistantId === null && !this.isCreatingNewAssistantsAllowed) {
21613
+ throw new NotAllowed(`Assistant ID is null and creating new assistants is not allowed - this configuration does not make sense`);
21614
+ }
21615
+ // <- TODO: !!! `OpenAiAssistantExecutionToolsOptions` - Allow `assistantId: null` together with `isCreatingNewAssistantsAllowed: true`
21616
+ // TODO: [👱] Make limiter same as in `OpenAiExecutionTools`
21617
+ }
21618
+ get title() {
21619
+ return 'OpenAI Assistant';
21620
+ }
21621
+ get description() {
21622
+ return 'Use single assistant provided by OpenAI';
21623
+ }
21624
+ /**
21625
+ * Calls OpenAI API to use a chat model.
21626
+ */
21627
+ async callChatModel(prompt) {
21628
+ return this.callChatModelStream(prompt, () => { });
21629
+ }
21630
+ /**
21631
+ * Calls OpenAI API to use a chat model with streaming.
21632
+ */
21633
+ async callChatModelStream(prompt, onProgress) {
21634
+ var _a, _b, _c, _d, _e, _f;
21432
21635
  if (this.options.isVerbose) {
21433
- console.info('[🤰]', 'Creating vector store with knowledge sources', {
21434
- name,
21435
- knowledgeSourcesCount,
21436
- downloadTimeoutMs,
21437
- logLabel,
21438
- });
21636
+ console.info('💬 OpenAI callChatModel call', { prompt });
21637
+ }
21638
+ const { content, parameters, modelRequirements /*, format*/ } = prompt;
21639
+ const client = await this.getClient();
21640
+ // TODO: [☂] Use here more modelRequirements
21641
+ if (modelRequirements.modelVariant !== 'CHAT') {
21642
+ throw new PipelineExecutionError('Use callChatModel only for CHAT variant');
21439
21643
  }
21440
- const vectorStore = await client.beta.vectorStores.create({
21441
- name: `${name} Knowledge Base`,
21644
+ // TODO: [👨‍👨‍👧‍👧] Remove:
21645
+ for (const key of ['maxTokens', 'modelName', 'seed', 'temperature']) {
21646
+ if (modelRequirements[key] !== undefined) {
21647
+ throw new NotYetImplementedError(`In \`OpenAiAssistantExecutionTools\` you cannot specify \`${key}\``);
21648
+ }
21649
+ }
21650
+ /*
21651
+ TODO: [👨‍👨‍👧‍👧] Implement all of this for Assistants
21652
+ const modelName = modelRequirements.modelName || this.getDefaultChatModel().modelName;
21653
+ const modelSettings = {
21654
+ model: modelName,
21655
+
21656
+ temperature: modelRequirements.temperature,
21657
+
21658
+ // <- TODO: [🈁] Use `seed` here AND/OR use is `isDeterministic` for entire execution tools
21659
+ // <- Note: [🧆]
21660
+ } as OpenAI.Chat.Completions.CompletionCreateParamsNonStreaming; // <- TODO: Guard here types better
21661
+
21662
+ if (format === 'JSON') {
21663
+ modelSettings.response_format = {
21664
+ type: 'json_object',
21665
+ };
21666
+ }
21667
+ */
21668
+ // <- TODO: [🚸] Not all models are compatible with JSON mode
21669
+ // > 'response_format' of type 'json_object' is not supported with this model.
21670
+ const rawPromptContent = templateParameters(content, {
21671
+ ...parameters,
21672
+ modelName: 'assistant',
21673
+ // <- [🧠] What is the best value here
21442
21674
  });
21443
- const vectorStoreId = vectorStore.id;
21444
- if (this.options.isVerbose) {
21445
- console.info('[🤰]', 'Vector store created', {
21446
- vectorStoreId,
21447
- logLabel,
21448
- });
21675
+ // Build thread messages: include previous thread messages + current user message
21676
+ const threadMessages = [];
21677
+ // TODO: [🈹] Maybe this should not be here but in other place, look at commit 39d705e75e5bcf7a818c3af36bc13e1c8475c30c
21678
+ // Add previous messages from thread (if any)
21679
+ if ('thread' in prompt && Array.isArray(prompt.thread)) {
21680
+ const previousMessages = prompt.thread.map((msg) => ({
21681
+ role: (msg.sender === 'assistant' ? 'assistant' : 'user'),
21682
+ content: msg.content,
21683
+ }));
21684
+ threadMessages.push(...previousMessages);
21449
21685
  }
21450
- const fileStreams = [];
21451
- const skippedSources = [];
21452
- let totalBytes = 0;
21453
- const processingStartedAtMs = Date.now();
21454
- for (const [index, source] of knowledgeSources.entries()) {
21455
- try {
21456
- const sourceType = source.startsWith('http') || source.startsWith('https') ? 'url' : 'file';
21457
- if (this.options.isVerbose) {
21458
- console.info('[🤰]', 'Processing knowledge source', {
21459
- index: index + 1,
21460
- total: knowledgeSourcesCount,
21461
- source,
21462
- sourceType,
21463
- logLabel,
21464
- });
21465
- }
21466
- // Check if it's a URL
21467
- if (source.startsWith('http://') || source.startsWith('https://')) {
21468
- const downloadResult = await this.downloadKnowledgeSourceFile({
21469
- source,
21470
- timeoutMs: downloadTimeoutMs,
21471
- logLabel,
21472
- });
21473
- if (downloadResult) {
21474
- fileStreams.push(downloadResult.file);
21475
- totalBytes += downloadResult.sizeBytes;
21476
- }
21477
- else {
21478
- skippedSources.push({ source, reason: 'download_failed' });
21686
+ // Always add the current user message
21687
+ const currentUserMessage = {
21688
+ role: 'user',
21689
+ content: rawPromptContent,
21690
+ };
21691
+ if ('files' in prompt && Array.isArray(prompt.files) && prompt.files.length > 0) {
21692
+ const fileIds = await uploadFilesToOpenAi(client, prompt.files);
21693
+ currentUserMessage.attachments = fileIds.map((fileId) => ({
21694
+ file_id: fileId,
21695
+ tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
21696
+ }));
21697
+ }
21698
+ threadMessages.push(currentUserMessage);
21699
+ // Check if tools are being used - if so, use non-streaming mode
21700
+ const hasTools = modelRequirements.tools !== undefined && modelRequirements.tools.length > 0;
21701
+ const start = $getCurrentDate();
21702
+ let complete;
21703
+ // [🐱‍🚀] When tools are present, we need to use the non-streaming Runs API
21704
+ // because streaming doesn't support tool execution flow properly
21705
+ if (hasTools) {
21706
+ onProgress({
21707
+ content: '',
21708
+ modelName: 'assistant',
21709
+ timing: { start, complete: $getCurrentDate() },
21710
+ usage: UNCERTAIN_USAGE,
21711
+ rawPromptContent,
21712
+ rawRequest: null,
21713
+ rawResponse: null,
21714
+ });
21715
+ const rawRequest = {
21716
+ assistant_id: this.assistantId,
21717
+ thread: {
21718
+ messages: threadMessages,
21719
+ },
21720
+ tools: mapToolsToOpenAi(modelRequirements.tools),
21721
+ };
21722
+ if (this.options.isVerbose) {
21723
+ console.info(colors.bgWhite('rawRequest (non-streaming with tools)'), JSON.stringify(rawRequest, null, 4));
21724
+ }
21725
+ // Create thread and run
21726
+ let run = (await client.beta.threads.createAndRun(rawRequest));
21727
+ const completedToolCalls = [];
21728
+ const toolCallStartedAt = new Map();
21729
+ // Poll until run completes or requires action
21730
+ while (run.status === 'queued' || run.status === 'in_progress' || run.status === 'requires_action') {
21731
+ if (run.status === 'requires_action' && ((_a = run.required_action) === null || _a === void 0 ? void 0 : _a.type) === 'submit_tool_outputs') {
21732
+ // Execute tools
21733
+ const toolCalls = run.required_action.submit_tool_outputs.tool_calls;
21734
+ const toolOutputs = [];
21735
+ for (const toolCall of toolCalls) {
21736
+ if (toolCall.type === 'function') {
21737
+ const functionName = toolCall.function.name;
21738
+ const functionArgs = JSON.parse(toolCall.function.arguments);
21739
+ const calledAt = $getCurrentDate();
21740
+ if (toolCall.id) {
21741
+ toolCallStartedAt.set(toolCall.id, calledAt);
21742
+ }
21743
+ onProgress({
21744
+ content: '',
21745
+ modelName: 'assistant',
21746
+ timing: { start, complete: $getCurrentDate() },
21747
+ usage: UNCERTAIN_USAGE,
21748
+ rawPromptContent,
21749
+ rawRequest: null,
21750
+ rawResponse: null,
21751
+ toolCalls: [
21752
+ {
21753
+ name: functionName,
21754
+ arguments: toolCall.function.arguments,
21755
+ result: '',
21756
+ rawToolCall: toolCall,
21757
+ createdAt: calledAt,
21758
+ },
21759
+ ],
21760
+ });
21761
+ if (this.options.isVerbose) {
21762
+ console.info(`🔧 Executing tool: ${functionName}`, functionArgs);
21763
+ }
21764
+ // Get execution tools for script execution
21765
+ const executionTools = this.options
21766
+ .executionTools;
21767
+ if (!executionTools || !executionTools.script) {
21768
+ throw new PipelineExecutionError(`Model requested tool '${functionName}' but no executionTools.script were provided in OpenAiAssistantExecutionTools options`);
21769
+ }
21770
+ // TODO: [DRY] Use some common tool caller (similar to OpenAiCompatibleExecutionTools)
21771
+ const scriptTools = Array.isArray(executionTools.script)
21772
+ ? executionTools.script
21773
+ : [executionTools.script];
21774
+ let functionResponse;
21775
+ let errors;
21776
+ try {
21777
+ const scriptTool = scriptTools[0]; // <- TODO: [🧠] Which script tool to use?
21778
+ functionResponse = await scriptTool.execute({
21779
+ scriptLanguage: 'javascript',
21780
+ script: `
21781
+ const args = ${JSON.stringify(functionArgs)};
21782
+ return await ${functionName}(args);
21783
+ `,
21784
+ parameters: prompt.parameters,
21785
+ });
21786
+ if (this.options.isVerbose) {
21787
+ console.info(`✅ Tool ${functionName} executed:`, functionResponse);
21788
+ }
21789
+ }
21790
+ catch (error) {
21791
+ assertsError(error);
21792
+ const serializedError = serializeError(error);
21793
+ errors = [serializedError];
21794
+ functionResponse = spaceTrim$2((block) => `
21795
+
21796
+ The invoked tool \`${functionName}\` failed with error:
21797
+
21798
+ \`\`\`json
21799
+ ${block(JSON.stringify(serializedError, null, 4))}
21800
+ \`\`\`
21801
+
21802
+ `);
21803
+ console.error(colors.bgRed(`❌ Error executing tool ${functionName}:`));
21804
+ console.error(error);
21805
+ }
21806
+ toolOutputs.push({
21807
+ tool_call_id: toolCall.id,
21808
+ output: functionResponse,
21809
+ });
21810
+ completedToolCalls.push({
21811
+ name: functionName,
21812
+ arguments: toolCall.function.arguments,
21813
+ result: functionResponse,
21814
+ rawToolCall: toolCall,
21815
+ createdAt: toolCall.id ? toolCallStartedAt.get(toolCall.id) || calledAt : calledAt,
21816
+ errors,
21817
+ });
21818
+ }
21479
21819
  }
21820
+ // Submit tool outputs
21821
+ run = (await client.beta.threads.runs.submitToolOutputs(run.thread_id, run.id, {
21822
+ tool_outputs: toolOutputs,
21823
+ }));
21480
21824
  }
21481
21825
  else {
21482
- skippedSources.push({ source, reason: 'unsupported_source_type' });
21483
- if (this.options.isVerbose) {
21484
- console.info('[🤰]', 'Skipping knowledge source (unsupported type)', {
21485
- source,
21486
- sourceType,
21487
- logLabel,
21488
- });
21489
- }
21490
- /*
21491
- TODO: [?????] Resolve problem with browser environment
21492
- // Assume it's a local file path
21493
- // Note: This will work in Node.js environment
21494
- // For browser environments, this would need different handling
21495
- const fs = await import('fs');
21496
- const fileStream = fs.createReadStream(source);
21497
- fileStreams.push(fileStream);
21498
- */
21826
+ // Wait a bit before polling again
21827
+ await new Promise((resolve) => setTimeout(resolve, 500));
21828
+ run = (await client.beta.threads.runs.retrieve(run.thread_id, run.id));
21499
21829
  }
21500
21830
  }
21501
- catch (error) {
21502
- assertsError(error);
21503
- skippedSources.push({ source, reason: 'processing_error' });
21504
- console.error('[🤰]', 'Error processing knowledge source', {
21505
- source,
21506
- logLabel,
21507
- error: serializeError(error),
21508
- });
21831
+ if (run.status !== 'completed') {
21832
+ throw new PipelineExecutionError(`Assistant run failed with status: ${run.status}`);
21833
+ }
21834
+ // Get messages from the thread
21835
+ const messages = await client.beta.threads.messages.list(run.thread_id);
21836
+ const assistantMessages = messages.data.filter((msg) => msg.role === 'assistant');
21837
+ if (assistantMessages.length === 0) {
21838
+ throw new PipelineExecutionError('No assistant messages found after run completion');
21839
+ }
21840
+ const lastMessage = assistantMessages[0];
21841
+ const textContent = lastMessage.content.find((c) => c.type === 'text');
21842
+ if (!textContent || textContent.type !== 'text') {
21843
+ throw new PipelineExecutionError('No text content in assistant response');
21509
21844
  }
21845
+ complete = $getCurrentDate();
21846
+ const resultContent = textContent.text.value;
21847
+ const usage = UNCERTAIN_USAGE;
21848
+ // Progress callback with final result
21849
+ const finalChunk = {
21850
+ content: resultContent,
21851
+ modelName: 'assistant',
21852
+ timing: { start, complete },
21853
+ usage,
21854
+ rawPromptContent,
21855
+ rawRequest,
21856
+ rawResponse: { run, messages: messages.data },
21857
+ toolCalls: completedToolCalls.length > 0 ? completedToolCalls : undefined,
21858
+ };
21859
+ onProgress(finalChunk);
21860
+ return exportJson({
21861
+ name: 'promptResult',
21862
+ message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\` (with tools)`,
21863
+ order: [],
21864
+ value: finalChunk,
21865
+ });
21510
21866
  }
21867
+ // Streaming mode (without tools)
21868
+ const rawRequest = {
21869
+ // TODO: [👨‍👨‍👧‍👧] ...modelSettings,
21870
+ // TODO: [👨‍👨‍👧‍👧][🧠] What about system message for assistants, does it make sense - combination of OpenAI assistants with Promptbook Personas
21871
+ assistant_id: this.assistantId,
21872
+ thread: {
21873
+ messages: threadMessages,
21874
+ },
21875
+ tools: modelRequirements.tools === undefined ? undefined : mapToolsToOpenAi(modelRequirements.tools),
21876
+ // <- TODO: Add user identification here> user: this.options.user,
21877
+ };
21511
21878
  if (this.options.isVerbose) {
21512
- console.info('[🤰]', 'Finished processing knowledge sources', {
21513
- total: knowledgeSourcesCount,
21514
- downloadedCount: fileStreams.length,
21515
- skippedCount: skippedSources.length,
21516
- totalBytes,
21517
- elapsedMs: Date.now() - processingStartedAtMs,
21518
- skippedSamples: skippedSources.slice(0, 3),
21519
- logLabel,
21520
- });
21879
+ console.info(colors.bgWhite('rawRequest (streaming)'), JSON.stringify(rawRequest, null, 4));
21521
21880
  }
21522
- if (fileStreams.length > 0) {
21881
+ const stream = await client.beta.threads.createAndRunStream(rawRequest);
21882
+ stream.on('connect', () => {
21523
21883
  if (this.options.isVerbose) {
21524
- console.info('[🤰]', 'Uploading files to vector store', {
21525
- vectorStoreId,
21526
- fileCount: fileStreams.length,
21527
- totalBytes,
21528
- maxConcurrency: this.getKnowledgeSourceUploadMaxConcurrency(),
21529
- pollIntervalMs: this.getKnowledgeSourceUploadPollIntervalMs(),
21530
- uploadTimeoutMs: this.getKnowledgeSourceUploadTimeoutMs(),
21531
- logLabel,
21532
- });
21884
+ console.info('connect', stream.currentEvent);
21533
21885
  }
21534
- try {
21535
- await this.uploadKnowledgeSourceFilesToVectorStore({
21536
- client,
21537
- vectorStoreId,
21538
- files: fileStreams,
21539
- totalBytes,
21540
- logLabel,
21541
- });
21886
+ });
21887
+ stream.on('textDelta', (textDelta, snapshot) => {
21888
+ if (this.options.isVerbose && textDelta.value) {
21889
+ console.info('textDelta', textDelta.value);
21542
21890
  }
21543
- catch (error) {
21544
- assertsError(error);
21545
- console.error('[🤰]', 'Error uploading files to vector store', {
21546
- vectorStoreId,
21547
- logLabel,
21548
- error: serializeError(error),
21549
- });
21891
+ const chunk = {
21892
+ content: snapshot.value,
21893
+ modelName: 'assistant',
21894
+ timing: {
21895
+ start,
21896
+ complete: $getCurrentDate(),
21897
+ },
21898
+ usage: UNCERTAIN_USAGE,
21899
+ rawPromptContent,
21900
+ rawRequest,
21901
+ rawResponse: snapshot,
21902
+ };
21903
+ onProgress(chunk);
21904
+ });
21905
+ stream.on('messageCreated', (message) => {
21906
+ if (this.options.isVerbose) {
21907
+ console.info('messageCreated', message);
21908
+ }
21909
+ });
21910
+ stream.on('messageDone', (message) => {
21911
+ if (this.options.isVerbose) {
21912
+ console.info('messageDone', message);
21550
21913
  }
21914
+ });
21915
+ // TODO: [🐱‍🚀] Handle tool calls in assistants
21916
+ // Note: OpenAI Assistant streaming with tool calls requires special handling.
21917
+ // The stream will pause when a tool call is needed, and we need to:
21918
+ // 1. Wait for the run to reach 'requires_action' status
21919
+ // 2. Execute the tool calls
21920
+ // 3. Submit tool outputs via a separate API call (not on the stream)
21921
+ // 4. Continue the run
21922
+ // This requires switching to non-streaming mode or using the Runs API directly.
21923
+ // For now, tools with assistants should use the non-streaming chat completions API instead.
21924
+ const rawResponse = await stream.finalMessages();
21925
+ if (this.options.isVerbose) {
21926
+ console.info(colors.bgWhite('rawResponse'), JSON.stringify(rawResponse, null, 4));
21551
21927
  }
21552
- else if (this.options.isVerbose) {
21553
- console.info('[🤰]', 'No knowledge source files to upload', {
21554
- vectorStoreId,
21555
- skippedCount: skippedSources.length,
21556
- logLabel,
21557
- });
21928
+ if (rawResponse.length !== 1) {
21929
+ throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse.length} finalMessages from OpenAI`);
21558
21930
  }
21559
- return {
21560
- vectorStoreId,
21561
- uploadedFileCount: fileStreams.length,
21562
- skippedCount: skippedSources.length,
21563
- totalBytes,
21564
- };
21931
+ if (rawResponse[0].content.length !== 1) {
21932
+ throw new PipelineExecutionError(`There is NOT 1 BUT ${rawResponse[0].content.length} finalMessages content from OpenAI`);
21933
+ }
21934
+ if (((_b = rawResponse[0].content[0]) === null || _b === void 0 ? void 0 : _b.type) !== 'text') {
21935
+ throw new PipelineExecutionError(`There is NOT 'text' BUT ${(_c = rawResponse[0].content[0]) === null || _c === void 0 ? void 0 : _c.type} finalMessages content type from OpenAI`);
21936
+ }
21937
+ let resultContent = (_d = rawResponse[0].content[0]) === null || _d === void 0 ? void 0 : _d.text.value;
21938
+ // Process annotations to replace file IDs with filenames
21939
+ if ((_e = rawResponse[0].content[0]) === null || _e === void 0 ? void 0 : _e.text.annotations) {
21940
+ const annotations = (_f = rawResponse[0].content[0]) === null || _f === void 0 ? void 0 : _f.text.annotations;
21941
+ // Map to store file ID -> filename to avoid duplicate requests
21942
+ const fileIdToName = new Map();
21943
+ for (const annotation of annotations) {
21944
+ if (annotation.type === 'file_citation') {
21945
+ const fileId = annotation.file_citation.file_id;
21946
+ let filename = fileIdToName.get(fileId);
21947
+ if (!filename) {
21948
+ try {
21949
+ const file = await client.files.retrieve(fileId);
21950
+ filename = file.filename;
21951
+ fileIdToName.set(fileId, filename);
21952
+ }
21953
+ catch (error) {
21954
+ console.error(`Failed to retrieve file info for ${fileId}`, error);
21955
+ // Fallback to "Source" or keep original if fetch fails
21956
+ filename = 'Source';
21957
+ }
21958
+ }
21959
+ if (filename && resultContent) {
21960
+ // Replace the citation marker with filename
21961
+ // Regex to match the second part of the citation: 【id†source】 -> 【id†filename】
21962
+ // Note: annotation.text contains the exact marker like 【4:0†source】
21963
+ const newText = annotation.text.replace(/†.*?】/, `†${filename}】`);
21964
+ resultContent = resultContent.replace(annotation.text, newText);
21965
+ }
21966
+ }
21967
+ }
21968
+ }
21969
+ // eslint-disable-next-line prefer-const
21970
+ complete = $getCurrentDate();
21971
+ const usage = UNCERTAIN_USAGE;
21972
+ // <- TODO: [🥘] Compute real usage for assistant
21973
+ // ?> const usage = computeOpenAiUsage(content, resultContent || '', rawResponse);
21974
+ if (resultContent === null) {
21975
+ throw new PipelineExecutionError('No response message from OpenAI');
21976
+ }
21977
+ return exportJson({
21978
+ name: 'promptResult',
21979
+ message: `Result of \`OpenAiAssistantExecutionTools.callChatModelStream\``,
21980
+ order: [],
21981
+ value: {
21982
+ content: resultContent,
21983
+ modelName: 'assistant',
21984
+ // <- TODO: [🥘] Detect used model in assistant
21985
+ // ?> model: rawResponse.model || modelName,
21986
+ timing: {
21987
+ start,
21988
+ complete,
21989
+ },
21990
+ usage,
21991
+ rawPromptContent,
21992
+ rawRequest,
21993
+ rawResponse,
21994
+ // <- [🗯]
21995
+ },
21996
+ });
21997
+ }
21998
+ /*
21999
+ public async playground() {
22000
+ const client = await this.getClient();
22001
+
22002
+ // List all assistants
22003
+ const assistants = await client.beta.assistants.list();
22004
+
22005
+ // Get details of a specific assistant
22006
+ const assistantId = 'asst_MO8fhZf4dGloCfXSHeLcIik0';
22007
+ const assistant = await client.beta.assistants.retrieve(assistantId);
22008
+
22009
+ // Update an assistant
22010
+ const updatedAssistant = await client.beta.assistants.update(assistantId, {
22011
+ name: assistant.name + '(M)',
22012
+ description: 'Updated description via Promptbook',
22013
+ metadata: {
22014
+ [Math.random().toString(36).substring(2, 15)]: new Date().toISOString(),
22015
+ },
22016
+ });
22017
+
22018
+ await forEver();
22019
+ }
22020
+ */
22021
+ /**
22022
+ * Get an existing assistant tool wrapper
22023
+ */
22024
+ getAssistant(assistantId) {
22025
+ return new OpenAiAssistantExecutionTools({
22026
+ ...this.options,
22027
+ isCreatingNewAssistantsAllowed: this.isCreatingNewAssistantsAllowed,
22028
+ assistantId,
22029
+ });
21565
22030
  }
21566
22031
  async createNewAssistant(options) {
21567
22032
  var _a, _b, _c;
@@ -21770,6 +22235,7 @@ function emitAssistantPreparationProgress(options) {
21770
22235
  * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
21771
22236
  * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
21772
22237
  * - `OpenAiAssistantExecutionTools` - (Deprecated) which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities
22238
+ * - `OpenAiAgentKitExecutionTools` - which is a specific implementation of `LlmExecutionTools` backed by OpenAI AgentKit
21773
22239
  * - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
21774
22240
  *
21775
22241
  * @public exported from `@promptbook/core`
@@ -21904,6 +22370,7 @@ class AgentLlmExecutionTools {
21904
22370
  * Calls the chat model with agent-specific system prompt and requirements with streaming
21905
22371
  */
21906
22372
  async callChatModelStream(prompt, onProgress) {
22373
+ var _a, _b;
21907
22374
  // Ensure we're working with a chat prompt
21908
22375
  if (prompt.modelRequirements.modelVariant !== 'CHAT') {
21909
22376
  throw new Error('AgentLlmExecutionTools only supports chat prompts');
@@ -21931,7 +22398,75 @@ class AgentLlmExecutionTools {
21931
22398
  }, // Cast to avoid readonly mismatch from spread
21932
22399
  };
21933
22400
  console.log('!!!! promptWithAgentModelRequirements:', promptWithAgentModelRequirements);
21934
- if (OpenAiAssistantExecutionTools.isOpenAiAssistantExecutionTools(this.options.llmTools)) {
22401
+ if (OpenAiAgentKitExecutionTools.isOpenAiAgentKitExecutionTools(this.options.llmTools)) {
22402
+ const requirementsHash = SHA256(JSON.stringify(modelRequirements)).toString();
22403
+ const vectorStoreHash = SHA256(JSON.stringify((_a = modelRequirements.knowledgeSources) !== null && _a !== void 0 ? _a : [])).toString();
22404
+ const cachedVectorStore = AgentLlmExecutionTools.vectorStoreCache.get(this.title);
22405
+ const cachedAgentKit = AgentLlmExecutionTools.agentKitAgentCache.get(this.title);
22406
+ let preparedAgentKit = this.options.assistantPreparationMode === 'external'
22407
+ ? this.options.llmTools.getPreparedAgentKitAgent()
22408
+ : null;
22409
+ const vectorStoreId = (preparedAgentKit === null || preparedAgentKit === void 0 ? void 0 : preparedAgentKit.vectorStoreId) ||
22410
+ (cachedVectorStore && cachedVectorStore.requirementsHash === vectorStoreHash
22411
+ ? cachedVectorStore.vectorStoreId
22412
+ : undefined);
22413
+ if (!preparedAgentKit && cachedAgentKit && cachedAgentKit.requirementsHash === requirementsHash) {
22414
+ if (this.options.isVerbose) {
22415
+ console.info('[🤰]', 'Using cached OpenAI AgentKit agent', {
22416
+ agent: this.title,
22417
+ });
22418
+ }
22419
+ preparedAgentKit = {
22420
+ agent: cachedAgentKit.agent,
22421
+ vectorStoreId: cachedAgentKit.vectorStoreId,
22422
+ };
22423
+ }
22424
+ if (!preparedAgentKit) {
22425
+ if (this.options.isVerbose) {
22426
+ console.info('[🤰]', 'Preparing OpenAI AgentKit agent', {
22427
+ agent: this.title,
22428
+ });
22429
+ }
22430
+ if (!vectorStoreId && ((_b = modelRequirements.knowledgeSources) === null || _b === void 0 ? void 0 : _b.length)) {
22431
+ emitAssistantPreparationProgress({
22432
+ onProgress,
22433
+ prompt,
22434
+ modelName: this.modelName,
22435
+ phase: 'Creating knowledge base',
22436
+ });
22437
+ }
22438
+ emitAssistantPreparationProgress({
22439
+ onProgress,
22440
+ prompt,
22441
+ modelName: this.modelName,
22442
+ phase: 'Preparing AgentKit agent',
22443
+ });
22444
+ preparedAgentKit = await this.options.llmTools.prepareAgentKitAgent({
22445
+ name: this.title,
22446
+ instructions: modelRequirements.systemMessage || '',
22447
+ knowledgeSources: modelRequirements.knowledgeSources,
22448
+ tools: modelRequirements.tools ? [...modelRequirements.tools] : undefined,
22449
+ vectorStoreId,
22450
+ });
22451
+ }
22452
+ if (preparedAgentKit.vectorStoreId) {
22453
+ AgentLlmExecutionTools.vectorStoreCache.set(this.title, {
22454
+ vectorStoreId: preparedAgentKit.vectorStoreId,
22455
+ requirementsHash: vectorStoreHash,
22456
+ });
22457
+ }
22458
+ AgentLlmExecutionTools.agentKitAgentCache.set(this.title, {
22459
+ agent: preparedAgentKit.agent,
22460
+ requirementsHash,
22461
+ vectorStoreId: preparedAgentKit.vectorStoreId,
22462
+ });
22463
+ underlyingLlmResult = await this.options.llmTools.callChatModelStreamWithPreparedAgent({
22464
+ openAiAgentKitAgent: preparedAgentKit.agent,
22465
+ prompt: promptWithAgentModelRequirements,
22466
+ onProgress,
22467
+ });
22468
+ }
22469
+ else if (OpenAiAssistantExecutionTools.isOpenAiAssistantExecutionTools(this.options.llmTools)) {
21935
22470
  // ... deprecated path ...
21936
22471
  const requirementsHash = SHA256(JSON.stringify(modelRequirements)).toString();
21937
22472
  const cached = AgentLlmExecutionTools.assistantCache.get(this.title);
@@ -22056,6 +22591,10 @@ class AgentLlmExecutionTools {
22056
22591
  return agentResult;
22057
22592
  }
22058
22593
  }
22594
+ /**
22595
+ * Cached AgentKit agents to avoid rebuilding identical instances.
22596
+ */
22597
+ AgentLlmExecutionTools.agentKitAgentCache = new Map();
22059
22598
  /**
22060
22599
  * Cache of OpenAI assistants to avoid creating duplicates
22061
22600
  */
@@ -22137,6 +22676,7 @@ function buildTeacherSummary(commitments, used) {
22137
22676
  * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
22138
22677
  * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
22139
22678
  * - `OpenAiAssistantExecutionTools` - (Deprecated) which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities
22679
+ * - `OpenAiAgentKitExecutionTools` - which is a specific implementation of `LlmExecutionTools` backed by OpenAI AgentKit
22140
22680
  * - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
22141
22681
  *
22142
22682
  * @public exported from `@promptbook/core`
@@ -22507,7 +23047,8 @@ function buildRemoteAgentSource(profile, meta) {
22507
23047
  * - `Agent` - which represents an AI Agent with its source, memories, actions, etc. Agent is a higher-level abstraction which is internally using:
22508
23048
  * - `LlmExecutionTools` - which wraps one or more LLM models and provides an interface to execute them
22509
23049
  * - `AgentLlmExecutionTools` - which is a specific implementation of `LlmExecutionTools` that wraps another LlmExecutionTools and applies agent-specific system prompts and requirements
22510
- * - `OpenAiAssistantExecutionTools` - which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities, recommended for usage in `Agent` or `AgentLlmExecutionTools`
23050
+ * - `OpenAiAssistantExecutionTools` - (Deprecated) which is a specific implementation of `LlmExecutionTools` for OpenAI models with assistant capabilities
23051
+ * - `OpenAiAgentKitExecutionTools` - which is a specific implementation of `LlmExecutionTools` backed by OpenAI AgentKit
22511
23052
  * - `RemoteAgent` - which is an `Agent` that connects to a Promptbook Agents Server
22512
23053
  *
22513
23054
  * @public exported from `@promptbook/core`