aimyapi 0.2.1 → 1.0.3

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/src/index.ts CHANGED
@@ -3,177 +3,259 @@ import fs from 'fs';
3
3
  import { OpenAI } from "openai";
4
4
  import path from 'path';
5
5
 
6
- import createSandbox from "./sandbox";
6
+ import createSandbox, { CodeRunResult } from "./sandbox";
7
7
  import { rejectOpenPromises } from './sandbox-wrappers';
8
8
 
9
9
  import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
10
10
  import { QuickJSWASMModule, getQuickJS } from "quickjs-emscripten";
11
11
 
12
- const openai = new OpenAI({
13
- apiKey: process.env.OPENAI_API_KEY // This is also the default, can be omitted
14
- });
12
+ import { AutoParseableTool } from 'openai/lib/parser.js';
13
+ import { ZodSchema, z } from 'zod';
14
+ import { zodToJsonSchema } from 'zod-to-json-schema';
15
15
 
16
- const CODE_START = `\`\`\`
17
- import * as ApiDefs from 'api.ts'
16
+ const openai = new OpenAI( { baseURL: process.env.OPENAI_API_BASE_URL || undefined, apiKey: process.env.OPENAI_API_KEY || undefined } );
18
17
 
19
- (async() {
20
- try {`;
18
+ let QuickJS:QuickJSWASMModule | null = null;
21
19
 
22
- const CODE_END = ` } catch(err) {
20
+ export interface AIMyAPIOptions {
21
+ apiObject: object;
22
+ apiExports?: object;
23
+ apiWhitelist?: string[]; // List of functions to expose to the user. Necessary to pass this yourself if you are extending your api from another class.
24
+ apiGlobals?: object;
25
+ apiDefFilePath: string; // Full path to the api definition file so that it can be loaded into the prompt
26
+ apiGlobalName?: string;
27
+ apiDocsPath?: string;
28
+ debug?: boolean;
29
+ model?: string;
30
+ additionalModelOptions?: object;
31
+ hideLogsFromAgent?: boolean; // If true, logs from code execution will not be included in the messages sent to the agent. This can be useful to prevent the agent from being overwhelmed with logs or to hide implementation details. Default is false (logs will be included in messages).
32
+ };
33
+
34
+ export interface AIMyAPIInstance {
35
+ options:AIMyAPIOptions;
36
+ generateCode: (queryText:string, userChatHistory:ChatCompletionMessageParam[]) => Promise<GenerateCodeResult | null>;
37
+ runCode: (code:string) => Promise<CodeRunResult>;
38
+ checkCode: (code:string) => Promise<CodeRunResult>;
39
+ // run a single request with no history
40
+ processRequest: (userQuery:string, context?: object) => Promise<GenerateCodeResult | null>;
41
+ }
42
+
43
+ export interface GenerateCodeResult {
44
+ code?: string;
45
+ comments?: string;
46
+ }
47
+
48
+
49
+ const wrapCode = (apiGlobalName:string, apiFileDefPath:string, code:string) => `
50
+ (async () => {
51
+ try {
52
+ ${code}
53
+ } catch(err) {
23
54
  console.error(err);
24
55
  }
25
56
  })();
