aimyapi 0.2.0 → 1.0.2

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,159 +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 openai = new OpenAI();
16
17
 
17
- // function formatObject(obj: any): string {
18
- // return Object.keys(obj).reduce((acc, key) => {
19
- // return `${acc}${key}:\n${obj[key]}\n\n`;
20
- // }, "");
21
- // }
18
+ let QuickJS:QuickJSWASMModule | null = null;
22
19
 
23
- export function createBasePrompt(apiFilePath:string, documentationPath: string): string {
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) {
54
+ console.error(err);
55
+ }
56
+ })();
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));
67
+ }
68
+
69
+ export function createBasePrompt(apiFilePath:string, apiGlobalName: string, documentationPath?: string, ): string {
24
70
  const apiText: string = fs.readFileSync(apiFilePath, 'utf8');
25
71
  const documentationText: string = documentationPath ? fs.readFileSync(documentationPath, 'utf8') : '';
26
72
 
27
73
  //console.time("Loading templates");
28
- const createTaskPrompt: string = fs.readFileSync( path.join(__dirname, '../prompts/create-task-prompt.md'), 'utf8').replace("{{DOCUMENTATION}}", 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 || "");
29
75
  //console.timeEnd("Loading templates")
30
76
 
31
77
  return createTaskPrompt;
32
78
  }
33
79
 
34
- export interface GenerateCodeResult {
35
- code: string;
36
- loggableCode: string;
37
- rawResponse?: string;
38
- }
39
80
 
40
- // FIXME: add an options parameter and allow specifying the token limit
41
- export const generateCode = async function(queryText:string, userChatHistory:ChatCompletionMessageParam[], createTaskPrompt:string, debug:boolean = false, model="gpt-4o-mini"): Promise<GenerateCodeResult> {
42
- if(!queryText)
43
- 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 = "";
44
95
 
45
- const prompt = createTaskPrompt.replace("{{QUERY_TEXT}}", queryText);
46
- const messages:ChatCompletionMessageParam[] = [
96
+ if(debug) {
97
+ console.log("Generating code for query:", queryText);
98
+ }
99
+ // checkcode
100
+ const codingTools: AutoParseableTool<any, true>[] = [
47
101
  {
48
- role: "system",
49
- 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
+ }
50
122
  },
51
- ...userChatHistory,
52
123
  {
53
- role: "user",
54
- content: `${queryText}. Respond with json`,
55
- 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
+ }
56
157
  }
57
158
  ];
58
159
 
59
160
  try {
60
- const results = await openai.chat.completions.create({
61
- model,
62
- messages: messages,
63
- temperature: 0.0,
64
- max_tokens: 2000,
65
- response_format: {
66
- json_schema: {
67
- name: "codeResponse",
68
- description: "The code generated by the AI",
69
- schema: {
70
- type: 'object',
71
- properties: {
72
- yourCode: {
73
- type: 'string',
74
- description: 'The code generated by the AI',
75
- }
76
- },
77
- },
78
- },
79
- type: 'json_schema',
80
- }
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));
81
190
  }
82
- );
191
+
83
192
 
84
- if(debug)
85
- console.log(results.usage);
86
-
87
- 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
+ }
88
210
 
89
- if(debug)
90
- console.log(response);
91
-
92
- try {
93
- const json = JSON.parse(response);
94
- const code = json.yourCode.replaceAll('```', '');
95
-
96
- return { code, loggableCode: code };
97
- } catch(err) {
98
- console.error("Error parsing response", err);
99
- return { code: '', loggableCode: '' };
100
- }
101
211
  } catch(err) {
102
212
  console.error(err);
103
- return { code: '', loggableCode: '' };
104
- }
105
- };
106
-
107
- let QuickJS:QuickJSWASMModule = null;
108
-
109
- export interface AIMyAPIOptions {
110
- apiObject: object;
111
- apiExports: object;
112
- apiWhitelist?: string[]; // List of functions to expose to the user. Necessary to pass this yourself if you are extending your api from another class.
113
- apiGlobals?: object;
114
- apiDefFilePath: string; // Full path to the api definition file so that it can be loaded into the prompt
115
- apiGlobalName?: string;
116
- apiDocsPath?: string;
117
- debug?: boolean;
118
- model?: string;
213
+ return null;
214
+ }
119
215
  };
120
216
 
121
- export interface AIMyAPIInstance {
122
- // see xample 2
123
- options:AIMyAPIOptions;
124
- generateCode: (queryText:string, userChatHistory:ChatCompletionMessageParam[]) => Promise<GenerateCodeResult>;
125
- runCode: (task:string) => Promise<void>;
126
- // run a single request with no history
127
- processRequest: (userQuery:string, context?: object) => Promise<GenerateCodeResult>;
128
-
129
- }
130
-
131
217
  async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
132
218
  // extend options with defaults
133
- options = {
134
- apiGlobalName: "api",
135
- apiGlobals: {},
136
- apiWhitelist: Object.getOwnPropertyNames(Object.getPrototypeOf(options.apiObject)).filter((f) => f !== "constructor" && !f.startsWith("_")),
137
- debug: false,
138
- 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
+ },
139
229
  ...options
140
- } as AIMyAPIOptions;
230
+ };
141
231
 
