├── index.ts ├── tests ├── basketball.ts ├── magic.ts ├── magic.test.ts └── basketball.test.ts ├── tsconfig.json ├── validateResponse.ts ├── jest.config.js ├── package.json ├── jsonSchemaGenerator.ts ├── LICENSE ├── extract.ts ├── utils.ts ├── example.ts ├── fetchCompletion.ts ├── .npmignore ├── .gitignore ├── README.md └── transformer.ts /index.ts: -------------------------------------------------------------------------------- 1 | import { fetchCompletion } from './fetchCompletion'; 2 | import transformer from './transformer'; 3 | 4 | export { fetchCompletion } 5 | export default transformer; 6 | -------------------------------------------------------------------------------- /tests/basketball.ts: -------------------------------------------------------------------------------- 1 | export type BasketballPlayer = { 2 | name: string; 3 | position: 'Guard' | 'Forward' | 'Center'; 4 | height: number; 5 | weight: number; 6 | team: string; 7 | }; 8 | 9 | export type BasketballPlayersArray = BasketballPlayer[]; 10 | 11 | // @ts-ignore 12 | // @magic 13 | export async function getTop5BasketBallPlayers(): Promise { 14 | //Return the top 5 baskeball players of all time 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "moduleResolution": "node", 7 | "target": "es2017", 8 | "outDir": "./dist", 9 | "plugins": [ 10 | { 11 | "transform": "./transformer.ts" 12 | } 13 | ] 14 | }, 15 | "include": [ 16 | "./**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /validateResponse.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | import ajvFormats from 'ajv-formats'; 3 | 4 | 5 | export function validateAPIResponse(apiResponse: any, schema: object): boolean { 6 | const ajvInstance = new Ajv(); 7 | ajvFormats(ajvInstance); 8 | const validate = ajvInstance.compile(schema); 9 | const isValid = validate(apiResponse); 10 | 11 | if (!isValid) { 12 | console.log("Validation errors:", validate.errors); 13 | } 14 | 15 | return isValid; 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | /* 3 | */ 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | moduleNameMapper: { 8 | "^@jumploops/magic$": "/usr/local/lib/node_modules/@jumploops/magic" 9 | }, 10 | testTimeout: 15000, 11 | testPathIgnorePatterns: [ 12 | '/node_modules/', // ignore tests in the "node_modules" folder 13 | '/dist/', // ignore tests in the specified folder 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.6", 3 | "name": "@jumploops/magic", 4 | "dependencies": { 5 | "ajv": "^8.12.0", 6 | "ajv-formats": "^2.1.1", 7 | "openai": "^3.2.1", 8 | "ts-patch": "2.1.0", 9 | "ttypescript": "^1.5.15", 10 | "typescript": "4.8.2", 11 | "typescript-json-schema": "0.55.0" 12 | }, 13 | "devDependencies": { 14 | "@types/ajv": "^1.0.0", 15 | "@types/jest": "^29.5.0", 16 | "ts-jest": "^29.1.0", 17 | "ts-node": "10.9.1" 18 | }, 19 | "main": "dist/index.js", 20 | "types": "dist/index.d.ts", 21 | "scripts": { 22 | "build": "tsc", 23 | "clean": "rm -rf dist", 24 | "prepare": "ts-patch install -s && npm run build", 25 | "test": "jest" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /jsonSchemaGenerator.ts: -------------------------------------------------------------------------------- 1 | // jsonSchemaGenerator.ts 2 | import * as ts from 'typescript'; 3 | import * as tsjson from 'typescript-json-schema'; 4 | 5 | export function generateSchemaFromType(typeChecker: ts.TypeChecker, type: ts.Type, program: ts.Program): object | null { 6 | const symbol = type.aliasSymbol || type.getSymbol(); 7 | 8 | if (!symbol) return null; 9 | 10 | const compilerOptions = program.getCompilerOptions(); 11 | 12 | const settings = tsjson.getDefaultArgs(); 13 | settings.required = true; 14 | const generator = tsjson.buildGenerator(program, settings); 15 | 16 | if (!generator) return null; 17 | 18 | // Generate the JSON schema for the extracted type 19 | const schema = generator.getSchemaForSymbol(symbol.getName()); 20 | return schema; 21 | } 22 | -------------------------------------------------------------------------------- /tests/magic.ts: -------------------------------------------------------------------------------- 1 | interface Person { 2 | firstName: string; 3 | lastName: string; 4 | } 5 | 6 | export async function nonMagicAsyncFunction(name: string): Promise { 7 | let person = { firstName: "Jane", lastName: "User" }; 8 | 9 | let [firstName, lastName] = name.split(' '); 10 | 11 | //Replace user name 12 | person = { 13 | firstName, 14 | lastName, 15 | } 16 | return person; 17 | } 18 | 19 | // @ts-ignore 20 | // @magic 21 | export async function magicAsyncFunction(name: string): Promise { 22 | //Return the first name of the 5th president and the last name of the 40th president 23 | let person = { firstName: "Jane", lastName: "User" }; 24 | 25 | let [firstName, lastName] = name.split(' '); 26 | 27 | //Replace user name 28 | person = { 29 | firstName, 30 | lastName, 31 | } 32 | return person; 33 | } 34 | -------------------------------------------------------------------------------- /tests/magic.test.ts: -------------------------------------------------------------------------------- 1 | import { nonMagicAsyncFunction, magicAsyncFunction } from './magic'; 2 | 3 | describe('Person Functions', () => { 4 | describe('nonMagicAsyncFunction', () => { 5 | it('should return a Person with firstName and lastName from the given name string', async () => { 6 | const name = "John Doe"; 7 | const expectedPerson = { firstName: "John", lastName: "Doe" }; 8 | 9 | const result = await nonMagicAsyncFunction(name); 10 | 11 | expect(result).toEqual(expectedPerson); 12 | }); 13 | }); 14 | 15 | describe('magicAsyncFunction', () => { 16 | it('should return a Person with the first name of the 5th president and the last name of the 40th president regardless of input name', async () => { 17 | const name = "Name Input"; 18 | const expectedPerson = { firstName: "James", lastName: "Reagan" }; 19 | 20 | const result = await magicAsyncFunction(name); 21 | 22 | expect(result).toEqual(expectedPerson); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 jumploops 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extract.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import { generateSchemaFromType } from './jsonSchemaGenerator'; 4 | 5 | export function extractFunctionArgumentsAndReturn(node: ts.FunctionDeclaration, typeChecker: ts.TypeChecker, program: ts.Program): void { 6 | if (!node.name || !node.type) return; 7 | 8 | //console.log(`Function: ${node.name.getText()}`); 9 | 10 | // Extracting argument types 11 | node.parameters.forEach((parameter) => { 12 | // console.log(`Parameter: ${parameter.name.getText()} - Type: ${parameter.type?.getText()}`); 13 | }); 14 | 15 | // Extracting return type 16 | const returnTypeText = node.type.getText(); 17 | // console.log(`Return type: ${returnTypeText}`); 18 | 19 | // Check if the return type is a Promise 20 | const returnType = typeChecker.getTypeFromTypeNode(node.type); 21 | const typeArgs = (returnType as ts.TypeReference).typeArguments; 22 | 23 | let schema; 24 | 25 | if (typeArgs && returnTypeText.startsWith('Promise')) { 26 | const nestedType = typeArgs[0]; 27 | // console.log(`Nested type: ${nestedType.toString()}`); 28 | 29 | // Generate JSON schema for nested type (Person) 30 | const nestedSchema = generateSchemaFromType(typeChecker, nestedType, program); 31 | // console.log('JSON Schema:', JSON.stringify(nestedSchema, null, 2)); 32 | schema = nestedSchema; 33 | } else { 34 | // Generate JSON schema for type (Person) 35 | schema = generateSchemaFromType(typeChecker, returnType, program); 36 | // console.log('JSON Schema:', JSON.stringify(schema, null, 2)); 37 | } 38 | return schema; 39 | } 40 | -------------------------------------------------------------------------------- /tests/basketball.test.ts: -------------------------------------------------------------------------------- 1 | import { getTop5BasketBallPlayers, BasketballPlayer, BasketballPlayersArray } from './basketball'; 2 | 3 | describe('getTop5BasketBallPlayers', () => { 4 | let players: BasketballPlayersArray; 5 | 6 | beforeAll(async () => { 7 | players = await getTop5BasketBallPlayers(); 8 | }); 9 | 10 | test('Function returns an array of 5 players', () => { 11 | expect(players.length).toBe(5); 12 | }); 13 | 14 | test('Function returns an array of BasketballPlayer object type', () => { 15 | players.forEach((player: BasketballPlayer) => { 16 | expect(typeof player.name).toBe('string'); 17 | expect(['Guard', 'Forward', 'Center']).toContain(player.position); 18 | expect(typeof player.height).toBe('number'); 19 | expect(typeof player.weight).toBe('number'); 20 | expect(typeof player.team).toBe('string'); 21 | }); 22 | }); 23 | 24 | test('Function returns top 5 basketball players', () => { 25 | // Write your own assertions to check if the returned players are indeed the top 5 26 | // For instance (using your own list of top 5 players): 27 | const top5PlayerNames = ['Michael Jordan', 'LeBron James', 'Kareem Abdul-Jabbar', 'Bill Russell', 'Magic Johnson']; 28 | players.forEach((player: BasketballPlayer) => { 29 | expect(top5PlayerNames).toContain(player.name); 30 | }); 31 | }); 32 | 33 | test('Function handles errors', async () => { 34 | // Simulate or mock an error and test if the function handles it gracefully 35 | // For instance, you can mock the API call or data source and reject with an error 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | function getLeadingCommentsTrivia(node: ts.Node, sourceFile: ts.SourceFile): readonly ts.CommentRange[] { 4 | // Get the position of the node (including comments) 5 | const nodePos = node.pos; 6 | 7 | // Retrieve the trivia 8 | return ts.getLeadingCommentRanges(sourceFile.text, nodePos) || []; 9 | } 10 | 11 | function containsMagic(triviaText: string): boolean { 12 | const magicPattern = /@magic/; 13 | return triviaText.match(magicPattern) !== null; 14 | } 15 | 16 | function getAllCommentRanges(sourceFile: ts.SourceFile): readonly ts.CommentRange[] { 17 | const commentRanges: ts.CommentRange[] = []; 18 | const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, undefined, sourceFile.text); 19 | 20 | let token: ts.SyntaxKind; 21 | do { 22 | token = scanner.scan(); 23 | if (token === ts.SyntaxKind.MultiLineCommentTrivia || token === ts.SyntaxKind.SingleLineCommentTrivia) { 24 | const range = { 25 | kind: token, 26 | pos: scanner.getTokenPos(), 27 | end: scanner.getTextPos(), 28 | }; 29 | commentRanges.push(range); 30 | } 31 | } while (token !== ts.SyntaxKind.EndOfFileToken); 32 | 33 | return commentRanges; 34 | } 35 | 36 | export function isFileMagic(sourceFile: ts.SourceFile): boolean { 37 | // Get all comment ranges in the file 38 | const commentRanges = getAllCommentRanges(sourceFile); 39 | 40 | // Check if any comment contains @magic 41 | return commentRanges.some((commentRange) => { 42 | const commentText = sourceFile.text.slice(commentRange.pos, commentRange.end); 43 | return containsMagic(commentText); 44 | }); 45 | } 46 | 47 | export function isFunctionMagic(node: ts.Node, sourceFile: ts.SourceFile): boolean { 48 | // Get leading comments trivia for the node 49 | const leadingCommentsTrivia = getLeadingCommentsTrivia(node, sourceFile); 50 | 51 | return leadingCommentsTrivia.some((commentRange) => { 52 | // Get comment text for each comment range 53 | const commentText = sourceFile.text.slice(commentRange.pos, commentRange.end); 54 | 55 | // Check if it contains @magic 56 | return containsMagic(commentText); 57 | }); 58 | } 59 | 60 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | import { getTop5BasketBallPlayers } from './tests/basketball'; 2 | 3 | type Car = { 4 | make: string, 5 | model: string 6 | } 7 | 8 | export type CarArray = Car[]; 9 | 10 | // @ts-ignore 11 | // @magic 12 | export async function getCars(): Promise { 13 | // Get the top 5 cars from the 1970s 14 | } 15 | 16 | interface Person { 17 | firstName: string; 18 | lastName: string; 19 | } 20 | 21 | // @ts-ignore 22 | // @magic 23 | export async function asyncFunction(): Promise { 24 | //Return the first name of the 5th president and the last name of the 40th president 25 | } 26 | 27 | interface Building { 28 | name: string; 29 | height: string; 30 | } 31 | 32 | // @magic 33 | export async function asyncFunction2(): Promise { 34 | //How tall is the Eiffel tower? 35 | return { name: "Eiffel Tower", height: "?" } 36 | } 37 | 38 | interface Structure { 39 | name: string; 40 | heightInMeters: string; 41 | heightInFeet: string; 42 | } 43 | 44 | // @ts-ignore 45 | // @magic 46 | export async function asyncFunction3(): Promise { 47 | //How tall is a school bus? 48 | } 49 | 50 | interface Height { 51 | meters: number; 52 | feet: number; 53 | } 54 | 55 | interface Mountain { 56 | name: string; 57 | height: Height; 58 | } 59 | 60 | // @ts-ignore 61 | // @magic 62 | export async function asyncFunction4(): Promise { 63 | //Return the 3rd highest mountain 64 | } 65 | 66 | interface Peak { 67 | name: string; 68 | height: string; 69 | } 70 | 71 | // @ts-ignore 72 | // @magic 73 | export async function asyncFunction5(): Promise { 74 | //Return the 3rd highest peak 75 | } 76 | 77 | //getTop5BasketBallPlayers().then((res) => console.log(res)).catch((err) => console.error(err)); 78 | //getCars().then((result) => console.log(result)); 79 | 80 | //asyncFunction().then((value) => console.log(value)).catch((err) => console.error(err)); 81 | //asyncFunction2().then((value) => console.log(value)).catch((err) => console.error(err)); 82 | //asyncFunction3().then((value) => console.log(value)).catch((err) => console.error(err)); 83 | //asyncFunction4().then((value) => console.log(value)).catch((err) => console.error(err)); 84 | asyncFunction5().then((value) => console.log(value)).catch((err) => console.error(err)); 85 | -------------------------------------------------------------------------------- /fetchCompletion.ts: -------------------------------------------------------------------------------- 1 | // fetchCompletion.ts 2 | import { Configuration, OpenAIApi } from 'openai'; 3 | 4 | import { validateAPIResponse } from './validateResponse'; 5 | 6 | const configuration = new Configuration({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | 10 | const openai = new OpenAIApi(configuration); 11 | 12 | export async function fetchCompletion(existingFunction: string, { schema }: { schema: any }) { 13 | let completion; 14 | 15 | // TODO improve Prompt cleanup 16 | const prompt = ` 17 | You are a robotic assistant. Your only language is code. You only respond with valid JSON. Nothing but JSON. 18 | 19 | Your goal is to read a prompt and create a response, in JSON, that matches to a JSON schema. 20 | 21 | If the JSON Schema type is "array", only return the array, not the array nested in an object wrapper, even if the array itself contains objects or otherwise. 22 | 23 | For example, if you're planning to return: 24 | { "list": [ { "name": "Alice" }, { "name": "Bob" }, { "name": "Carol"}] } 25 | Instead just return: 26 | [ { "name": "Alice" }, { "name": "Bob" }, { "name": "Carol"}] 27 | 28 | If the JSON schema is type "object", just return valid JSON. Don't include trailing commas, as they are invalid JSON. 29 | 30 | For example, if you're planning to return: 31 | { 32 | "abc": "123", 33 | "def": "456", 34 | "ghi": "789", 35 | } 36 | Instead return: 37 | { 38 | "abc": "123", 39 | "def": "456", 40 | "ghi": "789" 41 | } 42 | 43 | Prompt: ${existingFunction.replace('{', '').replace('}', '').replace('//', '').replace('\n', '')} 44 | 45 | JSON Schema: 46 | \`\`\` 47 | ${JSON.stringify(JSON.parse(schema), null, 2)} 48 | \`\`\` 49 | `; 50 | 51 | process.env.DEBUG && console.log(prompt); 52 | 53 | try { 54 | completion = await openai.createChatCompletion({ 55 | model: process.env.OPENAI_MODEL ? process.env.OPENAI_MODEL : 'gpt-3.5-turbo', 56 | messages: [{ role: 'user', content: prompt }], 57 | }); 58 | } catch (err) { 59 | console.error(err); 60 | return; 61 | } 62 | 63 | process.env.DEBUG && console.log(completion.data.choices[0].message) 64 | const response = JSON.parse(completion.data.choices[0].message.content); 65 | 66 | if (!validateAPIResponse(response, JSON.parse(schema))) { 67 | process.env.DEBUG && console.log(response); 68 | throw new Error("Invalid JSON response from LLM"); 69 | } 70 | 71 | return JSON.parse(completion.data.choices[0].message.content); 72 | } 73 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪄 2 | 3 | ## AI functions for Typescript 4 | 5 | Magically run typesafe functions by utilizing large language models (LLMs) as a runtime. 6 | 7 | ## tl;dr 8 | 9 | 10 | 11 | > **Warning** 12 | > 13 | > The code in this repository (including this README) was created with GPT-4. This is not a production repository and caution should be used when running this code. 14 | 15 | ### How does it work? 16 | 17 | This library uses a [Typescript transformer](https://github.com/itsdouges/typescript-transformer-handbook) to take the return type of a function, convert that type [into a JSON Schema](https://github.com/YousefED/typescript-json-schema), and then replace the function body with code to query the [OpenAI API](https://github.com/openai/openai-node) and [validate the response](https://github.com/ajv-validator/ajv) against the JSON Schema. 18 | 19 | _This library doesn't write code for your functions, it allows you to use LLMs as a runtime._ 20 | 21 | 22 |

