@rcrsr/rill-ext-openai 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +81 -4
- package/dist/index.js +940 -14
- package/package.json +10 -6
- package/dist/factory.d.ts +0 -27
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -768
- package/dist/factory.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
package/dist/factory.js
DELETED
|
@@ -1,768 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extension factory for OpenAI API integration.
|
|
3
|
-
* Creates extension instance with config validation and SDK lifecycle management.
|
|
4
|
-
*/
|
|
5
|
-
import OpenAI from 'openai';
|
|
6
|
-
import { RuntimeError, emitExtensionEvent, createVector, isCallable, isVector, } from '@rcrsr/rill';
|
|
7
|
-
import { validateApiKey, validateModel, validateTemperature, validateEmbedText, validateEmbedModel, validateEmbedBatch, mapProviderError, executeToolLoop, } from '@rcrsr/rill-ext-llm-shared';
|
|
8
|
-
// ============================================================
|
|
9
|
-
// CONSTANTS
|
|
10
|
-
// ============================================================
|
|
11
|
-
const DEFAULT_MAX_TOKENS = 4096;
|
|
12
|
-
// ============================================================
|
|
13
|
-
// ERROR DETECTION
|
|
14
|
-
// ============================================================
|
|
15
|
-
/**
|
|
16
|
-
* OpenAI-specific error detector for mapProviderError.
|
|
17
|
-
* Extracts status code and message from OpenAI.APIError instances.
|
|
18
|
-
*
|
|
19
|
-
* @param error - Error to detect
|
|
20
|
-
* @returns Status and message if OpenAI error, null otherwise
|
|
21
|
-
*/
|
|
22
|
-
const detectOpenAIError = (error) => {
|
|
23
|
-
if (error instanceof OpenAI.APIError) {
|
|
24
|
-
return {
|
|
25
|
-
status: error.status ?? undefined,
|
|
26
|
-
message: error.message,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
// ============================================================
|
|
32
|
-
// FACTORY
|
|
33
|
-
// ============================================================
|
|
34
|
-
/**
|
|
35
|
-
* Create OpenAI extension instance.
|
|
36
|
-
* Validates configuration and returns host functions with cleanup.
|
|
37
|
-
*
|
|
38
|
-
* @param config - Extension configuration
|
|
39
|
-
* @returns ExtensionResult with message, messages, embed, embed_batch, tool_loop and dispose
|
|
40
|
-
* @throws Error for invalid configuration (EC-1 through EC-4)
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```typescript
|
|
44
|
-
* const ext = createOpenAIExtension({
|
|
45
|
-
* api_key: process.env.OPENAI_API_KEY,
|
|
46
|
-
* model: 'gpt-4-turbo',
|
|
47
|
-
* temperature: 0.7
|
|
48
|
-
* });
|
|
49
|
-
* // Use with rill runtime...
|
|
50
|
-
* await ext.dispose();
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
export function createOpenAIExtension(config) {
|
|
54
|
-
// Validate required fields (§4.1)
|
|
55
|
-
validateApiKey(config.api_key);
|
|
56
|
-
validateModel(config.model);
|
|
57
|
-
validateTemperature(config.temperature);
|
|
58
|
-
// Instantiate SDK client at factory time (§4.1)
|
|
59
|
-
// Note: will be used in tasks 3.3 and 3.4 for actual function implementations
|
|
60
|
-
const client = new OpenAI({
|
|
61
|
-
apiKey: config.api_key,
|
|
62
|
-
baseURL: config.base_url,
|
|
63
|
-
maxRetries: config.max_retries,
|
|
64
|
-
timeout: config.timeout,
|
|
65
|
-
});
|
|
66
|
-
// Extract config values for use in functions
|
|
67
|
-
const factoryModel = config.model;
|
|
68
|
-
const factoryTemperature = config.temperature;
|
|
69
|
-
const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
70
|
-
const factorySystem = config.system;
|
|
71
|
-
const factoryEmbedModel = config.embed_model;
|
|
72
|
-
// Suppress unused variable warnings for values used in task 3.4
|
|
73
|
-
void factoryEmbedModel;
|
|
74
|
-
// AbortController for cancelling pending requests (§4.9, IR-11)
|
|
75
|
-
let abortController = new AbortController();
|
|
76
|
-
// Dispose function for cleanup (§4.9)
|
|
77
|
-
const dispose = async () => {
|
|
78
|
-
// AC-28: Idempotent cleanup, try-catch each step
|
|
79
|
-
try {
|
|
80
|
-
// Cancel pending API requests via AbortController (IR-11)
|
|
81
|
-
if (abortController) {
|
|
82
|
-
abortController.abort();
|
|
83
|
-
abortController = undefined;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
-
console.warn(`Failed to abort OpenAI requests: ${message}`);
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
// Cleanup SDK HTTP connections
|
|
92
|
-
// Note: OpenAI SDK doesn't expose a close() method, but we include
|
|
93
|
-
// this structure for consistency with extension pattern
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
97
|
-
console.warn(`Failed to cleanup OpenAI SDK: ${message}`);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
// Return extension result with implementations
|
|
101
|
-
const result = {
|
|
102
|
-
// IR-4: openai::message
|
|
103
|
-
message: {
|
|
104
|
-
params: [
|
|
105
|
-
{ name: 'text', type: 'string' },
|
|
106
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
107
|
-
],
|
|
108
|
-
fn: async (args, ctx) => {
|
|
109
|
-
const startTime = Date.now();
|
|
110
|
-
try {
|
|
111
|
-
// Extract arguments
|
|
112
|
-
const text = args[0];
|
|
113
|
-
const options = (args[1] ?? {});
|
|
114
|
-
// EC-5: Validate text is non-empty
|
|
115
|
-
if (text.trim().length === 0) {
|
|
116
|
-
throw new RuntimeError('RILL-R004', 'prompt text cannot be empty');
|
|
117
|
-
}
|
|
118
|
-
// Extract options
|
|
119
|
-
const system = typeof options['system'] === 'string'
|
|
120
|
-
? options['system']
|
|
121
|
-
: factorySystem;
|
|
122
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
123
|
-
? options['max_tokens']
|
|
124
|
-
: factoryMaxTokens;
|
|
125
|
-
// Build messages array (OpenAI uses system as first message, not separate param)
|
|
126
|
-
const apiMessages = [];
|
|
127
|
-
if (system !== undefined) {
|
|
128
|
-
apiMessages.push({
|
|
129
|
-
role: 'system',
|
|
130
|
-
content: system,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
apiMessages.push({
|
|
134
|
-
role: 'user',
|
|
135
|
-
content: text,
|
|
136
|
-
});
|
|
137
|
-
// Call OpenAI API
|
|
138
|
-
const apiParams = {
|
|
139
|
-
model: factoryModel,
|
|
140
|
-
max_tokens: maxTokens,
|
|
141
|
-
messages: apiMessages,
|
|
142
|
-
};
|
|
143
|
-
// Add optional parameters only if defined
|
|
144
|
-
if (factoryTemperature !== undefined) {
|
|
145
|
-
apiParams.temperature = factoryTemperature;
|
|
146
|
-
}
|
|
147
|
-
const response = await client.chat.completions.create(apiParams);
|
|
148
|
-
// Extract text content from response (§4.2: choices[0].message.content)
|
|
149
|
-
const content = response.choices[0]?.message?.content ?? '';
|
|
150
|
-
// Build normalized response dict (§3.2)
|
|
151
|
-
const result = {
|
|
152
|
-
content,
|
|
153
|
-
model: response.model,
|
|
154
|
-
usage: {
|
|
155
|
-
input: response.usage?.prompt_tokens ?? 0,
|
|
156
|
-
output: response.usage?.completion_tokens ?? 0,
|
|
157
|
-
},
|
|
158
|
-
stop_reason: response.choices[0]?.finish_reason ?? 'unknown',
|
|
159
|
-
id: response.id,
|
|
160
|
-
messages: [
|
|
161
|
-
...(system ? [{ role: 'system', content: system }] : []),
|
|
162
|
-
{ role: 'user', content: text },
|
|
163
|
-
{ role: 'assistant', content },
|
|
164
|
-
],
|
|
165
|
-
};
|
|
166
|
-
// Emit success event (§4.10)
|
|
167
|
-
const duration = Date.now() - startTime;
|
|
168
|
-
emitExtensionEvent(ctx, {
|
|
169
|
-
event: 'openai:message',
|
|
170
|
-
subsystem: 'extension:openai',
|
|
171
|
-
duration,
|
|
172
|
-
model: response.model,
|
|
173
|
-
usage: result.usage,
|
|
174
|
-
});
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
// Map error and emit failure event
|
|
179
|
-
const duration = Date.now() - startTime;
|
|
180
|
-
const rillError = mapProviderError('OpenAI', error, detectOpenAIError);
|
|
181
|
-
emitExtensionEvent(ctx, {
|
|
182
|
-
event: 'openai:error',
|
|
183
|
-
subsystem: 'extension:openai',
|
|
184
|
-
error: rillError.message,
|
|
185
|
-
duration,
|
|
186
|
-
});
|
|
187
|
-
throw rillError;
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
description: 'Send single message to OpenAI API',
|
|
191
|
-
returnType: 'dict',
|
|
192
|
-
},
|
|
193
|
-
// IR-5: openai::messages
|
|
194
|
-
messages: {
|
|
195
|
-
params: [
|
|
196
|
-
{ name: 'messages', type: 'list' },
|
|
197
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
198
|
-
],
|
|
199
|
-
fn: async (args, ctx) => {
|
|
200
|
-
const startTime = Date.now();
|
|
201
|
-
try {
|
|
202
|
-
// Extract arguments
|
|
203
|
-
const messages = args[0];
|
|
204
|
-
const options = (args[1] ?? {});
|
|
205
|
-
// AC-23: Empty messages list raises error
|
|
206
|
-
if (messages.length === 0) {
|
|
207
|
-
throw new RuntimeError('RILL-R004', 'messages list cannot be empty');
|
|
208
|
-
}
|
|
209
|
-
// Extract options
|
|
210
|
-
const system = typeof options['system'] === 'string'
|
|
211
|
-
? options['system']
|
|
212
|
-
: factorySystem;
|
|
213
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
214
|
-
? options['max_tokens']
|
|
215
|
-
: factoryMaxTokens;
|
|
216
|
-
// Build messages array (OpenAI uses system as first message)
|
|
217
|
-
const apiMessages = [];
|
|
218
|
-
if (system !== undefined) {
|
|
219
|
-
apiMessages.push({
|
|
220
|
-
role: 'system',
|
|
221
|
-
content: system,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
// Validate and transform messages
|
|
225
|
-
for (let i = 0; i < messages.length; i++) {
|
|
226
|
-
const msg = messages[i];
|
|
227
|
-
// EC-10: Missing role raises error
|
|
228
|
-
if (!msg || typeof msg !== 'object' || !('role' in msg)) {
|
|
229
|
-
throw new RuntimeError('RILL-R004', "message missing required 'role' field");
|
|
230
|
-
}
|
|
231
|
-
const role = msg['role'];
|
|
232
|
-
// EC-11: Unknown role value raises error
|
|
233
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'tool') {
|
|
234
|
-
throw new RuntimeError('RILL-R004', `invalid role '${role}'`);
|
|
235
|
-
}
|
|
236
|
-
// EC-12: User message missing content
|
|
237
|
-
if (role === 'user' || role === 'tool') {
|
|
238
|
-
if (!('content' in msg) || typeof msg['content'] !== 'string') {
|
|
239
|
-
throw new RuntimeError('RILL-R004', `${role} message requires 'content'`);
|
|
240
|
-
}
|
|
241
|
-
apiMessages.push({
|
|
242
|
-
role: role,
|
|
243
|
-
content: msg['content'],
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
// EC-13: Assistant missing both content and tool_calls
|
|
247
|
-
else if (role === 'assistant') {
|
|
248
|
-
const hasContent = 'content' in msg && msg['content'];
|
|
249
|
-
const hasToolCalls = 'tool_calls' in msg && msg['tool_calls'];
|
|
250
|
-
if (!hasContent && !hasToolCalls) {
|
|
251
|
-
throw new RuntimeError('RILL-R004', "assistant message requires 'content' or 'tool_calls'");
|
|
252
|
-
}
|
|
253
|
-
// For now, we only support content
|
|
254
|
-
if (hasContent) {
|
|
255
|
-
apiMessages.push({
|
|
256
|
-
role: 'assistant',
|
|
257
|
-
content: msg['content'],
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// Call OpenAI API
|
|
263
|
-
const apiParams = {
|
|
264
|
-
model: factoryModel,
|
|
265
|
-
max_tokens: maxTokens,
|
|
266
|
-
messages: apiMessages,
|
|
267
|
-
};
|
|
268
|
-
// Add optional parameters only if defined
|
|
269
|
-
if (factoryTemperature !== undefined) {
|
|
270
|
-
apiParams.temperature = factoryTemperature;
|
|
271
|
-
}
|
|
272
|
-
const response = await client.chat.completions.create(apiParams);
|
|
273
|
-
// Extract text content from response
|
|
274
|
-
const content = response.choices[0]?.message?.content ?? '';
|
|
275
|
-
// Build full conversation history (§3.2)
|
|
276
|
-
const fullMessages = [
|
|
277
|
-
...messages.map((m) => {
|
|
278
|
-
const normalized = { role: m['role'] };
|
|
279
|
-
if ('content' in m)
|
|
280
|
-
normalized['content'] = m['content'];
|
|
281
|
-
if ('tool_calls' in m)
|
|
282
|
-
normalized['tool_calls'] = m['tool_calls'];
|
|
283
|
-
return normalized;
|
|
284
|
-
}),
|
|
285
|
-
{ role: 'assistant', content },
|
|
286
|
-
];
|
|
287
|
-
// Build normalized response dict (§3.2)
|
|
288
|
-
const result = {
|
|
289
|
-
content,
|
|
290
|
-
model: response.model,
|
|
291
|
-
usage: {
|
|
292
|
-
input: response.usage?.prompt_tokens ?? 0,
|
|
293
|
-
output: response.usage?.completion_tokens ?? 0,
|
|
294
|
-
},
|
|
295
|
-
stop_reason: response.choices[0]?.finish_reason ?? 'unknown',
|
|
296
|
-
id: response.id,
|
|
297
|
-
messages: fullMessages,
|
|
298
|
-
};
|
|
299
|
-
// Emit success event (§4.10)
|
|
300
|
-
const duration = Date.now() - startTime;
|
|
301
|
-
emitExtensionEvent(ctx, {
|
|
302
|
-
event: 'openai:messages',
|
|
303
|
-
subsystem: 'extension:openai',
|
|
304
|
-
duration,
|
|
305
|
-
model: response.model,
|
|
306
|
-
usage: result.usage,
|
|
307
|
-
});
|
|
308
|
-
return result;
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
// Map error and emit failure event
|
|
312
|
-
const duration = Date.now() - startTime;
|
|
313
|
-
const rillError = mapProviderError('OpenAI', error, detectOpenAIError);
|
|
314
|
-
emitExtensionEvent(ctx, {
|
|
315
|
-
event: 'openai:error',
|
|
316
|
-
subsystem: 'extension:openai',
|
|
317
|
-
error: rillError.message,
|
|
318
|
-
duration,
|
|
319
|
-
});
|
|
320
|
-
throw rillError;
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
description: 'Send multi-turn conversation to OpenAI API',
|
|
324
|
-
returnType: 'dict',
|
|
325
|
-
},
|
|
326
|
-
// IR-6: openai::embed
|
|
327
|
-
embed: {
|
|
328
|
-
params: [{ name: 'text', type: 'string' }],
|
|
329
|
-
fn: async (args, ctx) => {
|
|
330
|
-
const startTime = Date.now();
|
|
331
|
-
try {
|
|
332
|
-
// Extract arguments
|
|
333
|
-
const text = args[0];
|
|
334
|
-
// EC-15: Validate text is non-empty
|
|
335
|
-
validateEmbedText(text.trim());
|
|
336
|
-
// EC-16: Validate embed_model is configured
|
|
337
|
-
validateEmbedModel(factoryEmbedModel);
|
|
338
|
-
// Call OpenAI embeddings API
|
|
339
|
-
const response = await client.embeddings.create({
|
|
340
|
-
model: factoryEmbedModel,
|
|
341
|
-
input: text,
|
|
342
|
-
encoding_format: 'float',
|
|
343
|
-
});
|
|
344
|
-
// Extract embedding data
|
|
345
|
-
const embeddingData = response.data[0]?.embedding;
|
|
346
|
-
if (!embeddingData || embeddingData.length === 0) {
|
|
347
|
-
throw new RuntimeError('RILL-R004', 'OpenAI: empty embedding returned');
|
|
348
|
-
}
|
|
349
|
-
// Convert to Float32Array and create RillVector
|
|
350
|
-
const float32Data = new Float32Array(embeddingData);
|
|
351
|
-
const vector = createVector(float32Data, factoryEmbedModel);
|
|
352
|
-
// Emit success event (§4.10)
|
|
353
|
-
const duration = Date.now() - startTime;
|
|
354
|
-
emitExtensionEvent(ctx, {
|
|
355
|
-
event: 'openai:embed',
|
|
356
|
-
subsystem: 'extension:openai',
|
|
357
|
-
duration,
|
|
358
|
-
model: factoryEmbedModel,
|
|
359
|
-
dimensions: float32Data.length,
|
|
360
|
-
});
|
|
361
|
-
return vector;
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
// Map error and emit failure event
|
|
365
|
-
const duration = Date.now() - startTime;
|
|
366
|
-
const rillError = mapProviderError('OpenAI', error, detectOpenAIError);
|
|
367
|
-
emitExtensionEvent(ctx, {
|
|
368
|
-
event: 'openai:error',
|
|
369
|
-
subsystem: 'extension:openai',
|
|
370
|
-
error: rillError.message,
|
|
371
|
-
duration,
|
|
372
|
-
});
|
|
373
|
-
throw rillError;
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
description: 'Generate embedding vector for text',
|
|
377
|
-
returnType: 'vector',
|
|
378
|
-
},
|
|
379
|
-
// IR-7: openai::embed_batch
|
|
380
|
-
embed_batch: {
|
|
381
|
-
params: [{ name: 'texts', type: 'list' }],
|
|
382
|
-
fn: async (args, ctx) => {
|
|
383
|
-
const startTime = Date.now();
|
|
384
|
-
try {
|
|
385
|
-
// Extract arguments
|
|
386
|
-
const texts = args[0];
|
|
387
|
-
// AC-24: Empty list returns empty list
|
|
388
|
-
if (texts.length === 0) {
|
|
389
|
-
return [];
|
|
390
|
-
}
|
|
391
|
-
// EC-17: Validate embed_model is configured
|
|
392
|
-
validateEmbedModel(factoryEmbedModel);
|
|
393
|
-
// EC-18: Validate all elements are strings
|
|
394
|
-
const stringTexts = validateEmbedBatch(texts);
|
|
395
|
-
// Call OpenAI embeddings API with batch
|
|
396
|
-
const response = await client.embeddings.create({
|
|
397
|
-
model: factoryEmbedModel,
|
|
398
|
-
input: stringTexts,
|
|
399
|
-
encoding_format: 'float',
|
|
400
|
-
});
|
|
401
|
-
// Convert embeddings to RillVector list
|
|
402
|
-
const vectors = [];
|
|
403
|
-
for (const embeddingItem of response.data) {
|
|
404
|
-
const embeddingData = embeddingItem.embedding;
|
|
405
|
-
if (!embeddingData || embeddingData.length === 0) {
|
|
406
|
-
throw new RuntimeError('RILL-R004', 'OpenAI: empty embedding returned');
|
|
407
|
-
}
|
|
408
|
-
const float32Data = new Float32Array(embeddingData);
|
|
409
|
-
const vector = createVector(float32Data, factoryEmbedModel);
|
|
410
|
-
vectors.push(vector);
|
|
411
|
-
}
|
|
412
|
-
// Emit success event (§4.10)
|
|
413
|
-
const duration = Date.now() - startTime;
|
|
414
|
-
const firstVector = vectors[0];
|
|
415
|
-
const dimensions = firstVector && isVector(firstVector) ? firstVector.data.length : 0;
|
|
416
|
-
emitExtensionEvent(ctx, {
|
|
417
|
-
event: 'openai:embed_batch',
|
|
418
|
-
subsystem: 'extension:openai',
|
|
419
|
-
duration,
|
|
420
|
-
model: factoryEmbedModel,
|
|
421
|
-
dimensions,
|
|
422
|
-
count: vectors.length,
|
|
423
|
-
});
|
|
424
|
-
return vectors;
|
|
425
|
-
}
|
|
426
|
-
catch (error) {
|
|
427
|
-
// Map error and emit failure event
|
|
428
|
-
const duration = Date.now() - startTime;
|
|
429
|
-
const rillError = mapProviderError('OpenAI', error, detectOpenAIError);
|
|
430
|
-
emitExtensionEvent(ctx, {
|
|
431
|
-
event: 'openai:error',
|
|
432
|
-
subsystem: 'extension:openai',
|
|
433
|
-
error: rillError.message,
|
|
434
|
-
duration,
|
|
435
|
-
});
|
|
436
|
-
throw rillError;
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
description: 'Generate embedding vectors for multiple texts',
|
|
440
|
-
returnType: 'list',
|
|
441
|
-
},
|
|
442
|
-
// IR-8: openai::tool_loop
|
|
443
|
-
tool_loop: {
|
|
444
|
-
params: [
|
|
445
|
-
{ name: 'prompt', type: 'string' },
|
|
446
|
-
{ name: 'options', type: 'dict', defaultValue: {} },
|
|
447
|
-
],
|
|
448
|
-
fn: async (args, ctx) => {
|
|
449
|
-
const startTime = Date.now();
|
|
450
|
-
try {
|
|
451
|
-
// Extract arguments
|
|
452
|
-
const prompt = args[0];
|
|
453
|
-
const options = (args[1] ?? {});
|
|
454
|
-
// EC-20: Validate prompt is non-empty
|
|
455
|
-
if (prompt.trim().length === 0) {
|
|
456
|
-
throw new RuntimeError('RILL-R004', 'prompt text cannot be empty');
|
|
457
|
-
}
|
|
458
|
-
// EC-21: Validate tools option is present
|
|
459
|
-
if (!('tools' in options) || !Array.isArray(options['tools'])) {
|
|
460
|
-
throw new RuntimeError('RILL-R004', "tool_loop requires 'tools' option");
|
|
461
|
-
}
|
|
462
|
-
const toolDescriptors = options['tools'];
|
|
463
|
-
// Convert tool descriptors array to dict for shared tool loop
|
|
464
|
-
const toolsDict = {};
|
|
465
|
-
for (const descriptor of toolDescriptors) {
|
|
466
|
-
const name = typeof descriptor['name'] === 'string'
|
|
467
|
-
? descriptor['name']
|
|
468
|
-
: null;
|
|
469
|
-
if (!name) {
|
|
470
|
-
throw new RuntimeError('RILL-R004', 'tool descriptor missing name');
|
|
471
|
-
}
|
|
472
|
-
const toolFnValue = descriptor['fn'];
|
|
473
|
-
if (!toolFnValue) {
|
|
474
|
-
throw new RuntimeError('RILL-R004', `tool '${name}' missing fn property`);
|
|
475
|
-
}
|
|
476
|
-
// Validate tool is callable
|
|
477
|
-
if (!isCallable(toolFnValue)) {
|
|
478
|
-
throw new RuntimeError('RILL-R004', `tool '${name}' fn must be callable`);
|
|
479
|
-
}
|
|
480
|
-
// Extract params metadata from descriptor and enhance callable
|
|
481
|
-
const paramsObj = descriptor['params'];
|
|
482
|
-
const description = typeof descriptor['description'] === 'string'
|
|
483
|
-
? descriptor['description']
|
|
484
|
-
: '';
|
|
485
|
-
let enhancedCallable = toolFnValue;
|
|
486
|
-
if (paramsObj &&
|
|
487
|
-
typeof paramsObj === 'object' &&
|
|
488
|
-
!Array.isArray(paramsObj)) {
|
|
489
|
-
// Convert params object to CallableParam[] format
|
|
490
|
-
const params = Object.entries(paramsObj).map(([paramName, paramMeta]) => {
|
|
491
|
-
const meta = paramMeta;
|
|
492
|
-
const typeStr = typeof meta['type'] === 'string' ? meta['type'] : null;
|
|
493
|
-
// Map type string to RillTypeName
|
|
494
|
-
let typeName = null;
|
|
495
|
-
if (typeStr === 'string')
|
|
496
|
-
typeName = 'string';
|
|
497
|
-
else if (typeStr === 'number')
|
|
498
|
-
typeName = 'number';
|
|
499
|
-
else if (typeStr === 'bool' || typeStr === 'boolean')
|
|
500
|
-
typeName = 'bool';
|
|
501
|
-
else if (typeStr === 'list' || typeStr === 'array')
|
|
502
|
-
typeName = 'list';
|
|
503
|
-
else if (typeStr === 'dict' || typeStr === 'object')
|
|
504
|
-
typeName = 'dict';
|
|
505
|
-
else if (typeStr === 'vector')
|
|
506
|
-
typeName = 'vector';
|
|
507
|
-
const param = {
|
|
508
|
-
name: paramName,
|
|
509
|
-
typeName,
|
|
510
|
-
defaultValue: null,
|
|
511
|
-
annotations: {},
|
|
512
|
-
};
|
|
513
|
-
// Add description only if it exists (optional property)
|
|
514
|
-
if (typeof meta['description'] === 'string') {
|
|
515
|
-
param.description =
|
|
516
|
-
meta['description'];
|
|
517
|
-
}
|
|
518
|
-
return param;
|
|
519
|
-
});
|
|
520
|
-
// Create enhanced ApplicationCallable with params metadata
|
|
521
|
-
const baseCallable = toolFnValue;
|
|
522
|
-
enhancedCallable = {
|
|
523
|
-
__type: 'callable',
|
|
524
|
-
kind: 'application',
|
|
525
|
-
params,
|
|
526
|
-
fn: baseCallable.fn,
|
|
527
|
-
description,
|
|
528
|
-
isProperty: baseCallable.isProperty ?? false,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
toolsDict[name] = enhancedCallable;
|
|
532
|
-
}
|
|
533
|
-
// Extract options
|
|
534
|
-
const system = typeof options['system'] === 'string'
|
|
535
|
-
? options['system']
|
|
536
|
-
: factorySystem;
|
|
537
|
-
const maxTokens = typeof options['max_tokens'] === 'number'
|
|
538
|
-
? options['max_tokens']
|
|
539
|
-
: factoryMaxTokens;
|
|
540
|
-
const maxErrors = typeof options['max_errors'] === 'number'
|
|
541
|
-
? options['max_errors']
|
|
542
|
-
: 3;
|
|
543
|
-
const maxTurns = typeof options['max_turns'] === 'number'
|
|
544
|
-
? options['max_turns']
|
|
545
|
-
: 10;
|
|
546
|
-
// Initialize conversation with prepended messages if provided
|
|
547
|
-
const messages = [];
|
|
548
|
-
if (system !== undefined) {
|
|
549
|
-
messages.push({
|
|
550
|
-
role: 'system',
|
|
551
|
-
content: system,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
if ('messages' in options && Array.isArray(options['messages'])) {
|
|
555
|
-
const prependedMessages = options['messages'];
|
|
556
|
-
for (const msg of prependedMessages) {
|
|
557
|
-
if (!msg || typeof msg !== 'object' || !('role' in msg)) {
|
|
558
|
-
throw new RuntimeError('RILL-R004', "message missing required 'role' field");
|
|
559
|
-
}
|
|
560
|
-
const role = msg['role'];
|
|
561
|
-
if (role !== 'user' && role !== 'assistant') {
|
|
562
|
-
throw new RuntimeError('RILL-R004', `invalid role '${role}'`);
|
|
563
|
-
}
|
|
564
|
-
if (!('content' in msg) || typeof msg['content'] !== 'string') {
|
|
565
|
-
throw new RuntimeError('RILL-R004', `${role} message requires 'content'`);
|
|
566
|
-
}
|
|
567
|
-
messages.push({
|
|
568
|
-
role: role,
|
|
569
|
-
content: msg['content'],
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// Add the prompt as initial user message
|
|
574
|
-
messages.push({
|
|
575
|
-
role: 'user',
|
|
576
|
-
content: prompt,
|
|
577
|
-
});
|
|
578
|
-
// Define OpenAI-specific callbacks for shared tool loop
|
|
579
|
-
const callbacks = {
|
|
580
|
-
// Build OpenAI Tool format from tool definitions
|
|
581
|
-
buildTools: (toolDefs) => {
|
|
582
|
-
return toolDefs.map((def) => ({
|
|
583
|
-
type: 'function',
|
|
584
|
-
function: {
|
|
585
|
-
name: def.name,
|
|
586
|
-
description: def.description,
|
|
587
|
-
parameters: def.input_schema,
|
|
588
|
-
},
|
|
589
|
-
}));
|
|
590
|
-
},
|
|
591
|
-
// Call OpenAI API
|
|
592
|
-
callAPI: async (msgs, tools) => {
|
|
593
|
-
const apiParams = {
|
|
594
|
-
model: factoryModel,
|
|
595
|
-
max_tokens: maxTokens,
|
|
596
|
-
messages: msgs,
|
|
597
|
-
tools: tools,
|
|
598
|
-
tool_choice: 'auto',
|
|
599
|
-
};
|
|
600
|
-
if (factoryTemperature !== undefined) {
|
|
601
|
-
apiParams.temperature = factoryTemperature;
|
|
602
|
-
}
|
|
603
|
-
const response = await client.chat.completions.create(apiParams);
|
|
604
|
-
// Normalize response to include usage in expected format
|
|
605
|
-
return {
|
|
606
|
-
...response,
|
|
607
|
-
usage: {
|
|
608
|
-
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
609
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
610
|
-
},
|
|
611
|
-
};
|
|
612
|
-
},
|
|
613
|
-
// Extract tool calls from OpenAI response
|
|
614
|
-
extractToolCalls: (response) => {
|
|
615
|
-
if (!response ||
|
|
616
|
-
typeof response !== 'object' ||
|
|
617
|
-
!('choices' in response)) {
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
const choices = response.choices;
|
|
621
|
-
if (!Array.isArray(choices) || choices.length === 0) {
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
const choice = choices[0];
|
|
625
|
-
if (!choice ||
|
|
626
|
-
typeof choice !== 'object' ||
|
|
627
|
-
!('message' in choice)) {
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
const message = choice.message;
|
|
631
|
-
if (!message ||
|
|
632
|
-
typeof message !== 'object' ||
|
|
633
|
-
!('tool_calls' in message)) {
|
|
634
|
-
return null;
|
|
635
|
-
}
|
|
636
|
-
const toolCalls = message
|
|
637
|
-
.tool_calls;
|
|
638
|
-
if (!toolCalls || !Array.isArray(toolCalls)) {
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
// Filter for function tool calls and extract relevant data
|
|
642
|
-
const functionToolCalls = toolCalls.filter((tc) => typeof tc === 'object' &&
|
|
643
|
-
tc !== null &&
|
|
644
|
-
'type' in tc &&
|
|
645
|
-
tc.type === 'function');
|
|
646
|
-
return functionToolCalls.map((tc) => {
|
|
647
|
-
// Type assertion safe because we filtered for function type
|
|
648
|
-
const functionCall = tc;
|
|
649
|
-
const args = functionCall.function.arguments;
|
|
650
|
-
let parsedArgs;
|
|
651
|
-
try {
|
|
652
|
-
parsedArgs = JSON.parse(args);
|
|
653
|
-
}
|
|
654
|
-
catch {
|
|
655
|
-
parsedArgs = {};
|
|
656
|
-
}
|
|
657
|
-
return {
|
|
658
|
-
id: tc.id,
|
|
659
|
-
name: functionCall.function.name,
|
|
660
|
-
input: parsedArgs,
|
|
661
|
-
};
|
|
662
|
-
});
|
|
663
|
-
},
|
|
664
|
-
// Format tool results into OpenAI message format
|
|
665
|
-
formatToolResult: (toolResults) => {
|
|
666
|
-
// For OpenAI, we need to add assistant message with tool calls,
|
|
667
|
-
// then tool messages with results
|
|
668
|
-
// Since executeToolLoop already extracted the tool calls, we only
|
|
669
|
-
// return the tool result messages here
|
|
670
|
-
return toolResults.map((tr) => ({
|
|
671
|
-
role: 'tool',
|
|
672
|
-
tool_call_id: tr.id,
|
|
673
|
-
content: tr.error
|
|
674
|
-
? JSON.stringify({ error: tr.error, code: 'RILL-R001' })
|
|
675
|
-
: typeof tr.result === 'string'
|
|
676
|
-
? tr.result
|
|
677
|
-
: JSON.stringify(tr.result),
|
|
678
|
-
}));
|
|
679
|
-
},
|
|
680
|
-
};
|
|
681
|
-
// Execute shared tool loop
|
|
682
|
-
const loopResult = await executeToolLoop(messages, toolsDict, maxErrors, callbacks, (event, data) => {
|
|
683
|
-
// Map shared events to OpenAI-specific events
|
|
684
|
-
const eventMap = {
|
|
685
|
-
tool_call: 'openai:tool_call',
|
|
686
|
-
tool_result: 'openai:tool_result',
|
|
687
|
-
};
|
|
688
|
-
emitExtensionEvent(ctx, {
|
|
689
|
-
event: eventMap[event] || event,
|
|
690
|
-
subsystem: 'extension:openai',
|
|
691
|
-
...data,
|
|
692
|
-
});
|
|
693
|
-
}, maxTurns, ctx);
|
|
694
|
-
// Extract response data
|
|
695
|
-
const response = loopResult.response;
|
|
696
|
-
const content = response?.choices[0]?.message?.content ?? '';
|
|
697
|
-
const stopReason = loopResult.turns >= maxTurns
|
|
698
|
-
? 'max_turns'
|
|
699
|
-
: (response?.choices[0]?.finish_reason ?? 'stop');
|
|
700
|
-
// Build conversation history for response
|
|
701
|
-
// Reconstruct full message history from messages array
|
|
702
|
-
const fullMessages = [];
|
|
703
|
-
for (const msg of messages) {
|
|
704
|
-
if ('role' in msg && msg.role !== 'system') {
|
|
705
|
-
const historyMsg = {
|
|
706
|
-
role: msg.role,
|
|
707
|
-
};
|
|
708
|
-
if ('content' in msg && msg.content) {
|
|
709
|
-
historyMsg['content'] = msg.content;
|
|
710
|
-
}
|
|
711
|
-
if ('tool_calls' in msg && msg.tool_calls) {
|
|
712
|
-
historyMsg['tool_calls'] = msg.tool_calls;
|
|
713
|
-
}
|
|
714
|
-
fullMessages.push(historyMsg);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// Add final assistant response if present
|
|
718
|
-
if (response) {
|
|
719
|
-
fullMessages.push({
|
|
720
|
-
role: 'assistant',
|
|
721
|
-
content,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
// Build result dict
|
|
725
|
-
const result = {
|
|
726
|
-
content,
|
|
727
|
-
model: factoryModel,
|
|
728
|
-
usage: {
|
|
729
|
-
input: loopResult.totalTokens.input,
|
|
730
|
-
output: loopResult.totalTokens.output,
|
|
731
|
-
},
|
|
732
|
-
stop_reason: stopReason,
|
|
733
|
-
turns: loopResult.turns,
|
|
734
|
-
messages: fullMessages,
|
|
735
|
-
};
|
|
736
|
-
// Emit success event (§4.10)
|
|
737
|
-
const duration = Date.now() - startTime;
|
|
738
|
-
emitExtensionEvent(ctx, {
|
|
739
|
-
event: 'openai:tool_loop',
|
|
740
|
-
subsystem: 'extension:openai',
|
|
741
|
-
turns: loopResult.turns,
|
|
742
|
-
total_duration: duration,
|
|
743
|
-
usage: result.usage,
|
|
744
|
-
});
|
|
745
|
-
return result;
|
|
746
|
-
}
|
|
747
|
-
catch (error) {
|
|
748
|
-
// Map error and emit failure event
|
|
749
|
-
const duration = Date.now() - startTime;
|
|
750
|
-
const rillError = mapProviderError('OpenAI', error, detectOpenAIError);
|
|
751
|
-
emitExtensionEvent(ctx, {
|
|
752
|
-
event: 'openai:error',
|
|
753
|
-
subsystem: 'extension:openai',
|
|
754
|
-
error: rillError.message,
|
|
755
|
-
duration,
|
|
756
|
-
});
|
|
757
|
-
throw rillError;
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
description: 'Execute tool-use loop with OpenAI API',
|
|
761
|
-
returnType: 'dict',
|
|
762
|
-
},
|
|
763
|
-
};
|
|
764
|
-
// IR-11: Attach dispose lifecycle method
|
|
765
|
-
result.dispose = dispose;
|
|
766
|
-
return result;
|
|
767
|
-
}
|
|
768
|
-
//# sourceMappingURL=factory.js.map
|