142
- if(!options.apiObject || !options.apiDefFilePath) {
143
- throw new Error("apiObject and apiFilePath are required");
232
+ if(!localOptions.apiObject || !localOptions.apiDefFilePath) {
233
+ throw new Error("apiObject and apiDefFilePath are required");
144
234
  }
145
235
 
146
- const {apiObject, apiWhitelist, apiGlobalName, apiExports, apiDefFilePath, apiDocsPath, debug, model} = options;
236
+ const {apiObject, apiWhitelist, apiGlobalName, apiExports, apiDefFilePath, apiDocsPath, debug, model, additionalModelOptions, hideLogsFromAgent} = localOptions;
147
237
 
148
- const createTaskPrompt = createBasePrompt( apiDefFilePath, apiDocsPath);
238
+ const createTaskPrompt = createBasePrompt( apiDefFilePath, apiGlobalName, apiDocsPath);
149
239
 
150
- return {
151
- options,
152
- 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) {
153
243
  if(!queryText)
154
- return { code: '', loggableCode: '' };
155
-
156
- 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
+ );
157
257
  },
158
- runCode: async function(generatedCode:string) {
258
+ checkCode: async function (generatedCode:string): Promise<CodeRunResult> {
159
259
  if(!QuickJS) {
160
260
  // Create QuickJS
161
261
  if(debug)
@@ -163,18 +263,93 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
163
263
  QuickJS = await getQuickJS();
164
264
  }
165
265
 
166
- let apiGlobals = {}
266
+ let apiGlobals: { [key: string]: { value: any } } = {}
167
267
 
168
- for(const key in options.apiGlobals) {
169
- apiGlobals[key] = {
170
- 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
+ }
171
273
  }
172
274
  }
173
-
275
+
276
+ // Create sandbox
277
+ const {vm, checkCode} = await createSandbox(QuickJS, {
278
+ [apiDefFilePath]: apiExports,
279
+ }, {
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
+
174
349
  // Create sandbox
175
350
  // NOTE: not sure why I have to replace the backslash here
176
- const {vm, runTask, isAsyncProcessRunning} = await createSandbox(QuickJS, {
177
- [apiDefFilePath.replaceAll("\\", "")]: apiExports,
351
+ const {vm, runCode: runTask, isAsyncProcessRunning} = await createSandbox(QuickJS, {
352
+ [apiDefFilePath]: apiExports,
178
353
  }, {
179
354
  ...apiGlobals,
180
355
  [apiGlobalName]: {
@@ -185,13 +360,17 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
185
360
 
186
361
  try {
187
362
  if(debug) {
188
- console.log("Running Code:\n", generatedCode);
363
+ console.log("Running Code:\n", code);
189
364
  }
190
365
 
191
- const res = await runTask(generatedCode.replace("'api.ts'", `'${options.apiDefFilePath}'`));
366
+ const res = await runTask(wrapCode(apiGlobalName, apiDefFilePath, code));
192
367
 
193
368
  if(!res) {
194
- console.error("Task unsuccessful");
369
+ console.error("Unknown failure running task");
370
+ return {
371
+ success: false,
372
+ logs: [],
373
+ };
195
374
  }
196
375
 
197
376
  while(isAsyncProcessRunning()) {
@@ -201,6 +380,8 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
201
380
  if(debug) {
202
381
  console.log("Completed");
203
382
  }
383
+
384
+ return res;
204
385
  } catch(err) {
205
386
  if(debug) {
206
387
  console.error("Error running task:", err);
@@ -210,10 +391,15 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
210
391
  await new Promise((resolve) => setTimeout(resolve, 100));
211
392
 
212
393
  rejectOpenPromises(vm);
213
- vm.dispose()
214
-
394
+ vm.dispose();
395
+
396
+ return {
397
+ success: false,
398
+ logs: [],
399
+ };
215
400
  },
216
- 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> {
217
403
  // Create prompt
218
404
 
219
405
  if(debug) {
@@ -222,24 +408,38 @@ async function createWithAPI(options:AIMyAPIOptions): Promise<AIMyAPIInstance> {
222
408
 
223
409
  // Generate Code
224
410
  console.time("Generate Task")
225
- 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
+ );
226
420
  console.timeEnd("Generate Task")
227
421
 
228
422
  console.time("Run Code")
229
- 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
+ }
230
428
  console.timeEnd("Run Code")
231
429
 
232
430
  return generatedCode;
233
431
  }
234
432
  }
235
433
 
434
+ return instance;
435
+
236
436
  }
237
437
 
238
438
  export interface AIMyAPIModuleExports {
239
439
  createWithAPI: (options:AIMyAPIOptions) => Promise<AIMyAPIInstance>;
240
440
 
241
441
  createBasePrompt: (apiFilePath:string, documentationPath: string) => string;
242
- 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>;
243
443
  createSandbox: (QuickJS:QuickJSWASMModule, globals: any) => Promise<any>;
244
444
  }
245
445
 
@@ -252,3 +452,6 @@ export const AIMyAPI:AIMyAPIModuleExports = {
252
452
  generateCode,
253
453
  createSandbox,
254
454
  }
455
+
456
+ export * from "./sandbox";
457
+