Usage

23 | 24 | Transform your TypeScript functions by adding the `//@magic` comment. 25 | 26 | Here's an example function: 27 | 28 | ```typescript 29 | // @magic 30 | async function example(): Promise { 31 | //Return the 3rd highest mountain 32 | } 33 | ``` 34 | 35 | When this function is called, it'll leverage an AI language model like `GPT-4` and return the following result: 36 | 37 | ```typescript 38 | { name: 'Kangchenjunga', height: { meters: 8586, feet: 28169 } } 39 | ``` 40 | 41 | > **Note**: In this example, `Mountain` is defined as: 42 | > 43 | > ```typescript 44 | > interface Height { 45 | > meters: number; 46 | > feet: number; 47 | > } 48 | > 49 | > interface Mountain { 50 | > name: string; 51 | > height: Height; 52 | > } 53 | > ``` 54 | 55 |
56 | 57 | ### Prerequisites 58 | 59 | ```bash 60 | export OPENAI_API_KEY=your_api_key_here 61 | export OPENAI_MODEL=gpt-4 #optional 62 | ``` 63 | 64 | ### Installation 65 | 66 | ```bash 67 | npm install --save @jumploops/magic 68 | ``` 69 | 70 |

Setup with `ts-patch`

71 | 72 | ```bash 73 | npm install -D ts-patch 74 | ts-patch install 75 | ``` 76 | 77 | Now, add the plugin to your `tsconfig.json`: 78 | ```json 79 | { 80 | "compilerOptions": { 81 | "plugins": [ 82 | { "transform": "@jumploops/magic" } 83 | ] 84 | } 85 | } 86 | ``` 87 |
88 | 89 |