26
- \`\`\``;
27
-
28
- function indentString(str, numTabs) {
29
- const tabs = '\t'.repeat(numTabs);
30
- return str.split('\n').map(line => tabs + line).join('\n');
31
- }
32
-
33
- function replaceCodeBlocks(text) {
34
- const codeBlockRegex = /```[\s\S]*?```/g;
35
- return text.replace(codeBlockRegex, (match) => {
36
- const code = match.slice(3, -3).trim();
37
- return `${CODE_START}\n${indentString(code, 2)}\n${CODE_END}`;
38
- });
57
+ `;
58
+
59
+ const wrapCodeBlock = (code:string, codeType:string = "") => `
60
+ \`\`\`${codeType}
61
+ ${code}
62
+ \`\`\`
63
+ `;
64
+
65
+ export function zodParseJSON<T>(schema: ZodSchema<T>) {
66
+ return (input: string): T => schema.parse(JSON.parse(input));
39
67
  }
40
68
 
41
- export function createBasePrompt(apiFilePath:string, documentationPath: string): string {
69
+ export function createBasePrompt(apiFilePath:string, apiGlobalName: string, documentationPath?: string, ): string {
42
70
  const apiText: string = fs.readFileSync(apiFilePath, 'utf8');
43
71
  const documentationText: string = documentationPath ? fs.readFileSync(documentationPath, 'utf8') : '';
44
72
 
45
73
  //console.time("Loading templates");
46
- const createTaskPrompt: string = fs.readFileSync( path.join(__dirname, '../prompts/create-task-prompt.md'), 'utf8').replace("{{DOCUMENTATION}}", replaceCodeBlocks(documentationText)).replace("{{API}}", apiText);
74
+ const createTaskPrompt: string = fs.readFileSync( path.join(__dirname, '../prompts/create-task-prompt.md'), 'utf8').replace("{{DOCUMENTATION}}", documentationText).replace("{{API}}", apiText).replace("{{API_GLOBAL_NAME}}", apiGlobalName || "");
47
75
  //console.timeEnd("Loading templates")
48
76
 
49
77
  return createTaskPrompt;
50
78
  }
51
79
 
52
- export interface GenerateCodeResult {
53
- code: string;
54
- loggableCode: string;
55
- rawResponse?: string;
56
- }
57
80
 
58
- // FIXME: add an options parameter and allow specifying the token limit
59
- export const generateCode = async function(queryText:string, userChatHistory:ChatCompletionMessageParam[], createTaskPrompt:string, debug:boolean = false, model="gpt-4o-mini"): Promise<GenerateCodeResult> {
60
- if(!queryText)
61
- return { code: '', loggableCode: '' };
81
+ const runCodeSchema = z.object({
82
+ code: z.string(),
83
+ });
84
+
85
+ const checkCodeSchema = z.object({
86
+ code: z.string(),
87
+ });
88
+
89
+ const submitCodeSchema = z.object({
90
+ code: z.string(),
91
+ });
92
+
93
+ export const generateCode = async function(instance:AIMyAPIInstance, queryText:string, userChatHistory:ChatCompletionMessageParam[], createTaskPrompt:string, debug:boolean = false, hideLogsFromAgent:boolean = false, model="gpt-5-mini", additionalModelOptions?: object): Promise<GenerateCodeResult | null> {
94
+ let finalSubmittedCode = "";
62
95
 
63
- const prompt = createTaskPrompt.replace("{{QUERY_TEXT}}", queryText);
64
- const messages:ChatCompletionMessageParam[] = [
96
+ if(debug) {
97
+ console.log("Generating code for query:", queryText);
98
+ }
99
+ // checkcode
100
+ const codingTools: AutoParseableTool<any, true>[] = [
65
101
  {
66
- role: "system",
67
- content: prompt,
102
+ type: "function",
103
+ function: {
104
+ name: "runCode",
105
+ // @ts-ignore
106
+ function: async ({code}: {code: string}) => {
107
+ if(debug) {
108
+ console.log("Running code...", code);
109
+ }
110
+
111
+ const res = await instance.runCode(code);
112
+
113
+ if(hideLogsFromAgent) {
114
+ res.logs = [];
115
+ }
116
+ return res;
117
+ },
118
+ description: "Run the provided code in a sandboxed environment. The code will have access to the API object. Returns the result of the code execution.",
119
+ parse: zodParseJSON(runCodeSchema),
120
+ parameters: zodToJsonSchema(runCodeSchema),
121
+ }
68
122
  },
69
- ...userChatHistory,
70
123
  {
71
- role: "user",
72
- content: `${queryText}. Respond with json`,
73
- name: "user"
124
+ type: "function",
125
+ function: {
126
+ name: "checkCode",
127
+ // @ts-ignore
128
+ function: async ({code}: {code: string}) => {
129
+ if(debug) {
130
+ console.log("Checking code...", code);
131
+ }
132
+ const res = await instance.checkCode(code);
133
+
134
+ return res;
135
+
136
+ },
137
+ description: "Check the provided code for errors or issues without running it. Returns the result of the code check.",
138
+ parse: zodParseJSON(checkCodeSchema),
139
+ parameters: zodToJsonSchema(checkCodeSchema),
140
+ }
141
+ },
142
+ {
143
+ type: "function",
144
+ function: {
145
+ name: "submitCode",
146
+ // @ts-ignore
147
+ function: async ({code}: {code: string}) => {
148
+ finalSubmittedCode = code;
149
+ return {
150
+ success: true,
151
+ }
152
+ },
153
+ description: "Submit the provided code as the final code to run. This will end the code generation process and the code will be run in the sandbox. Use this when you are confident in the code you have generated and want to run it.",
154
+ parse: zodParseJSON(submitCodeSchema),
155
+ parameters: zodToJsonSchema(submitCodeSchema),
156
+ }
74
157
  }
75
158
  ];
76
159
 
77
160
  try {
78
- const results = await openai.chat.completions.create({
79
- model,
80
- messages: messages,
81
- temperature: 0.0,
82
- max_tokens: 2000,
83
- response_format: {
84
- json_schema: {
85
- name: "codeResponse",
86
- description: "The code generated by the AI",
87
- schema: {
88
- type: 'object',
89
- properties: {
90
- yourCode: {
91
- type: 'string',
92
- description: 'The code generated by the AI',
93
- }
94
- },
95
- },
96
- },
97
- type: 'json_schema',
98
- }
161
+
162
+ const runner = openai.chat.completions
163
+ .runTools({
164
+ model,
165
+ messages: [
166
+ {
167
+ role: 'system',
168
+ content: "You will assist with writing code to perform the user's request.",
169
+ },
170
+ ...userChatHistory,
171
+ {
172
+ role: 'user',
173
+ content: createTaskPrompt.replace("{{USER_QUERY}}", queryText)
174
+ },
175
+
176
+ ],
177
+ tools: codingTools,
178
+ ...additionalModelOptions,
179
+
180
+ })
181
+ .on('message', async (message: ChatCompletionMessageParam) => {
182
+ if(debug) {
183
+ console.log("Message:", JSON.stringify(message, null, 2));
184
+
185
+ }
186
+ })
187
+ .on('functionToolCall', (functionCall: OpenAI.Chat.Completions.ChatCompletionMessageFunctionToolCall.Function) => {
188
+ if(debug) {
189
+ console.log("Function Call:", JSON.stringify(functionCall, null, 2));
99
190
  }
100
- );
191
+
101
192
 
102
- if(debug)
103
- console.log(results.usage);
104
-
105
- const response = results.choices[0].message.content;
193
+ })
194
+ .on('functionToolCallResult', (functionCall) => {
195
+ if(debug) {
196
+ console.log("Function Call Result:", JSON.stringify(functionCall, null, 2));
197
+ }
198
+ })
199
+
200
+ const finalContent = await runner.finalContent();
201
+
202
+ if(debug) {
203
+ console.log("Final Content:", finalContent);
204
+ }
205
+
206
+ return {
207
+ code: finalSubmittedCode,
208
+ comments: finalContent || undefined,
209
+ }
106
210
 
107
- if(debug)
108
- console.log(response);
109
-
110
- try {
111
- const json = JSON.parse(response);
112
- const code = json.yourCode.replaceAll('```', '');
113
-
114
- return { code, loggableCode: code };
115
- } catch(err) {
116
- console.error("Error parsing response", err);
117
- return { code: '', loggableCode: '' };
118
- }
119
211
  } catch(err) {
120
212
  console.error(err);
121
- return { code: '', loggableCode: '' };
122
- }
123
- };
124
-
125
- let QuickJS:QuickJSWASMModule = null;
126
-
127
- export interface AIMyAPIOptions {
128
- apiObject: object;
129
- apiExports: object;
130
- apiWhitelist?: string[]; // List of functions to expose to the user. Necessary to pass this yourself if you are extending your api from another class.
131
- apiGlobals?: object;
132
- apiDefFilePath: string; // Full path to the api definition file so that it can be loaded into the prompt
133
- apiGlobalName?: string;
134
- apiDocsPath?: string;
135
- debug?: boolean;
136
- model?: string;
213
+ return null;
214
+ }
137
215
  };
138
216
 
139
- export interface AIMyAPIInstance {
140
- // see xample 2
141
- options:AIMyAPIOptions;
142
- generateCode: (queryText:string, userChatHistory:ChatCompletionMessageParam[]) => Promise<GenerateCodeResult>;
143
- runCode: (task:string) => Promise<void>;
144
- // run a single request with no history
145
- processRequest: (userQuery:string, context?: object) => Promise<GenerateCodeResult>;
146
-
147
- }
148
-
149
217
  async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
150
218
  // extend options with defaults
151
- options = {
152
- apiGlobalName: "api",
153
- apiGlobals: {},
154
- apiWhitelist: Object.getOwnPropertyNames(Object.getPrototypeOf(options.apiObject)).filter((f) => f !== "constructor" && !f.startsWith("_")),
155
- debug: false,
156
- model: "gpt-4o-mini",
219
+ const localOptions = {
220
+ ...{
221
+ apiGlobalName: "api",
222
+ apiGlobals: {},
223
+ apiWhitelist: Object.getOwnPropertyNames(Object.getPrototypeOf(options.apiObject)).filter((f) => f !== "constructor" && !f.startsWith("_")),
224
+ debug: false,
225
+ model: process.env.OPENAI_API_MODEL || "gpt-5.4-mini",
226
+ additionalModelOptions: {},
227
+ hideLogsFromAgent: false,
228
+ },
157
229
  ...options
158
- } as AIMyAPIOptions;
230
+ };
159
231
 
160
- if(!options.apiObject || !options.apiDefFilePath) {
161
- throw new Error("apiObject and apiFilePath are required");
232
+ if(!localOptions.apiObject || !localOptions.apiDefFilePath) {
233
+ throw new Error("apiObject and apiDefFilePath are required");
162
234
  }
163
235
 
164
- const {apiObject, apiWhitelist, apiGlobalName, apiExports, apiDefFilePath, apiDocsPath, debug, model} = options;
236
+ const {apiObject, apiWhitelist, apiGlobalName, apiExports, apiDefFilePath, apiDocsPath, debug, model, additionalModelOptions, hideLogsFromAgent} = localOptions;
165
237
 
166
- const createTaskPrompt = createBasePrompt( apiDefFilePath, apiDocsPath);
238
+ const createTaskPrompt = createBasePrompt( apiDefFilePath, apiGlobalName, apiDocsPath);
167
239
 
168
- return {
169
- options,
170
- generateCode: async function(queryText:string, userChatHistory:ChatCompletionMessageParam[], currentContext:any = undefined) {
240
+ const instance = {
241
+ options: localOptions,
242
+ generateCode: async function(queryText:string, userChatHistory:ChatCompletionMessageParam[], currentContext:any = undefined) {
171
243
  if(!queryText)
172
- return { code: '', loggableCode: '' };
173
-
174
- return await generateCode(queryText, userChatHistory, createTaskPrompt.replace("{{CONTEXT}}", currentContext ? "```" + JSON.stringify(currentContext, null, 2) + "```" : ""), debug, model);
244
+ return null;
245
+
246
+ const formattedContext = wrapCodeBlock(JSON.stringify(currentContext, null, 2), "json");
247
+
248
+ return await generateCode(instance,
249
+ queryText,
250
+ userChatHistory,
251
+ createTaskPrompt.replace("{{CONTEXT}}", formattedContext),
252
+ debug,
253
+ hideLogsFromAgent,
254
+ model,
255
+ additionalModelOptions
256
+ );
175
257
  },
176
- runCode: async function(generatedCode:string) {
258
+ checkCode: async function (generatedCode:string): Promise<CodeRunResult> {
177
259
  if(!QuickJS) {
178
260
  // Create QuickJS
179
261
  if(debug)
@@ -181,19 +263,94 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
181
263
  QuickJS = await getQuickJS();
182
264
  }
183
265
 
184
- let apiGlobals = {}
266
+ let apiGlobals: { [key: string]: { value: any } } = {}
185
267
 
186
- for(const key in options.apiGlobals) {
187
- apiGlobals[key] = {
188
- value: options.apiGlobals[key],
268
+ if(options.apiGlobals) {
269
+ for(const key in options.apiGlobals) {
270
+ apiGlobals[key] = {
271
+ value: (options.apiGlobals as Record<string, any>)[key],
272
+ }
189
273
  }
190
274
  }
191
-
275
+
276
+ // Create sandbox
277
+ const {vm, checkCode} = await createSandbox(QuickJS, apiExports ? {
278
+ [apiDefFilePath]: apiExports,
279
+ } : undefined, {
280
+ ...apiGlobals,
281
+ [apiGlobalName]: {
282
+ value: apiObject,
283
+ whitelist: apiWhitelist
284
+ }
285
+ }, options.debug);
286
+
287
+ try {
288
+ if(debug) {
289
+ console.log("checking Code:\n", generatedCode);
290
+ }
291
+
292
+ const res = await checkCode(wrapCode(apiGlobalName, apiDefFilePath, generatedCode));
293
+
294
+ if(!res) {
295
+ console.error("Unknown failure checking code");
296
+ return {
297
+ success: false,
298
+ logs: [],
299
+ };
300
+ }
301
+
302
+
303
+ if(debug) {
304
+ console.log(res.success ? "Code check passed" : "Code check failed");
305
+ }
306
+
307
+ if(res.success) {
308
+ return {
309
+ success: true,
310
+ logs: [{ type: "log", args: ["✔️ Code check passed"], source: "compiler" }, ...res.logs],
311
+ }
312
+ }
313
+
314
+ return {
315
+ success: false,
316
+ logs: [{ type: "error", args: ["❌ Code check failed"], source: "compiler" }, ...res.logs],
317
+ };
318
+ } catch(err) {
319
+ if(debug) {
320
+ console.error("Error checking code:", err);
321
+ }
322
+ }
323
+ vm.dispose();
324
+
325
+ return {
326
+ success: false,
327
+ logs: [],
328
+ };
329
+ },
330
+ runCode: async function (code:string): Promise<CodeRunResult> {
331
+ // TODO: fix code duplication with checkCode, maybe combine into a single function that takes a parameter for whether to actually run the code or just check it?
332
+ if(!QuickJS) {
333
+ // Create QuickJS
334
+ if(debug)
335
+ console.log("Creating quick.js");
336
+ QuickJS = await getQuickJS();
337
+ }
338
+
339
+ let apiGlobals: { [key: string]: { value: any } } = {}
340
+
341
+ if(options.apiGlobals) {
342
+ for(const key in options.apiGlobals) {
343
+ apiGlobals[key] = {
344
+ value: (options.apiGlobals as Record<string, any>)[key],
345
+ }
346
+ }
347
+ }
348
+
192
349
  // Create sandbox
193
350
  // NOTE: not sure why I have to replace the backslash here
194
- const {vm, runTask, isAsyncProcessRunning} = await createSandbox(QuickJS, {
195
- [apiDefFilePath.replaceAll("\\", "")]: apiExports,
196
- }, {
351
+ const {vm, runCode: runTask, isAsyncProcessRunning} = await createSandbox(QuickJS, apiExports ? {
352
+ [apiDefFilePath]: apiExports,
353
+ } : undefined, {
197
354
  ...apiGlobals,
198
355
  [apiGlobalName]: {
199
356
  value: apiObject,
@@ -203,13 +360,17 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
203
360
 
204
361
  try {
205
362
  if(debug) {
206
- console.log("Running Code:\n", generatedCode);
363
+ console.log("Running Code:\n", code);
207
364
  }
208
365
 
209
- const res = await runTask(generatedCode.replace("'api.ts'", `'${options.apiDefFilePath}'`));
366
+ const res = await runTask(wrapCode(apiGlobalName, apiDefFilePath, code));
210
367
 
211
368
  if(!res) {
212
- console.error("Task unsuccessful");
369
+ console.error("Unknown failure running task");
370
+ return {
371
+ success: false,
372
+ logs: [],
373
+ };
213
374
  }
214
375
 
215
376
  while(isAsyncProcessRunning()) {
@@ -219,6 +380,8 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
219
380
  if(debug) {
220
381
  console.log("Completed");
221
382
  }
383
+
384
+ return res;
222
385
  } catch(err) {
223
386
  if(debug) {
224
387
  console.error("Error running task:", err);
@@ -228,10 +391,15 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
228
391
  await new Promise((resolve) => setTimeout(resolve, 100));
229
392
 
230
393
  rejectOpenPromises(vm);
231
- vm.dispose()
232
-
394
+ vm.dispose();
395
+
396
+ return {
397
+ success: false,
398
+ logs: [],
399
+ };
233
400
  },
234
- processRequest: async function (userQuery, currentContext:any = undefined):Promise<GenerateCodeResult> {
401
+ // generates code and runs it in one step
402
+ processRequest: async function (userQuery: string, currentContext:any = undefined):Promise<GenerateCodeResult | null> {
235
403
  // Create prompt
236
404
 
237
405
  if(debug) {
@@ -240,24 +408,38 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
240
408
 
241
409
  // Generate Code
242
410
  console.time("Generate Task")
243
- const generatedCode:GenerateCodeResult = await generateCode(userQuery, [], createTaskPrompt.replace("{{CONTEXT}}", currentContext ? "```" + JSON.stringify(currentContext, null, 2) + "```" : "" ));
411
+ const generatedCode:GenerateCodeResult | null = await generateCode(instance,
412
+ userQuery,
413
+ [],
414
+ createTaskPrompt.replace("{{CONTEXT}}", currentContext ? JSON.stringify(currentContext, null, 2) : "" ),
415
+ debug,
416
+ hideLogsFromAgent,
417
+ model,
418
+ additionalModelOptions
419
+ );
244
420
  console.timeEnd("Generate Task")
245
421
 
246
422
  console.time("Run Code")
247
- await this.runCode(generatedCode.code.replace("'api.ts'", `'${options.apiDefFilePath}'`));
423
+ if(generatedCode?.code) {
424
+ await instance.runCode(generatedCode.code);
425
+ } else {
426
+ console.error("No code generated", generatedCode);
427
+ }
248
428
  console.timeEnd("Run Code")
249
429
 
250
430
  return generatedCode;
251
431
  }
252
432
  }
253
433
 
434
+ return instance;
435
+
254
436
  }
255
437
 
256
438
  export interface AIMyAPIModuleExports {
257
439
  createWithAPI: (options:AIMyAPIOptions) => Promise<AIMyAPIInstance>;
258
440
 
259
441
  createBasePrompt: (apiFilePath:string, documentationPath: string) => string;
260
- generateCode: (queryText:string, userChatHistory:ChatCompletionMessageParam[], createTaskPrompt:string, debug:boolean) => Promise<GenerateCodeResult>;
442
+ generateCode: (instance: AIMyAPIInstance, queryText:string, userChatHistory:ChatCompletionMessageParam[], createTaskPrompt:string, debug:boolean) => Promise<GenerateCodeResult | null>;
261
443
  createSandbox: (QuickJS:QuickJSWASMModule, globals: any) => Promise<any>;
262
444
  }
263
445
 
@@ -270,3 +452,6 @@ export const AIMyAPI:AIMyAPIModuleExports = {
270
452
  generateCode,
271
453
  createSandbox,
272
454
  }
455
+
456
+ export * from "./sandbox";
457
+