Setup with `ttypescript`

90 | 91 | Install ttypescript: 92 | 93 | ```bash 94 | npm i -D ttypescript 95 | ``` 96 | 97 | Next, add the plugin to your `tsconfig.json`: 98 | ```json 99 | { 100 | "compilerOptions": { 101 | "plugins": [ 102 | { "transform": "@jumploops/magic" } 103 | ] 104 | } 105 | } 106 | ``` 107 | 108 | Compile your project using ttypescript: 109 | 110 | ```bash 111 | ttsc 112 | ``` 113 | 114 |
115 | 116 | ## Prior Art 117 | This package took inspiration from [Marvin - build AI functions that use an LLM as a runtime](https://news.ycombinator.com/item?id=35366838) 118 | 119 | ## Contributing 120 | 121 | We welcome contributions! Please open an issue or submit a pull request if you'd like to help improve @jumploops/magic. 122 | 123 | ## License 124 | 125 | MIT License. 126 | -------------------------------------------------------------------------------- /transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import { isFileMagic, isFunctionMagic } from './utils'; 4 | import { extractFunctionArgumentsAndReturn } from './extract'; 5 | 6 | export default function(program: ts.Program, pluginOptions: any) { 7 | const typeChecker = program.getTypeChecker(); 8 | 9 | return (ctx: ts.TransformationContext) => { 10 | 11 | // Require statement for function that wraps external LLM call 12 | // TODO Must be importable by any library consumers 13 | const requireStatement = ts.factory.createVariableStatement( 14 | undefined, 15 | ctx.factory.createVariableDeclarationList( 16 | [ 17 | ctx.factory.createVariableDeclaration( 18 | "{ fetchCompletion }", 19 | undefined, 20 | undefined, 21 | ctx.factory.createCallExpression( 22 | ctx.factory.createIdentifier('require'), 23 | undefined, 24 | [ctx.factory.createStringLiteral('@jumploops/magic')] 25 | ) 26 | ), 27 | ], 28 | ts.NodeFlags.Const 29 | ) 30 | ); 31 | 32 | return (sourceFile: ts.SourceFile) => { 33 | 34 | // Function to ensure the LLM fetch function can be found in any files that contain magic functions 35 | function insertRequireDeclarationIfNeeded(sourceFile: ts.SourceFile) { 36 | const hasFetchCompletionDeclaration = sourceFile.statements.some((stmt) => 37 | ts.isVariableStatement(stmt) && stmt.declarationList.declarations.some( 38 | (decl) => 39 | ts.isVariableDeclaration(decl) && 40 | ts.isIdentifier(decl.name) && 41 | decl.name.text === "fetchCompletion" 42 | ) 43 | ); 44 | if (isFileMagic(sourceFile) && !hasFetchCompletionDeclaration) { 45 | sourceFile = ts.factory.updateSourceFile( 46 | sourceFile, 47 | [requireStatement, ...sourceFile.statements], 48 | sourceFile.isDeclarationFile, 49 | sourceFile.referencedFiles, 50 | sourceFile.typeReferenceDirectives, 51 | sourceFile.hasNoDefaultLib, 52 | sourceFile.libReferenceDirectives 53 | ); 54 | } 55 | return sourceFile; 56 | } 57 | 58 | sourceFile = insertRequireDeclarationIfNeeded(sourceFile); 59 | 60 | function visitor(node: ts.Node): ts.Node { 61 | 62 | // Special logic for magic functions 63 | if (ts.isFunctionDeclaration(node) && isFunctionMagic(node, sourceFile)) { 64 | const schemaObject = extractFunctionArgumentsAndReturn(node, typeChecker, program); 65 | const schema = JSON.stringify(schemaObject); 66 | const existingFunction = node.body?.getText(sourceFile); 67 | 68 | // Create call to external LLM with function text and JSON schema of return arguments 69 | // TODO Handle argument passing to LLM 70 | const newStatement = ctx.factory.createReturnStatement( 71 | ctx.factory.createAwaitExpression( 72 | ctx.factory.createCallExpression( 73 | ctx.factory.createIdentifier('fetchCompletion'), 74 | undefined, 75 | [ 76 | ctx.factory.createStringLiteral(existingFunction), 77 | ctx.factory.createObjectLiteralExpression( 78 | [ 79 | ts.factory.createPropertyAssignment('schema', ctx.factory.createStringLiteral(schema)), 80 | ], 81 | true 82 | ), 83 | ], 84 | ), 85 | ), 86 | ); 87 | 88 | const newFunctionBody = ctx.factory.createBlock([newStatement], true); 89 | 90 | const newModifiers = node.modifiers?.filter((modifier) => [ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.AsyncKeyword].includes(modifier.kind)) || []; 91 | 92 | if (!newModifiers.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) { 93 | newModifiers.push(ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)); 94 | } 95 | 96 | const newNode = ctx.factory.updateFunctionDeclaration( 97 | node, 98 | newModifiers, 99 | node.asteriskToken, 100 | node.name, 101 | node.typeParameters, 102 | node.parameters, 103 | node.type, 104 | newFunctionBody 105 | ); 106 | 107 | return ts.visitEachChild(newNode, visitor, ctx); 108 | } 109 | 110 | return ts.visitEachChild(node, visitor, ctx); 111 | } 112 | 113 | return ts.visitNode(sourceFile, visitor); 114 | }; 115 | }; 116 | } 117 | --------------------------------------------------------------------------------