├── .devcontainer ├── devcontainer.json └── init.sh ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── .gitignore ├── .links.json ├── .prettierrc.json ├── .vscode ├── settings.json └── tasks.json ├── LICENSE.txt ├── README.md ├── docs └── image │ └── example-aiModel.png ├── jest.config.cjs ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src ├── SchemaRegistry.ts ├── ToolFunction.ts ├── TypeSchemaResolver.ts ├── aiClassifier.ts ├── aiFunction.ts ├── aiModel.ts ├── errors.ts ├── index.ts ├── tools │ └── web.ts ├── types.ts └── utils.ts ├── test ├── ToolFunction.test.ts ├── TypeSchemaResolver.test.ts ├── aiClassifier.test.ts ├── aiFunction.test.ts ├── aiFunctionEHRExample.test.ts ├── aiModel.test.ts ├── annotations.test.ts ├── builder.test.ts ├── chatCompletionFlow.test.ts └── typedDataReceiverFlow.test.ts └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", 6 | "initializeCommand": "./.devcontainer/init.sh", 7 | "postCreateCommand": "pnpm install", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | "features": { 11 | "ghcr.io/devcontainers-contrib/features/pnpm:2": {} 12 | } 13 | 14 | // Configure tool-specific properties. 15 | // "customizations": {}, 16 | } 17 | -------------------------------------------------------------------------------- /.devcontainer/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | which op >/dev/null 2>&1 4 | if [[ $? -eq 0 ]]; then 5 | # Assume we're in a local dev environment 6 | # If 1Password cli is available, use it to resolve secret references in .env 7 | rm -rf .env.secrets.run 8 | op inject -i .env -o .env.secrets.run 9 | else 10 | # Assume we're in a codespace, secrets will be injected by the devcontainer 11 | true 12 | fi 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["prettier", "@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:prettier/recommended" 9 | ], 10 | "rules": { 11 | "@typescript-eslint/no-explicit-any": "off", 12 | "@typescript-eslint/no-non-null-assertion": "off", 13 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 14 | "no-empty": ["error", { "allowEmptyCatch": true }] 15 | }, 16 | "ignorePatterns": ["dist/"] 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | name: Lint and Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v3.3.0 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v3.6.0 20 | with: 21 | node-version: 18 22 | 23 | - name: Install pnpm 24 | uses: pnpm/action-setup@v2.2.4 25 | with: 26 | version: 8.6 27 | 28 | - name: Install Dependencies 29 | run: pnpm install 30 | 31 | - name: Run Linter 32 | run: pnpm lint 33 | 34 | - name: Build Source 35 | run: pnpm build 36 | 37 | - name: Run Tests 38 | run: pnpm test 39 | env: 40 | CI: true 41 | BING_API_KEY: ${{ secrets.BING_API_KEY }} 42 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | # *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | /types 9 | /scratch 10 | 11 | .npmrc 12 | test.sh 13 | .watchman* 14 | -------------------------------------------------------------------------------- /.links.json: -------------------------------------------------------------------------------- 1 | { 2 | "@deepkit/core": "../deepkit-framework/packages/core", 3 | "@deepkit/type": "../deepkit-framework/packages/type", 4 | "@deepkit/type-compiler": "../deepkit-framework/packages/type-compiler" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "auto", 12 | "singleQuote": true 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | "./src" 4 | ], 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true, 8 | "[javascript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[typecript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "npm.packageManager": "pnpm", 15 | "jest.jestCommandLine": "pnpm test-op --", 16 | "jest.autoRun": "off" 17 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "test", 7 | "group": { 8 | "kind": "test", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: test", 13 | "detail": "jest", 14 | "options": { 15 | "env": { 16 | "DOTENV_CONFIG_PATH": ".env.secrets.run" 17 | } 18 | } 19 | }, 20 | { 21 | "type": "typescript", 22 | "tsconfig": "tsconfig.json", 23 | "option": "watch", 24 | "problemMatcher": [ 25 | "$tsc-watch" 26 | ], 27 | "label": "tsc: watch - tsconfig.json" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jeff LaPorte 4 | Copyright (c) 2022 Hanayashiki - JSON Schema generation code 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeAI: An AI Engineering Framework for TypeScript 2 | 3 |

4 | GitHub top language 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | GitHub 13 | 14 |

15 | 16 | ![TypeAI Example](docs/image/example-aiModel.png) 17 | 18 | TypeAI is a toolkit for building AI-enabled apps using TypeScript that makes things look so simple it seems like magic. More importantly, it makes building with LLMs "feel" like ordinary code with low impedance mismatch. 19 | 20 | An example: 21 | 22 | ```typescript 23 | import { toAIFunction } from '@typeai/core' 24 | 25 | /** @description Given `text`, returns a number between 1 (positive) and -1 (negative) indicating its sentiment score. */ 26 | function sentimentSpec(text: string): number | void {} 27 | const sentiment = toAIFunction(sentimentSpec) 28 | 29 | const score = await sentiment('That was surprisingly easy!') 30 | ``` 31 | 32 | Just specify your types and function signatures as you naturally would, and TypeAI will generate the appropriate implementation respecting your type declarations. No loading separate schema files, no prompt engineering, and no manually writing JSON Schema representations of your functions. 33 | 34 | ### Contents 35 | 36 | 1. [Installation](#installation) 37 | 2. [Usage](#usage) 38 | - **Using TypeAI to generate functionality** 39 | - AI Models 40 | - AI Functions 41 | - AI Classifiers 42 | - **Using TypeAI to expose functionality to an LLM** 43 | - AI "Tool Functions" 44 | 3. [Gotchas](#gotchas) 45 | 4. [How does it work?](#how) 46 | 5. [Future Direction & TODOs](#future) 47 | 6. [Acknowledgements](#acknowledgements) 48 | 7. [License](#license) 49 | 50 | ### Support 51 | 52 | Follow me on Twitter: ![Twitter Follow](https://img.shields.io/twitter/follow/jefflaporte) 53 | 54 | ## Installation 55 | 56 | [DeepKit](<[url](https://github.com/deepkit/deepkit-framework)>) is required in order to provide runtime type information on your functions and types. 57 | 58 | ```sh 59 | npm install @typeai/core @deepkit/core 60 | ``` 61 | 62 | > NOTE: For now, automatic extraction of JSDoc @description tags requires these 63 | > forked npm package builds @deepkit/type and @deepkit/type-compiler 64 | 65 | ```sh 66 | npm install @deepkit/type@npm:@jefflaporte/deepkit-type@1.0.1-alpha.97-jl 67 | npm install --save-dev @deepkit/type-compiler@npm:@jefflaporte/deepkit-type-compiler@1.0.1-alpha.97-jl 68 | # Bash 69 | ./node_modules/.bin/deepkit-type-install 70 | # PowerShell 71 | pwsh ./node_modules/.bin/deepkit-type-install.ps1 72 | ``` 73 | 74 | _tsconfig.json_ 75 | 76 | ```js 77 | // tsconfig.json 78 | { 79 | "compilerOptions": { 80 | // ... 81 | 82 | // Note: DeepKit says that experimentalDecorators is not necessary when using @deepkit/type, 83 | // but I have found that deepkit's typeOf() does not always work with TypeScript > 4.9 84 | // without experimentalDecorators set. 85 | "experimentalDecorators": true 86 | }, 87 | "reflection": true 88 | } 89 | ``` 90 | 91 | NOTE: Some runtimes, such as `tsx`, won't work with Deepkit. See [Gotchas](#gotchas) for more info. 92 | 93 | _At execution time_ 94 | 95 | ```sh 96 | export OPENAI_API_KEY='...' # currently required for core functionality 97 | export BING_API_KEY='...' # if using predefined SearchWeb Tool function 98 | ``` 99 | 100 | TypeAI makes connecting your functions and types to AI APIs like OpenAI's chat completion endpoints lightweight by using runtime type reflection on TypeScript code to generate the JSON schema required by OpenAI's function calling feature, and by handling function dispatch and result delivery to the LLM. 101 | 102 | ## Usage 103 | 104 | TypeAI currently provides two main areas of functionality: 105 | 106 | - Generation of "magic" AI-backed functions 107 | - AI Models 108 | - AI Functions 109 | - AI Classifiers 110 | - Generation and handing of LLM tool function glue 111 | - AI "Tool Functions" 112 | 113 | ### AI Functions 114 | 115 | To create an AI-backed function, write a stub function and pass it to `toAIFunction()`, which will generate an AI-backed function with the desired behaviour. 116 | 117 | ```typescript 118 | /** @description Given `text`, returns a number between 1 (positive) and -1 (negative) indicating its sentiment score. */ 119 | function sentimentSpec(text: string): number | void {} 120 | const sentiment = toAIFunction(sentimentSpec) 121 | 122 | const score = await sentiment('That was surprisingly easy!') 123 | ``` 124 | 125 | Functions with complex input and output TypeScript types work too. Here's a more interesting example: 126 | 127 | ```typescript 128 | type Patient = { 129 | name: string 130 | age: number 131 | isSmoker: boolean 132 | } 133 | type Diagnosis = { 134 | condition: string 135 | diagnosisDate: Date 136 | stage?: string 137 | type?: string 138 | histology?: string 139 | complications?: string 140 | } 141 | type Treatment = { 142 | name: string 143 | startDate: Date 144 | endDate?: Date 145 | } 146 | type Medication = Treatment & { 147 | dose?: string 148 | } 149 | type BloodTest = { 150 | name: string 151 | result: string 152 | testDate: Date 153 | } 154 | type PatientData = { 155 | patient: Patient 156 | diagnoses: Diagnosis[] 157 | treatments: Treatment | Medication[] 158 | bloodTests: BloodTest[] 159 | } 160 | 161 | /** @description Returns a PatientData record generate from the content of doctorsNotes notes. */ 162 | function generateElectronicHealthRecordSpec(input: string): PatientData | void {} 163 | const generateElectronicHealthRecord = toAIFunction(generateElectronicHealthRecordSpec, { 164 | model: 'gpt-4', 165 | }) 166 | ``` 167 | 168 | #### TypeScript enums to AI-backed Classifiers 169 | 170 | ```typescript 171 | enum AppRouteEnum { 172 | USER_PROFILE = '/user-profile', 173 | SEARCH = '/search', 174 | NOTIFICATIONS = '/notifications', 175 | SETTINGS = '/settings', 176 | HELP = '/help', 177 | SUPPORT_CHAT = '/support-chat', 178 | DOCS = '/docs', 179 | PROJECTS = '/projects', 180 | WORKSPACES = '/workspaces', 181 | } 182 | const AppRoute = toAIClassifier(AppRouteEnum) 183 | 184 | const appRouteRes = await AppRoute('I need to talk to somebody about billing') 185 | ``` 186 | 187 | ### AI "Tool Function" Helpers 188 | 189 | An AI tool function is a function provided to an LLM for it's own use in generating answers. 190 | 191 | Say you have a function and want to provide it's functionality to OpenAI's LLM for use with their **_Function Calling_** feature. 192 | 193 | _See:_ 194 | 195 | - [OpenAI Blog: Function calling and other API updates](https://openai.com/blog/function-calling-and-other-api-updates) 196 | - [OpenAI API: Create Chat Completion](https://platform.openai.com/docs/api-reference/chat/create) 197 | 198 | TypeAI provides three functions that make exposing your functions and models to GPT-3.5/4, and handling the resulting function call requests from GPT-3/4, transparent: 199 | 200 | ```typescript 201 | static ToolFunction.from( 202 | fn: (...args: any[]) => R, 203 | options?: ToolFunctionFromOptions 204 | ): ToolFunction 205 | 206 | static ToolFunction.modelSubmissionToolFor( 207 | cb: (arg: T) => Promise 208 | ): ToolFunction 209 | 210 | function handleToolUse( 211 | openAIClient: OpenAIApi, 212 | originalRequest: CreateChatCompletionRequest, 213 | responseData: CreateChatCompletionResponse, 214 | options?: { 215 | model?: string, 216 | registry?: SchemaRegistry, 217 | handle?: 'single' | 'multiple' 218 | }, 219 | ): Promise 220 | ``` 221 | 222 | They can be used like this: 223 | 224 | ```typescript 225 | import { 226 | OpenAIApi, 227 | Configuration, 228 | CreateChatCompletionRequest, 229 | ChatCompletionRequestMessage, 230 | ChatCompletionRequestMessageRoleEnum, 231 | } from 'openai' 232 | import { ToolFunction, handleToolUse } from '@typeai/core' 233 | import { getCurrentWeather } from 'yourModule' 234 | 235 | // Init OpenAI client 236 | const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY }) 237 | const openai = new OpenAIApi(configuration) 238 | 239 | // Generate JSON Schema for function and dependent types 240 | const getCurrentWeatherTool = ToolFunction.from(getCurrentWeather) 241 | 242 | // Run a chat completion sequence 243 | const messages: ChatCompletionRequestMessage[] = [ 244 | { 245 | role: ChatCompletionRequestMessageRoleEnum.User, 246 | content: "What's the weather like in Boston? Say it like a weather reporter.", 247 | }, 248 | ] 249 | const request: CreateChatCompletionRequest = { 250 | model: 'gpt-3.5-turbo', 251 | messages, 252 | functions: [getCurrentWeatherTool.schema], 253 | stream: false, 254 | max_tokens: 1000, 255 | } 256 | const { data: response } = await openai.createChatCompletion(request) 257 | 258 | // Transparently handle any LLM calls to your function. 259 | // handleToolUse() returns OpenAI's final response after 260 | // any/all function calls have been completed 261 | const responseData = await handleToolUse(openai, request, response) 262 | const result = responseData?.choices[0].message 263 | 264 | /* 265 | Good afternoon, Boston! This is your weather reporter bringing you the latest 266 | updates. Currently, we're experiencing a pleasant temperature of 82 degrees Celsius. The sky is a mix of sunshine and clouds, making for a beautiful day. However, there is a 25% chance of precipitation, so you might want to keep an umbrella handy. Additionally, the atmospheric pressure is at 25 mmHg. Overall, it's a great day to get outside and enjoy the city. Stay safe and have a wonderful time! 267 | */ 268 | ``` 269 | 270 | ## Gotchas 271 | 272 | Due to the way Deepkit injects it's type-compiler transform, by patching tsc, some runtimes may not work. These are know NOT to work: 273 | 274 | - `tsx` 275 | 276 | ## How does it work? 277 | 278 | TypeAI uses TypeScript runtime type info provided by `@deepkit/type` to: 279 | 280 | - generate replacement functions with the same signature as your function stubs 281 | - generate JSON Schema descriptions of your function and dependent types which are provided to the OpenAI API so that it can repect your desired type structure 282 | 283 | This results in a coding experience that feels "native". 284 | 285 | _Example_ 286 | 287 | ```typescript 288 | import { ToolFunction, handleToolUse } from '@typeai/core' 289 | 290 | // Your type definitions 291 | // ... 292 | // Your function definitions dependent on your types 293 | // ... 294 | // eg: 295 | const getCurrentWeather = function getCurrentWeather( 296 | location: string, 297 | unit: TemperatureUnit = 'fahrenheit', 298 | options?: WeatherOptions, 299 | ): WeatherInfo { 300 | const weatherInfo: WeatherInfo = { 301 | location: location, 302 | temperature: 82, 303 | unit: unit, 304 | precipitationPct: options?.flags?.includePrecipitation ? 25 : undefined, 305 | pressureMmHg: options?.flags?.includePressure ? 25 : undefined, 306 | forecast: ['sunny', 'cloudy'], 307 | } 308 | return weatherInfo 309 | } 310 | 311 | // Register your function and type info 312 | const getCurrentWeatherTool = ToolFunction.from(getCurrentWeather) 313 | 314 | // Run a completion series 315 | const messages: ChatCompletionRequestMessage[] = [ 316 | { 317 | role: ChatCompletionRequestMessageRoleEnum.User, 318 | content: "What's the weather like in Boston? Say it like a weather reporter.", 319 | }, 320 | ] 321 | const request: CreateChatCompletionRequest = { 322 | model: 'gpt-3.5-turbo-0613', 323 | messages, 324 | functions: [getCurrentWeatherTool.schema], 325 | stream: false, 326 | max_tokens: 1000, 327 | } 328 | const { data: response } = await openai.createChatCompletion(request) 329 | const responseData = await handleToolUse(openai, request, response) 330 | const result = responseData?.choices[0].message 331 | console.log(`LLM final result: ${JSON.stringify(result, null, 2)}`) 332 | ``` 333 | 334 | Note: The OpenAI completion API does not like void function responses. 335 | 336 | ## Future Direction & TODOs 337 | 338 | - TODO 339 | 340 | ## Acknowledgements 341 | 342 | - the Prefect / Marvin Team 343 | - The concept of source-code-less AI Functions, Models, and that use function specification and description info to auto-generate their behavior is originally due to the amazing team at [PrefectHQ](https://www.prefect.io/opensource/) that created [prefecthq/marvin](https://github.com/prefecthq/marvin) for use in Python. 344 | - Wang Chenyu 345 | - TypeAI's JSON Schema generation is currently a hacked up version of code plucked from Chenyu's [hanayashiki/deepkit-openapi](https://github.com/hanayashiki/deepkit-openapi) 346 | 347 | ## License 348 | 349 | [See LICENSE.txt](LICENSE.txt) 350 | -------------------------------------------------------------------------------- /docs/image/example-aiModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeAI-dev/typeai/413a0abf71590df123e4ce92a5375554dcdf0e25/docs/image/example-aiModel.png -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typeai/core", 3 | "version": "0.6.1", 4 | "description": "An AI Engineering Framework for TypeScript", 5 | "author": "Jeff LaPorte (https://github.com/jefflaporte)", 6 | "repository": "https://github.com/TypeAI-dev/typeai", 7 | "private": false, 8 | "license": "SEE LICENSE IN LICENSE.txt", 9 | "type": "module", 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.js", 16 | "require": "./dist/index.cjs" 17 | } 18 | }, 19 | "scripts": { 20 | "build": "rm -rf dist && rollup -c", 21 | "clean": "rm -rf dist node_modules", 22 | "link": "npm-local-development .", 23 | "lint": "eslint src", 24 | "prepare": "./node_modules/.bin/deepkit-type-install", 25 | "prepublishOnly": "pnpm run build", 26 | "test": "jest --setupFiles 'dotenv/config'", 27 | "test-op": "op run --env-file=.env jest" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "keywords": [ 33 | "ai", 34 | "llm", 35 | "gpt", 36 | "openai", 37 | "agents", 38 | "deepkit", 39 | "typescript" 40 | ], 41 | "files": [ 42 | "dist/*.js", 43 | "dist/*.map", 44 | "dist/*.d.ts", 45 | "README.md", 46 | "LICENSE.txt" 47 | ], 48 | "bugs": { 49 | "url": "https://github.com/TypeAI-dev/typeai/issues/new?template=bug_report.md" 50 | }, 51 | "devDependencies": { 52 | "@deepkit/type-compiler": "npm:@jefflaporte/deepkit-type-compiler@1.0.1-alpha.97-jl", 53 | "@rollup/plugin-commonjs": "^25.0.3", 54 | "@rollup/plugin-node-resolve": "^15.1.0", 55 | "@types/debug": "^4.1.8", 56 | "@types/jest": "^29.5.2", 57 | "@types/node": "^20.3.2", 58 | "@typescript-eslint/eslint-plugin": "^6.0.0", 59 | "@typescript-eslint/parser": "^6.0.0", 60 | "dotenv": "^16.3.1", 61 | "esbuild": "^0.18.11", 62 | "eslint": "^8.44.0", 63 | "eslint-config-prettier": "^8.8.0", 64 | "eslint-plugin-prettier": "^5.0.0", 65 | "jest": "^29.5.0", 66 | "npm-local-development": "^0.4.0", 67 | "rollup": "^3.26.2", 68 | "rollup-plugin-dts": "^5.3.0", 69 | "rollup-plugin-esbuild": "^5.0.0", 70 | "rollup-plugin-esbuild-transform": "^1.5.0", 71 | "ts-jest": "^29.1.1", 72 | "ts-node": "^10.9.1", 73 | "typescript": "^4.8.4" 74 | }, 75 | "peerDependencies": { 76 | "@deepkit/core": ">=1.0.1-alpha.97", 77 | "@deepkit/type": ">=1.0.1-alpha.97-jl", 78 | "openai": "~3.3" 79 | }, 80 | "dependencies": { 81 | "@deepkit/core": "1.0.1-alpha.97", 82 | "@deepkit/type": "npm:@jefflaporte/deepkit-type@1.0.1-alpha.97-jl", 83 | "@types/jsdom": "^21.1.1", 84 | "@types/lodash": "^4.14.195", 85 | "axios": "^1.4.0", 86 | "camelcase": "^6.3.0", 87 | "debug": "^4.3.4", 88 | "gpt-tokenizer": "^2.1.1", 89 | "jsdom": "^22.1.0", 90 | "lodash": "^4.17.21", 91 | "openai": "~3.3.0" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts' 2 | import esbuild from 'rollup-plugin-esbuild' 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | // import esbuild from 'rollup-plugin-esbuild-transform' 6 | // import { createRequire } from 'node:module' 7 | // const require = createRequire(import.meta.url) 8 | // const packageJson = require('./package.json') 9 | // const name = packageJson.main.replace(/\.js$/, '') 10 | 11 | const bundle = config => ({ 12 | ...config, 13 | input: 'src/index.ts', 14 | external: id => !/^[./]/.test(id), 15 | }) 16 | 17 | // const ext = id => { 18 | // const res = !/^[./]/.test(id) && id !== 'lodash/cloneDeepWith' && id !== 'src/index.ts' 19 | // console.log(`id: ${id} - ${res}`) 20 | // return res 21 | // } 22 | 23 | export default [ 24 | { 25 | input: 'src/index.ts', 26 | external: [/@deepkit/, 'debug', 'openai'], 27 | plugins: [ 28 | nodeResolve({ 29 | resolveOnly: id => { 30 | return true 31 | }, 32 | }), 33 | commonjs({ 34 | include: 'node_modules/**', 35 | }), 36 | esbuild(), 37 | ], 38 | output: [ 39 | { 40 | file: `dist/index.cjs`, 41 | format: 'cjs', 42 | sourcemap: true, 43 | }, 44 | { 45 | file: `dist/index.js`, 46 | format: 'es', 47 | sourcemap: true, 48 | }, 49 | ], 50 | }, 51 | bundle({ 52 | plugins: [dts()], 53 | output: { 54 | file: `dist/index.d.ts`, 55 | format: 'es', 56 | }, 57 | }), 58 | ] 59 | -------------------------------------------------------------------------------- /src/SchemaRegistry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isSameType, 3 | ReflectionKind, 4 | stringifyType, 5 | Type, 6 | TypeClass, 7 | TypeEnum, 8 | TypeObjectLiteral, 9 | TypeUnion, 10 | TypeFunction, 11 | ReflectionClass, 12 | } from '@deepkit/type' 13 | import camelCase from 'camelcase' 14 | import { TypeAiSchemaNameConflict } from './errors' 15 | import { Schema, getMetaDescription } from './types' 16 | import util from 'util' 17 | import Debug from 'debug' 18 | const debug = Debug('typeai') 19 | 20 | export interface SchemeEntry { 21 | name: string 22 | description?: string 23 | schema: Schema 24 | type: Type 25 | } 26 | 27 | export type RegistableSchema = TypeClass | TypeObjectLiteral | TypeEnum | TypeUnion | TypeFunction 28 | 29 | export class SchemaRegistry { 30 | static _instance?: SchemaRegistry 31 | static getInstance(): SchemaRegistry { 32 | return this._instance || (this._instance = new SchemaRegistry()) 33 | } 34 | static resetInstance() { 35 | this._instance = undefined 36 | } 37 | 38 | store: Map = new Map() 39 | 40 | getSchemaKey(t: RegistableSchema): string { 41 | // Handle user preferred name 42 | const rootName = t.kind === ReflectionKind.class ? t.classType.name : t.typeName ?? '' 43 | 44 | const args = t.kind === ReflectionKind.class ? t.arguments ?? [] : t.typeArguments ?? [] 45 | 46 | return camelCase([rootName, ...args.map(a => this.getTypeKey(a))], { 47 | pascalCase: true, 48 | }) 49 | } 50 | 51 | getTypeKey(t: Type): string { 52 | if ( 53 | t.kind === ReflectionKind.string || 54 | t.kind === ReflectionKind.number || 55 | t.kind === ReflectionKind.bigint || 56 | t.kind === ReflectionKind.boolean || 57 | t.kind === ReflectionKind.null || 58 | t.kind === ReflectionKind.undefined 59 | ) { 60 | return stringifyType(t) 61 | } else if ( 62 | t.kind === ReflectionKind.class || 63 | t.kind === ReflectionKind.objectLiteral || 64 | t.kind === ReflectionKind.enum || 65 | t.kind === ReflectionKind.union || 66 | t.kind === ReflectionKind.function 67 | ) { 68 | return this.getSchemaKey(t) 69 | } else if (t.kind === ReflectionKind.array) { 70 | return camelCase([this.getTypeKey(t.type), 'Array'], { 71 | pascalCase: false, 72 | }) 73 | } else { 74 | // Complex types not named 75 | return '' 76 | } 77 | } 78 | 79 | registerSchema(name: string, type: Type, schema: Schema) { 80 | const currentEntry = this.store.get(name) 81 | 82 | let description = '' 83 | try { 84 | const refl = ReflectionClass.from(type) 85 | const metaDescription = getMetaDescription(type) 86 | description = refl?.description || metaDescription 87 | debug(`t: description: ${description}`) 88 | debug(`refl: ${util.inspect(refl, { depth: 6 })}`) 89 | } catch (e) {} 90 | 91 | if (currentEntry && !isSameType(type, currentEntry?.type)) { 92 | throw new TypeAiSchemaNameConflict(type, currentEntry.type, name) 93 | } 94 | 95 | this.store.set(name, { type, schema, name, description: description }) 96 | schema.__registryKey = name 97 | } 98 | 99 | // eslint-disable-next-line @typescript-eslint/ban-types 100 | getFunction(name: string): Function | undefined { 101 | const currentEntry = this.store.get(name) 102 | if (!currentEntry) { 103 | throw new Error(`Schema ${name} not found`) 104 | } 105 | if (currentEntry.type.kind !== ReflectionKind.function) { 106 | throw new Error(`Schema ${name} is not a function`) 107 | } 108 | // debug(`SchemaRegistry.getFunction: ${util.inspect(currentEntry)}`) 109 | 110 | return currentEntry.type.function 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ToolFunction.ts: -------------------------------------------------------------------------------- 1 | import { DeepKitTypeError } from './errors' 2 | import { SchemaRegistry } from './SchemaRegistry' 3 | import { 4 | ReflectionFunction, 5 | ReceiveType, 6 | resolveReceiveType, 7 | InlineRuntimeType, 8 | Type, 9 | } from '@deepkit/type' 10 | import { TypeSchemaResolver } from './TypeSchemaResolver' 11 | import { Schema } from './types' 12 | import cloneDeepWith from 'lodash/cloneDeepWith' 13 | import { JSONSchemaOpenAIFunction, JSONSchema, JSONSchemaTypeString, JSONSchemaEnum } from './types' 14 | import { 15 | ChatCompletionRequestMessageRoleEnum, 16 | CreateChatCompletionRequest, 17 | CreateChatCompletionResponse, 18 | OpenAIApi, 19 | } from 'openai' 20 | import Debug from 'debug' 21 | const debug = Debug('typeai') 22 | const debugNet = Debug('typeai:net') 23 | 24 | type HandleToolUseOptions = { 25 | model?: string 26 | registry?: SchemaRegistry 27 | handle?: 'single' | 'multiple' 28 | } 29 | 30 | export const handleToolUse = async function ( 31 | openAIClient: OpenAIApi, 32 | originalRequest: CreateChatCompletionRequest, 33 | responseData: CreateChatCompletionResponse, 34 | options?: HandleToolUseOptions, 35 | ): Promise { 36 | const _options = { ...options, handle: options?.handle || 'multiple' } 37 | const messages = originalRequest.messages 38 | 39 | const currentMessage = responseData.choices[0].message 40 | const schemaRegistry = options?.registry || SchemaRegistry.getInstance() 41 | 42 | if (currentMessage?.function_call) { 43 | debug(`handleToolUse: function_call: ${JSON.stringify(currentMessage.function_call, null, 2)}`) 44 | const function_name = currentMessage.function_call.name as string 45 | const fn = schemaRegistry.getFunction(function_name) 46 | if (!fn) { 47 | throw new Error(`handleToolUse: function ${function_name} not found`) 48 | } 49 | const function_args = JSON.parse(currentMessage.function_call.arguments || '') 50 | debug(`handleToolUse: function_args: ${currentMessage.function_call.arguments}`) 51 | 52 | // Map args to positional args - naive for now - TODO 53 | const argKeys = Object.keys(function_args) 54 | const positionalArgs = argKeys.map(k => function_args[k]) 55 | const function_response = await fn(...positionalArgs) 56 | 57 | // Send function result to LLM 58 | messages.push(currentMessage) // extend conversation with assistant's reply 59 | messages.push({ 60 | role: ChatCompletionRequestMessageRoleEnum.Function, 61 | name: function_name, 62 | content: JSON.stringify(function_response), 63 | }) 64 | const nextRequest: CreateChatCompletionRequest = { 65 | model: options?.model || responseData.model, 66 | messages, 67 | functions: originalRequest?.functions || [], 68 | } 69 | debugNet(`handleToolUse: nextRequest: ${JSON.stringify(nextRequest, null, 2)}`) 70 | 71 | const nextResponse = await openAIClient.createChatCompletion(nextRequest) 72 | debugNet(`handleToolUse: nextResponse: ${JSON.stringify(nextResponse.data, null, 2)}`) 73 | 74 | if (nextResponse.data.choices[0]?.finish_reason === 'stop' || _options.handle === 'single') { 75 | debug( 76 | `handleToolUse: Completed with finish_reason:${nextResponse.data.choices[0]?.finish_reason} handle:${_options.handle}`, 77 | ) 78 | return responseData 79 | } else { 80 | return handleToolUse(openAIClient, originalRequest, nextResponse.data, _options) 81 | } 82 | } else { 83 | debug(`handleToolUse: Completed with no function_call`) 84 | return responseData 85 | } 86 | } 87 | 88 | type ToolFunctionFromOptions = { 89 | registry?: SchemaRegistry 90 | overrideName?: string 91 | } 92 | export class ToolFunction { 93 | schemaRegistry: SchemaRegistry = SchemaRegistry.getInstance() 94 | errors: DeepKitTypeError[] = [] 95 | // eslint-disable-next-line @typescript-eslint/ban-types 96 | fn: Function 97 | $defs: Map = new Map() 98 | _schema?: JSONSchemaOpenAIFunction 99 | 100 | constructor( 101 | // eslint-disable-next-line @typescript-eslint/ban-types 102 | fn: Function, 103 | schemaRegisty: SchemaRegistry, 104 | ) { 105 | this.fn = fn 106 | this.schemaRegistry = schemaRegisty || this.schemaRegistry 107 | } 108 | 109 | static from(fn: (...args: any[]) => R, options?: ToolFunctionFromOptions): ToolFunction { 110 | const reflectFn = ReflectionFunction.from(fn) 111 | const name = options?.overrideName || fn.name 112 | const registry = options?.registry || SchemaRegistry.getInstance() 113 | const resolver = new TypeSchemaResolver(reflectFn.type, registry, { 114 | overrideName: name, 115 | }) 116 | resolver.resolve() 117 | const oaif = new ToolFunction(fn, registry) 118 | return oaif 119 | } 120 | 121 | static modelSubmissionToolFor( 122 | cb: (arg: T) => Promise, 123 | t?: ReceiveType, 124 | ): ToolFunction { 125 | const tType = resolveReceiveType(t) 126 | const modelType = tType as Type 127 | const name = `submit${modelType.typeName}Data` 128 | 129 | /** Submits generated data */ 130 | const submitDataSpec = function submitDataSpec( 131 | data: InlineRuntimeType, 132 | ): string { 133 | debug(`submitData: for:${name} data:${JSON.stringify(data, null, 2)}`) 134 | cb(data) 135 | return '{ "status": "ok" }' 136 | } 137 | const fn = Object.defineProperty(submitDataSpec, 'name', { 138 | value: name, 139 | writable: false, 140 | }) 141 | 142 | const submitDataTool = ToolFunction.from(fn, { overrideName: name }) 143 | return submitDataTool 144 | } 145 | 146 | get schema(): JSONSchemaOpenAIFunction { 147 | this._schema = this._schema || this.serialize() 148 | return this._schema 149 | } 150 | get registry(): SchemaRegistry { 151 | return this.schemaRegistry 152 | } 153 | 154 | get name(): string { 155 | return this.fn.name 156 | } 157 | 158 | get description(): string { 159 | return this.schema.description || '' 160 | } 161 | 162 | registerDef(name: string, schema?: JSONSchema) { 163 | if (this.$defs.has(name) || !schema) return 164 | this.$defs.set(name, schema) 165 | } 166 | 167 | schemaToJSONSchema(schema: Schema): [JSONSchema, JSONSchema?] { 168 | let refJSONSchema: JSONSchema | undefined 169 | if (schema.__registryKey) { 170 | refJSONSchema = { $ref: `#/$defs/${schema.__registryKey}` } 171 | } 172 | const defJSONSchema: JSONSchema = { 173 | type: (schema.type as JSONSchemaTypeString) || 'null', 174 | } 175 | 176 | if (schema.type === 'array') { 177 | if (schema.items) { 178 | const [imm, def] = this.schemaToJSONSchema(schema.items) 179 | defJSONSchema.items = imm 180 | this.registerDef(schema.items.__registryKey as string, def) 181 | } 182 | } else if (schema.properties) { 183 | defJSONSchema.properties = {} 184 | for (const [key, property] of Object.entries(schema.properties)) { 185 | const [imm, def] = this.schemaToJSONSchema(property) 186 | defJSONSchema.properties[key] = imm 187 | this.registerDef(property.__registryKey as string, def) 188 | } 189 | } 190 | if (schema.description) defJSONSchema.description = schema.description 191 | if (schema.required) defJSONSchema.required = schema.required 192 | if (schema.enum) defJSONSchema.enum = schema.enum as JSONSchemaEnum[] 193 | 194 | if (refJSONSchema) { 195 | return [refJSONSchema, defJSONSchema] 196 | } else { 197 | return [defJSONSchema, undefined] 198 | } 199 | } 200 | 201 | getFunctionSchema(): JSONSchemaOpenAIFunction { 202 | const functionSchema: JSONSchemaOpenAIFunction = { 203 | name: this.fn.name, 204 | } 205 | 206 | for (const [, schema] of this.schemaRegistry.store) { 207 | if (schema.schema.type !== 'function' || schema.name != this.fn.name) continue 208 | functionSchema.parameters = functionSchema.parameters ?? { 209 | type: 'object', 210 | properties: {}, 211 | } 212 | functionSchema.parameters.properties = functionSchema.parameters.properties ?? {} 213 | functionSchema.parameters.required = schema.schema.required ?? [] 214 | for (const [key, subSchema] of Object.entries(schema.schema.properties || {})) { 215 | const [imm, def] = this.schemaToJSONSchema(subSchema || {}) 216 | this.registerDef(subSchema.__registryKey as string, def) 217 | const subSchemaJSON = imm 218 | functionSchema.parameters.properties[key] = { 219 | ...subSchemaJSON, 220 | } 221 | } 222 | } 223 | functionSchema.parameters = functionSchema.parameters || {} 224 | functionSchema.parameters.$defs = this.$defs.size ? Object.fromEntries(this.$defs) : undefined 225 | return functionSchema 226 | } 227 | 228 | serialize(): JSONSchemaOpenAIFunction { 229 | return cloneDeepWith(this.getFunctionSchema(), (c: any) => { 230 | if (typeof c === 'object') { 231 | for (const key of Object.keys(c)) { 232 | // Remove internal keys. 233 | if (key.startsWith('__')) delete c[key] 234 | } 235 | } 236 | }) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/TypeSchemaResolver.ts: -------------------------------------------------------------------------------- 1 | import { DeepKitTypeError, DeepKitTypeErrors, LiteralSupported, TypeNotSupported } from './errors' 2 | import { AnySchema, Schema, getMetaDescription } from './types' 3 | import * as util from 'util' 4 | 5 | import { 6 | reflect, 7 | ReflectionKind, 8 | Type, 9 | TypeClass, 10 | TypeEnum, 11 | TypeFunction, 12 | TypeLiteral, 13 | TypeObjectLiteral, 14 | TypeParameter, 15 | } from '@deepkit/type' 16 | import { getParentClass } from '@deepkit/core' 17 | import { SchemaRegistry } from './SchemaRegistry' 18 | import Debug from 'debug' 19 | const debug = Debug('typeai') 20 | 21 | // See DeepKit type reflection 22 | // https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/reflection/reflection.ts 23 | export class TypeSchemaResolver { 24 | result: Schema = { ...AnySchema } 25 | errors: DeepKitTypeError[] = [] 26 | 27 | constructor( 28 | public t: Type, 29 | public schemaRegisty: SchemaRegistry, 30 | public options?: { overrideName?: string }, 31 | ) {} 32 | 33 | resolveBasic() { 34 | debug(`*** resolveBasic: ${this.t.kind}`) 35 | const metaDescription = getMetaDescription(this.t) 36 | switch (this.t.kind) { 37 | case ReflectionKind.never: 38 | this.result.not = AnySchema 39 | return 40 | case ReflectionKind.any: 41 | case ReflectionKind.unknown: 42 | case ReflectionKind.void: 43 | this.result = AnySchema 44 | this.result.description = metaDescription || undefined 45 | return 46 | case ReflectionKind.object: 47 | this.result.type = 'object' 48 | return 49 | case ReflectionKind.string: 50 | this.result.type = 'string' 51 | this.result.description = metaDescription || undefined 52 | return 53 | case ReflectionKind.number: 54 | this.result.type = 'number' 55 | this.result.description = metaDescription || undefined 56 | return 57 | case ReflectionKind.boolean: 58 | this.result.type = 'boolean' 59 | this.result.description = metaDescription || undefined 60 | return 61 | case ReflectionKind.bigint: 62 | this.result.type = 'number' 63 | this.result.description = metaDescription || undefined 64 | return 65 | case ReflectionKind.null: 66 | this.result.type = 'null' 67 | return 68 | case ReflectionKind.undefined: 69 | this.result.__isUndefined = true 70 | return 71 | case ReflectionKind.literal: { 72 | const type = mapSimpleLiteralToType(this.t.literal) 73 | if (type) { 74 | this.result.type = type 75 | this.result.enum = [this.t.literal as any] 76 | this.result.description = metaDescription || undefined 77 | } else { 78 | this.errors.push(new LiteralSupported(typeof this.t.literal)) 79 | } 80 | return 81 | } 82 | case ReflectionKind.templateLiteral: 83 | this.result.type = 'string' 84 | this.errors.push( 85 | new TypeNotSupported(this.t, 'Literal is treated as string for simplicity'), 86 | ) 87 | return 88 | case ReflectionKind.class: 89 | case ReflectionKind.objectLiteral: 90 | this.resolveClassOrObjectLiteral() 91 | return 92 | case ReflectionKind.array: { 93 | this.result.type = 'array' 94 | const itemsResult = resolveTypeSchema(this.t.type, this.schemaRegisty) 95 | 96 | this.result.items = itemsResult.result 97 | this.result.description = metaDescription || undefined 98 | this.errors.push(...itemsResult.errors) 99 | return 100 | } 101 | case ReflectionKind.enum: 102 | this.resolveEnum() 103 | return 104 | case ReflectionKind.union: 105 | this.resolveUnion() 106 | return 107 | case ReflectionKind.function: 108 | this.resolveFunction() 109 | return 110 | 111 | default: 112 | this.errors.push(new TypeNotSupported(this.t, `kind: ${this.t.kind}`)) 113 | return 114 | } 115 | } 116 | 117 | resolveFunction() { 118 | if (this.t.kind !== ReflectionKind.function) return 119 | this.result.type = 'function' 120 | this.result.description = this.t.description || undefined 121 | 122 | const typeFunction: TypeFunction | undefined = this.t 123 | const parameters: TypeParameter[] = typeFunction.parameters ?? [] 124 | const required: string[] = [] 125 | 126 | this.result.properties = {} 127 | 128 | for (const parameter of parameters) { 129 | if (parameter.kind !== ReflectionKind.parameter) { 130 | throw new Error(`Expected ReflectionKind.parameter, got ${parameter.kind}`) 131 | } 132 | const wrappedType = parameter.type 133 | const typeResolver = resolveTypeSchema(wrappedType, this.schemaRegisty) 134 | if (!parameter.default && !required.includes(String(parameter.name))) { 135 | required.push(String(parameter.name)) 136 | } 137 | this.result.properties[String(parameter.name)] = typeResolver.result 138 | this.errors.push(...typeResolver.errors) 139 | } 140 | if (required.length) { 141 | this.result.required = required 142 | } 143 | 144 | let registryKey: string = String(typeFunction.name) 145 | if (this.options?.overrideName) { 146 | registryKey = this.options.overrideName 147 | } 148 | debug(`*** resolveFunction: registryKey: ${registryKey}`) 149 | 150 | if (registryKey) { 151 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result) 152 | } 153 | } 154 | 155 | resolveClassOrObjectLiteral() { 156 | if (this.t.kind !== ReflectionKind.class && this.t.kind !== ReflectionKind.objectLiteral) { 157 | return 158 | } 159 | 160 | const registryKey = this.schemaRegisty.getSchemaKey(this.t) 161 | if (this.schemaRegisty.store.has(registryKey)) { 162 | return 163 | } else if (registryKey) { 164 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result) 165 | } 166 | 167 | this.result.type = 'object' 168 | this.result.description = this.t.description || undefined 169 | 170 | let typeClass: TypeClass | TypeObjectLiteral | undefined = this.t 171 | this.result.properties = {} 172 | 173 | const typeClasses: (TypeClass | TypeObjectLiteral | undefined)[] = [this.t] 174 | 175 | const required: string[] = [] 176 | 177 | if (this.t.kind === ReflectionKind.class) { 178 | // Build a list of inheritance, from root to current class. 179 | for (;;) { 180 | const parentClass = getParentClass((typeClass as TypeClass).classType) 181 | if (parentClass) { 182 | typeClass = reflect(parentClass) as any 183 | typeClasses.unshift(typeClass) 184 | } else { 185 | break 186 | } 187 | } 188 | } 189 | 190 | // Follow the order to override properties. 191 | for (const typeClass of typeClasses) { 192 | for (const typeItem of typeClass!.types) { 193 | if ( 194 | typeItem.kind === ReflectionKind.property || 195 | typeItem.kind === ReflectionKind.propertySignature 196 | ) { 197 | const typeResolver = resolveTypeSchema(typeItem.type, this.schemaRegisty) 198 | 199 | if (!typeItem.optional && !required.includes(String(typeItem.name))) { 200 | required.push(String(typeItem.name)) 201 | } 202 | 203 | this.result.properties[String(typeItem.name)] = typeResolver.result 204 | this.errors.push(...typeResolver.errors) 205 | } 206 | } 207 | } 208 | 209 | if (required.length) { 210 | this.result.required = required 211 | } 212 | 213 | if (registryKey) { 214 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result) 215 | } 216 | } 217 | 218 | resolveEnum() { 219 | if (this.t.kind !== ReflectionKind.enum) { 220 | return 221 | } 222 | 223 | const registryKey = this.schemaRegisty.getSchemaKey(this.t) 224 | if (registryKey && this.schemaRegisty.store.has(registryKey)) { 225 | return 226 | } else { 227 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result) 228 | } 229 | 230 | const types = new Set() 231 | 232 | for (const value of this.t.values) { 233 | const currentType = mapSimpleLiteralToType(value) 234 | 235 | if (currentType === undefined) { 236 | this.errors.push(new TypeNotSupported(this.t, `Enum with unsupported members. `)) 237 | continue 238 | } 239 | 240 | types.add(currentType) 241 | } 242 | 243 | this.result.type = types.size > 1 ? undefined : [...types.values()][0] 244 | this.result.description = this.t.description || undefined 245 | this.result.enum = this.t.values as any 246 | 247 | if (registryKey) { 248 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result) 249 | } 250 | } 251 | 252 | resolveUnion() { 253 | if (this.t.kind !== ReflectionKind.union) { 254 | return 255 | } 256 | 257 | // Find out whether it is a union of literals. If so, treat it as an enum 258 | if ( 259 | this.t.types.every( 260 | (t): t is TypeLiteral => 261 | t.kind === ReflectionKind.literal && 262 | ['string', 'number'].includes(mapSimpleLiteralToType(t.literal) as any), 263 | ) 264 | ) { 265 | const enumType: TypeEnum = { 266 | ...this.t, 267 | kind: ReflectionKind.enum, 268 | // TODO: TypeKit needs support added for description on TypeUnion 269 | // description: this.t.description || undefined, 270 | enum: Object.fromEntries(this.t.types.map(t => [t.literal, t.literal as any])), 271 | values: this.t.types.map(t => t.literal as any), 272 | indexType: this.t, 273 | } 274 | 275 | const { result, errors } = resolveTypeSchema(enumType, this.schemaRegisty) 276 | this.result = result 277 | this.errors.push(...errors) 278 | } else if ( 279 | this.t.types.length === 2 && 280 | this.t.types.some(t => t.kind === ReflectionKind.void) 281 | ) { 282 | // We want to make it easier to write function stubs for wrapping, so we handle wrapping functions 283 | // with return types of the form "R | void" by removing the void from the return type. 284 | 285 | // Lift sole non-void type, replace union 286 | const nonVoidType = this.t.types.find(t => t.kind !== ReflectionKind.void) 287 | if (nonVoidType) { 288 | const { result, errors } = resolveTypeSchema(nonVoidType, this.schemaRegisty) 289 | this.result = result 290 | this.errors.push(...errors) 291 | } 292 | } else { 293 | this.result.type = undefined 294 | this.result.oneOf = [] 295 | 296 | for (const t of this.t.types) { 297 | const { result, errors } = resolveTypeSchema(t, this.schemaRegisty) 298 | this.result.oneOf?.push(result) 299 | this.errors.push(...errors) 300 | } 301 | } 302 | } 303 | 304 | resolve() { 305 | this.resolveBasic() 306 | 307 | return this 308 | } 309 | } 310 | 311 | export const mapSimpleLiteralToType = (literal: any) => { 312 | if (typeof literal === 'string') { 313 | return 'string' 314 | } else if (typeof literal === 'bigint') { 315 | return 'integer' 316 | } else if (typeof literal === 'number') { 317 | return 'number' 318 | } else if (typeof literal === 'boolean') { 319 | return 'boolean' 320 | } else { 321 | return 322 | } 323 | } 324 | 325 | export const unwrapTypeSchema = (t: Type, r: SchemaRegistry = new SchemaRegistry()) => { 326 | const resolver = new TypeSchemaResolver(t, r).resolve() 327 | 328 | if (resolver.errors.length === 0) { 329 | return resolver.result 330 | } else { 331 | throw new DeepKitTypeErrors(resolver.errors, 'Errors with input type. ') 332 | } 333 | } 334 | 335 | export const resolveTypeSchema = (t: Type, r: SchemaRegistry = new SchemaRegistry()) => { 336 | let tsr 337 | try { 338 | tsr = new TypeSchemaResolver(t, r).resolve() 339 | } catch (e) { 340 | console.error(`Error: ${util.inspect(e, { depth: 3 })}`) 341 | console.error(`SchemaRegistry.store: ${util.inspect(r.store, { depth: 3 })}`) 342 | throw e 343 | } 344 | return tsr 345 | } 346 | -------------------------------------------------------------------------------- /src/aiClassifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateChatCompletionRequest, 3 | ChatCompletionRequestMessage, 4 | ChatCompletionRequestMessageRoleEnum, 5 | } from 'openai' 6 | import { Configuration, OpenAIApi } from 'openai' 7 | import { encode } from 'gpt-tokenizer' 8 | import { TypeEnum, resolveRuntimeType, ReceiveType } from '@deepkit/type' 9 | import Debug from 'debug' 10 | const debug = Debug('typeai:aiClassifier') 11 | 12 | const configuration = new Configuration({ 13 | apiKey: process.env.OPENAI_API_KEY, 14 | }) 15 | const openai = new OpenAIApi(configuration) 16 | 17 | const prompt = (purpose: string, enumKeys: string[]) => { 18 | return `${purpose} 19 | The user will provide context through text, you will use your expertise 20 | to choose the best option based on it. ${ 21 | purpose !== '' ? 'The options relate to ' + purpose + '.' : '' 22 | } 23 | The options are: 24 | ${enumKeys.map((k, i) => `${i}. ${k}`).join(', ')} 25 | ` 26 | } 27 | 28 | const _infer = async (purpose: string, tStrings: string[], text: string): Promise => { 29 | // Run a completion series 30 | const messages: ChatCompletionRequestMessage[] = [ 31 | { 32 | role: ChatCompletionRequestMessageRoleEnum.System, 33 | content: prompt(purpose, tStrings), 34 | }, 35 | { 36 | role: ChatCompletionRequestMessageRoleEnum.User, 37 | content: text, 38 | }, 39 | ] 40 | const logit_bias = Object.fromEntries( 41 | [...Array(tStrings.length).keys()].map(k => [encode(String(k)), 100]), 42 | ) 43 | const request: CreateChatCompletionRequest = { 44 | model: 'gpt-3.5-turbo', 45 | messages, 46 | logit_bias, 47 | stream: false, 48 | temperature: 0, 49 | max_tokens: 1, 50 | } 51 | debug(`toAIClassifier CreateChatCompletionRequest: ${JSON.stringify(request, null, 2)}`) 52 | 53 | const response = await openai.createChatCompletion(request) 54 | debug(`toAIClassifier CreateChatCompletionResponse: ${JSON.stringify(response.data, null, 2)}`) 55 | 56 | const index = Number(response.data.choices[0].message?.content) 57 | return tStrings[index] 58 | } 59 | 60 | /** 61 | * Returns a synthesized function that classifies `text` according to the provided `enumType`. 62 | * 63 | * @example 64 | * ``` 65 | * \/** @description Classification labels for the basic type of matter, living or not *\/ 66 | * enum BasicMatterTypeEnum { 67 | * ANIMAL = 'animal', 68 | * VEGETABLE = 'vegetable', 69 | * MINERAL = 'mineral', 70 | * } 71 | * const BasicMatterType = toAIClassifier() 72 | * ``` 73 | * 74 | * @typeParam T - the type of the enum providing the classification classes 75 | * @param purposeOverride - an optional override for description of the classifier 76 | * 77 | * @returns A function with AI-backed implementation, which classifies `text` according to the provided `enumType`. 78 | */ 79 | export function toAIClassifier( 80 | purposeOverride?: string, 81 | p?: ReceiveType, 82 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 83 | ): (text: string, purposeOverride?: string) => Promise { 84 | const enumType = resolveRuntimeType(p, []) as TypeEnum 85 | type Tstring = keyof T 86 | 87 | const tKeys = Object.keys(enumType.enum as object) as Tstring[] 88 | const tStrings = tKeys as string[] 89 | debug(`toAIClassifier: enum keys: ${JSON.stringify(tStrings, null, 2)}`) 90 | debug(`toAIClassifier: description: ${JSON.stringify(enumType.description, null, 2)}`) 91 | 92 | type MagicAIClassifierFunction = { 93 | (text: string): Promise 94 | description: string 95 | } 96 | 97 | const fn = (async (text: string): Promise => { 98 | const purpose = purposeOverride || enumType.description || '' 99 | debug( 100 | `MagicAIClassifierFunction "${enumType.typeName}": purpose: ${JSON.stringify( 101 | purpose, 102 | null, 103 | 2, 104 | )}`, 105 | ) 106 | const key = await _infer(purpose, tStrings, text) 107 | const res: T = enumType.enum[key] as T 108 | return res 109 | }) 110 | fn.prototype = { description: enumType.description, name: enumType.typeName } 111 | return fn 112 | } 113 | -------------------------------------------------------------------------------- /src/aiFunction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateChatCompletionRequest, 3 | ChatCompletionRequestMessage, 4 | ChatCompletionRequestMessageRoleEnum, 5 | } from 'openai' 6 | import { Configuration, OpenAIApi } from 'openai' 7 | import { ReceiveType, ReflectionFunction, TypeObjectLiteral, Type } from '@deepkit/type' 8 | import { schemaToJSONSchema } from './utils' 9 | import { SchemaRegistry } from '../src/SchemaRegistry' 10 | import { TypeSchemaResolver } from '../src/TypeSchemaResolver' 11 | import { JSONSchema, JSONSchemaOpenAIFunction } from './types' 12 | import Debug from 'debug' 13 | import * as util from 'util' 14 | const debug = Debug('typeai') 15 | const debugNet = Debug('typeai:net') 16 | 17 | const configuration = new Configuration({ 18 | apiKey: process.env.OPENAI_API_KEY, 19 | }) 20 | const openai = new OpenAIApi(configuration) 21 | 22 | const prompt = ( 23 | purpose: string, 24 | signature: string, 25 | inputJSONSchema: Record | undefined, 26 | ) => { 27 | const schema = JSON.stringify(inputJSONSchema, null, 2) 28 | return `Your job is to generate likely outputs for a TypeScript function with the 29 | following signature and docstring: 30 | 31 | signature: ${signature} 32 | docstring: ${purpose} 33 | 34 | The user will provide function inputs (if any) and you must respond with 35 | the most likely result. The user's input data will conform to the following JSON schema: 36 | 37 | \`\`\`json 38 | ${schema} 39 | \`\`\` 40 | 41 | You must submit your result via the submitLLMGeneratedData function. 42 | ` 43 | } 44 | 45 | const _infer = async ( 46 | purpose: string, 47 | signature: string, 48 | functionJSONSchema: JSONSchemaOpenAIFunction, 49 | inputJSONSchema: Record | undefined, 50 | input: T, 51 | options?: AIFunctionOptions, 52 | rt?: ReceiveType, 53 | ): Promise => { 54 | // Run a completion series 55 | const inputJSON = JSON.stringify(input, null, 2) 56 | debug(`functionJsonSchema: ${JSON.stringify(functionJSONSchema, null, 2)}`) 57 | const messages: ChatCompletionRequestMessage[] = [ 58 | { 59 | role: ChatCompletionRequestMessageRoleEnum.System, 60 | content: prompt(purpose, signature, inputJSONSchema), 61 | }, 62 | { 63 | role: ChatCompletionRequestMessageRoleEnum.User, 64 | content: inputJSON, 65 | }, 66 | ] 67 | const request: CreateChatCompletionRequest = { 68 | model: options?.model || 'gpt-3.5-turbo', 69 | messages, 70 | functions: [functionJSONSchema], 71 | function_call: { name: 'submitLLMGeneratedData' }, 72 | stream: false, 73 | max_tokens: 1000, 74 | temperature: 0, 75 | } 76 | debugNet(`MagicAIFunction CreateChatCompletionRequest: ${JSON.stringify(request, null, 2)}`) 77 | 78 | const response = await openai.createChatCompletion(request) 79 | debugNet( 80 | `MagicAIFunction CreateChatCompletionResponse: ${JSON.stringify(response.data, null, 2)}`, 81 | ) 82 | 83 | const argsJSON = response.data.choices[0].message?.function_call?.arguments || '{}' 84 | const args = JSON.parse(argsJSON) 85 | return args?.result as R 86 | } 87 | 88 | export type ToAIFunctionOptions = { 89 | model?: string 90 | registry?: SchemaRegistry 91 | } 92 | export type AIFunctionOptions = { 93 | model?: string 94 | description?: string 95 | } 96 | 97 | /** 98 | * Returns a synthesized function that uses the OpenAI API to implement the desired behavior, with type signature matching `f`. 99 | * 100 | * @typeParam T - the input type of the generated AI function 101 | * @typeParam R - the output type of the generated AI function 102 | * @param f - a stub function with the desired type signature for the generated AI function 103 | * @param toAIFunctionOptions - options for the generated AI function 104 | * 105 | * @returns A function with AI-backed implementation, respecting the type signature of `f` 106 | * and the behavior described in the JSDoc description tag for `f`. 107 | */ 108 | export function toAIFunction( 109 | f: (input: T) => R, 110 | toAIFunctionOptions?: ToAIFunctionOptions, 111 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 112 | ): (input: T, aiFunctionOptions?: AIFunctionOptions) => Promise> { 113 | const rfn = ReflectionFunction.from(f) 114 | const tType = rfn.type.parameters[0].type 115 | const rType = rfn.type.return 116 | const options: ToAIFunctionViaRuntimeTypesOptions = { 117 | model: toAIFunctionOptions?.model, 118 | name: f.name, 119 | } 120 | const fn = toAIFunctionViaRuntimeTypes(tType, rType, options) 121 | return fn 122 | } 123 | 124 | export type ToAIFunctionViaRuntimeTypesOptions = ToAIFunctionOptions & { 125 | name?: string 126 | } 127 | 128 | export function toAIFunctionViaRuntimeTypes( 129 | iType: Type, 130 | rType: Type, 131 | options?: ToAIFunctionViaRuntimeTypesOptions, 132 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 133 | ): (input: T, aiFunctionOptions?: AIFunctionOptions) => Promise> { 134 | type RMinusVoid = Exclude 135 | 136 | const toAIFunctionOptions = options 137 | const name = options?.name || `to${rType.typeName}` 138 | const inputJSONSchema = { 139 | input: { 140 | type: 'string', 141 | } as JSONSchema, 142 | } 143 | debug(`rType: ${util.inspect(rType, { depth: 8 })}`) 144 | 145 | // Build JSON schema description of submitLLMGeneratedData 146 | const registry = options?.registry || SchemaRegistry.getInstance() 147 | const resolver = new TypeSchemaResolver(rType, registry) 148 | resolver.resolve() 149 | debug(`registry.store: ${util.inspect(registry.store, { depth: 8 })}`) 150 | const rSchemaJSON = schemaToJSONSchema(resolver.result) 151 | 152 | const rKey = registry.getTypeKey(rType) 153 | const signature = `${name}(input: string) => ${rType.typeName || rKey}` 154 | 155 | const submitGeneratedDataSchema: JSONSchemaOpenAIFunction = { 156 | name: 'submitLLMGeneratedData', 157 | parameters: { 158 | type: 'object', 159 | properties: { 160 | result: rSchemaJSON, 161 | }, 162 | required: ['result'], 163 | }, 164 | } 165 | 166 | // Magic function 167 | type MagicAIFunction = { 168 | (input: T, options?: AIFunctionOptions): Promise 169 | description: string 170 | } 171 | const fn = (async ( 172 | input: T, 173 | options?: AIFunctionOptions, 174 | ): Promise => { 175 | const aiFunctionOptions = options 176 | const _options: AIFunctionOptions = { 177 | model: aiFunctionOptions?.model || toAIFunctionOptions?.model, 178 | description: aiFunctionOptions?.description || '', 179 | } 180 | const purpose = aiFunctionOptions?.description || '' 181 | const res = await _infer( 182 | purpose, 183 | signature, 184 | submitGeneratedDataSchema, 185 | inputJSONSchema, 186 | input, 187 | _options, 188 | ) 189 | return Promise.resolve(res as RMinusVoid) 190 | }) 191 | fn.prototype = { description: (rType as TypeObjectLiteral).description } 192 | 193 | return fn 194 | } 195 | -------------------------------------------------------------------------------- /src/aiModel.ts: -------------------------------------------------------------------------------- 1 | import { ReceiveType, resolveReceiveType, typeOf } from '@deepkit/type' 2 | import { toAIFunctionViaRuntimeTypes } from '../src/aiFunction' 3 | // import Debug from 'debug' 4 | // const debug = Debug('typeai') 5 | 6 | /** 7 | * Returns a synthesized function that returns an instance of the model type provided as `R`. 8 | * 9 | * @example 10 | * ``` 11 | * \/** @description Model representing data about a biological organism *\/ 12 | * type Organisim = { 13 | * species: string & Description<'The principal natural taxonomic unit'> 14 | * genus: string & Description<'A principal taxonomic category above species and below family'> 15 | * family: string & Description<'A principal taxonomic category above genus and below order'> 16 | * commonName: string & Description<'The principal natural taxonomic unit'> 17 | * } 18 | * const organism = toAIModel() 19 | * ``` 20 | * 21 | * @typeParam R - the type of the model to be returned 22 | * 23 | * @returns A function with AI-backed implementation, which returns an instance of the model type provided as `R`, corresponding to the provided `input`. 24 | */ 25 | export function toAIModel(r?: ReceiveType): (input: string) => Promise { 26 | const iType = typeOf() 27 | const rType = resolveReceiveType(r) 28 | const fn = toAIFunctionViaRuntimeTypes(iType, rType) 29 | return fn 30 | } 31 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { stringifyType, Type } from '@deepkit/type' 2 | 3 | export class TypeAiError extends Error {} 4 | 5 | export class DeepKitTypeError extends TypeAiError {} 6 | 7 | export class TypeNotSupported extends DeepKitTypeError { 8 | constructor( 9 | public type: Type, 10 | public reason: string = '', 11 | ) { 12 | super(`${stringifyType(type)} is not supported. ${reason}`) 13 | } 14 | } 15 | 16 | export class LiteralSupported extends DeepKitTypeError { 17 | constructor(public typeName: string) { 18 | super(`${typeName} is not supported. `) 19 | } 20 | } 21 | 22 | export class DeepKitTypeErrors extends TypeAiError { 23 | constructor( 24 | public errors: DeepKitTypeError[], 25 | message: string, 26 | ) { 27 | super(message) 28 | } 29 | } 30 | 31 | export class TypeAiSchemaNameConflict extends TypeAiError { 32 | constructor( 33 | public newType: Type, 34 | public oldType: Type, 35 | public name: string, 36 | ) { 37 | super( 38 | `${stringifyType(newType)} and ${stringifyType( 39 | oldType, 40 | )} are not the same, but their schema are both named as ${JSON.stringify(name)}. ` + 41 | `Try to fix the naming of related types, or rename them using 'YourClass & Name'`, 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { toAIModel } from './aiModel' 2 | import { toAIFunction } from './aiFunction' 3 | import { toAIClassifier } from './aiClassifier' 4 | import { ToolFunction, handleToolUse } from './ToolFunction' 5 | import { SchemaRegistry } from './SchemaRegistry' 6 | import { TypeSchemaResolver } from './TypeSchemaResolver' 7 | import type { Description } from './types' 8 | 9 | export { 10 | toAIModel, 11 | toAIFunction, 12 | toAIClassifier, 13 | ToolFunction, 14 | handleToolUse, 15 | SchemaRegistry, 16 | TypeSchemaResolver, 17 | Description, 18 | } 19 | -------------------------------------------------------------------------------- /src/tools/web.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios' 2 | import { JSDOM } from 'jsdom' 3 | import { ToolFunction } from '../ToolFunction' 4 | import { truncateByTokens } from '../utils' 5 | import Debug from 'debug' 6 | const debug = Debug('typeai') 7 | const debugNet = Debug('typeai:net') 8 | 9 | // https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/quickstarts/rest/nodejs#example-json-response 10 | // https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/reference/response-objects 11 | type BingWebSearchResponseAbridged = { 12 | _type: string 13 | webPages?: { 14 | totalEstimatedMatches: number 15 | value: [ 16 | { 17 | name: string 18 | url: string 19 | snippet: string 20 | }, 21 | ] 22 | } 23 | } 24 | type BingWebSearchErrorResponse = { 25 | _type: string 26 | errors: [ 27 | { 28 | code: string 29 | message: string 30 | moreDetails: string 31 | parameter: string 32 | subCode: string 33 | value: string 34 | }, 35 | ] 36 | } 37 | 38 | /** @description Provides relevant web search results to well-constructed natural language queries */ 39 | async function searchWeb(query: string): Promise { 40 | debug(`searchWeb: ${query}`) 41 | const url = 42 | `https://api.bing.microsoft.com/v7.0/search?responseFilter=Webpages&count=3&q=` + 43 | encodeURIComponent(query) 44 | let result 45 | try { 46 | const response = await axios.get(url, { 47 | headers: { 'Ocp-Apim-Subscription-Key': process.env.BING_API_KEY }, 48 | responseType: 'json', 49 | }) 50 | switch (response.data._type) { 51 | case 'SearchResponse': 52 | result = response.data as BingWebSearchResponseAbridged 53 | break 54 | case 'ErrorResponse': 55 | result = response.data as BingWebSearchErrorResponse 56 | break 57 | default: 58 | result = { _type: 'UnknownResponse' } 59 | } 60 | } catch (e) { 61 | if (axios.isAxiosError(e)) { 62 | const error = e as AxiosError 63 | debug(`FetchUrl: error: ${JSON.stringify(error.stack)}`) 64 | result = { _type: 'AxiosErrorResponse' } 65 | } 66 | console.error(e) 67 | result = { _type: 'FailedToLoadURL' } 68 | } 69 | debugNet(`searchWeb: response: ${JSON.stringify(result, null, 2)}`) 70 | return result 71 | } 72 | 73 | /** @description Fetch a valid URL and return its contents */ 74 | async function fetchUrl(url: string): Promise { 75 | try { 76 | const response = await axios.get(url) 77 | const contentType = response.headers['content-type'] 78 | 79 | let content 80 | if (contentType?.includes('application/json')) { 81 | content = JSON.parse(response.data) 82 | } else if (contentType?.includes('text/html')) { 83 | const dom = new JSDOM(response.data) 84 | content = dom.window.document.textContent 85 | } else if (contentType?.includes('text/plain')) { 86 | content = response.data 87 | } 88 | debugNet(`FetchUrl: content: ${JSON.stringify(content)}`) 89 | content = truncateByTokens(content, 2000) 90 | return content 91 | } catch (e) { 92 | if (axios.isAxiosError(e)) { 93 | const error = e as AxiosError 94 | debug(`FetchUrl: error: ${JSON.stringify(error.stack)}`) 95 | return 'Failed to load URL: Connection timed out' 96 | } 97 | console.error(e) 98 | return 'Failed to load URL' 99 | } 100 | } 101 | 102 | export const Tools = new Map() 103 | export const registerTools = () => { 104 | Tools.forEach((v, k) => { 105 | Tools.delete(k) 106 | }) 107 | Tools.set('SearchWeb', ToolFunction.from(searchWeb)) 108 | Tools.set('FetchUrl', ToolFunction.from(fetchUrl)) 109 | return Tools 110 | } 111 | registerTools() 112 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Type, TypeLiteral, metaAnnotation } from '@deepkit/type' 2 | export type SimpleType = string | number | boolean | null | bigint 3 | 4 | export type Schema = { 5 | __type: 'schema' 6 | __registryKey?: string 7 | __isComponent?: boolean 8 | __isUndefined?: boolean 9 | description?: string 10 | type?: string 11 | not?: Schema 12 | pattern?: string 13 | multipleOf?: number 14 | minLength?: number 15 | maxLength?: number 16 | minimum?: number | bigint 17 | exclusiveMinimum?: number | bigint 18 | maximum?: number | bigint 19 | exclusiveMaximum?: number | bigint 20 | enum?: SimpleType[] 21 | properties?: Record 22 | required?: string[] 23 | items?: Schema 24 | default?: any 25 | oneOf?: Schema[] 26 | 27 | $ref?: string 28 | } 29 | export const AnySchema: Schema = { __type: 'schema' } 30 | 31 | // --- 32 | 33 | // OpenAI claims to use JSON Schema 2020-12 / Draft 8 patch 1 34 | // - https://community.openai.com/t/whitch-json-schema-version-should-function-calling-use/283535 35 | // - https://json-schema.org/specification-links.html#2020-12 36 | export type JSONSchemaEnum = string | number | boolean | bigint | null 37 | export type JSONSchemaTypeString = 38 | | 'object' 39 | | 'array' 40 | | 'string' 41 | | 'number' 42 | | 'integer' 43 | | 'boolean' 44 | | 'null' 45 | export type JSONSchema = { 46 | type?: JSONSchemaTypeString 47 | description?: string 48 | properties?: Record 49 | items?: JSONSchema 50 | enum?: JSONSchemaEnum[] 51 | required?: string[] 52 | $ref?: string 53 | $defs?: Record 54 | } 55 | export type JSONSchemaOpenAIFunction = { 56 | name: string 57 | description?: string 58 | parameters?: JSONSchema 59 | } 60 | export type Description = { __meta?: ['metaDescription', T] } 61 | export const getMetaDescription = (t: Type) => 62 | (metaAnnotation.getForName(t, 'metaDescription')?.[0] as TypeLiteral)?.literal as string 63 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode } from 'gpt-tokenizer' 2 | import { SchemaRegistry, SchemeEntry } from './SchemaRegistry' 3 | import cloneDeepWith from 'lodash/cloneDeepWith' 4 | import { JSONSchema, JSONSchemaTypeString, JSONSchemaEnum, Schema } from './types' 5 | // import * as util from 'util' 6 | // import Debug from 'debug' 7 | // const debug = Debug('typeai') 8 | 9 | export const truncateByTokens = (text: string, maxTokens: number): string => { 10 | return decode(encode(text).slice(0, maxTokens)) 11 | } 12 | 13 | export function schemaToJSONSchema(schema: Schema): JSONSchema { 14 | const jsonSchema: JSONSchema = { 15 | type: (schema.type as JSONSchemaTypeString) || 'null', 16 | } 17 | // new 18 | if (schema.type === 'array') { 19 | if (schema.items) { 20 | jsonSchema.items = schemaToJSONSchema(schema.items) 21 | } 22 | } else if (schema.properties) { 23 | jsonSchema.properties = {} 24 | for (const [key, property] of Object.entries(schema.properties)) { 25 | jsonSchema.properties[key] = schemaToJSONSchema(property) 26 | } 27 | } 28 | if (schema.required) { 29 | jsonSchema.required = schema.required 30 | } 31 | jsonSchema.description = schema.description 32 | jsonSchema.enum = (schema.enum as JSONSchemaEnum[]) || undefined 33 | return jsonSchema 34 | } 35 | 36 | export function getSchema(registry: SchemaRegistry, se: SchemeEntry): JSONSchema { 37 | // console.log('SchemeEntry: ', util.inspect(se, { depth: 8 })) 38 | const s: JSONSchema = { 39 | type: (se.schema.type as JSONSchemaTypeString) || 'object', 40 | required: se.schema.required || [], 41 | } 42 | s.properties = {} 43 | for (const [key, subSchema] of Object.entries(se.schema.properties || {})) { 44 | s.properties[key] = { 45 | ...schemaToJSONSchema(subSchema || {}), 46 | } 47 | } 48 | return s 49 | } 50 | 51 | export function serialize(schema: JSONSchema): JSONSchema { 52 | // console.log('schema: ', util.inspect(schema, { depth: 8 })) 53 | return cloneDeepWith(schema, (c: any) => { 54 | if (typeof c === 'function') { 55 | if (c.__type === 'schema' && c.__registryKey && !c.__isComponent) { 56 | return { 57 | $ref: `#/components/schemas/${c.__registryKey}`, 58 | } 59 | } 60 | 61 | for (const key of Object.keys(c)) { 62 | // Remove internal keys. 63 | if (key.startsWith('__')) delete c[key] 64 | } 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /test/ToolFunction.test.ts: -------------------------------------------------------------------------------- 1 | import { ToolFunction, handleToolUse } from '../src/ToolFunction' 2 | import { Tools, registerTools } from '../src/tools/web' 3 | import { 4 | CreateChatCompletionRequest, 5 | CreateChatCompletionResponse, 6 | ChatCompletionRequestMessage, 7 | ChatCompletionRequestMessageRoleEnum, 8 | } from 'openai' 9 | import { Configuration, OpenAIApi } from 'openai' 10 | 11 | import Debug from 'debug' 12 | import { SchemaRegistry } from '../src/SchemaRegistry' 13 | const debug = Debug('test') 14 | 15 | enum AircraftType { 16 | FixedWing = 'fixed-wing', 17 | RotaryWing = 'rotary-wing', 18 | } 19 | enum AircraftApplication { 20 | Military = 'military', 21 | Civilian = 'civilian', 22 | } 23 | type Aircraft = { 24 | manufacturer: string 25 | type: AircraftType 26 | application: AircraftApplication 27 | } 28 | 29 | let openai: OpenAIApi 30 | describe('Tools', () => { 31 | beforeAll(() => { 32 | const configuration = new Configuration({ 33 | apiKey: process.env.OPENAI_API_KEY, 34 | }) 35 | openai = new OpenAIApi(configuration) 36 | }) 37 | 38 | test('ToolFunction.handleToolUse should handle multiple different function invocations by the LLM', async () => { 39 | let aircraftResponse: Aircraft 40 | 41 | /** Submits generated data regarding aircraft */ 42 | const submitAircraftSpec = function submitAircraft(aircraft: Aircraft): string { 43 | aircraftResponse = aircraft 44 | debug(`aircraft: ${JSON.stringify(aircraft, null, 2)}`) 45 | return '{ "status": "ok" }' 46 | } 47 | 48 | // Build JSON schema description of the test function 49 | const submitAircraftTool = ToolFunction.from(submitAircraftSpec) 50 | 51 | // Run a completion series 52 | const messages: ChatCompletionRequestMessage[] = [ 53 | { 54 | role: ChatCompletionRequestMessageRoleEnum.System, 55 | content: 56 | "You may use the provided functions to generate up-to-date responses. Your final answer to the user's request MUST be submitted via the submitAircraft function. Don't provide a narrative responseData.", 57 | }, 58 | { 59 | role: ChatCompletionRequestMessageRoleEnum.User, 60 | content: 'Give me data on the F-35', 61 | }, 62 | ] 63 | const request: CreateChatCompletionRequest = { 64 | model: 'gpt-4', 65 | messages, 66 | functions: [Tools.get('SearchWeb')!.schema, submitAircraftTool.schema], 67 | function_call: 'auto', 68 | stream: false, 69 | max_tokens: 1000, 70 | } 71 | let response: CreateChatCompletionResponse 72 | try { 73 | ;({ data: response } = await openai.createChatCompletion(request)) 74 | } catch (e) { 75 | debug(e) 76 | debug(`responseWithFnUse: ${JSON.stringify(response!, null, 2)}`) 77 | throw e 78 | } 79 | 80 | // Handle function use by the LLM 81 | const responseData = await handleToolUse(openai, request, response) 82 | const result = responseData?.choices[0].message 83 | 84 | expect(aircraftResponse!).toEqual({ 85 | manufacturer: 'Lockheed Martin', 86 | type: AircraftType.FixedWing, 87 | application: AircraftApplication.Military, 88 | }) 89 | debug(`responseData: ${JSON.stringify(responseData, null, 2)}`) 90 | debug(`Final result: ${JSON.stringify(result, null, 2)}`) 91 | SchemaRegistry.resetInstance() 92 | registerTools() 93 | }, 60000) 94 | 95 | test('modelSubmissionToolFor generates ToolFunctions that accept the given model', async () => { 96 | let aircraftResponse: Aircraft 97 | const cb = async (aircraft: Aircraft) => { 98 | aircraftResponse = aircraft 99 | } 100 | const submitAircraftTool = ToolFunction.modelSubmissionToolFor(cb) 101 | 102 | // Run a completion series 103 | const messages: ChatCompletionRequestMessage[] = [ 104 | { 105 | role: ChatCompletionRequestMessageRoleEnum.System, 106 | content: 107 | "You may use the provided functions to generate up-to-date responses. Your final answer to the user's request MUST be submitted via the submitAircraft function. Don't provide a narrative responseData.", 108 | }, 109 | { 110 | role: ChatCompletionRequestMessageRoleEnum.User, 111 | content: 'Give me data on the F-35', 112 | }, 113 | ] 114 | const request: CreateChatCompletionRequest = { 115 | model: 'gpt-4', 116 | messages, 117 | functions: [Tools.get('SearchWeb')!.schema, submitAircraftTool.schema], 118 | function_call: 'auto', 119 | stream: false, 120 | max_tokens: 1000, 121 | } 122 | let responseWithFnUse 123 | try { 124 | responseWithFnUse = await openai.createChatCompletion(request) 125 | } catch (e) { 126 | debug(e) 127 | debug(`responseWithFnUse: ${JSON.stringify(responseWithFnUse?.data, null, 2)}`) 128 | throw e 129 | } 130 | 131 | // Handle function use by the LLM 132 | const responseData = await handleToolUse(openai, request, responseWithFnUse.data) 133 | const result = responseData?.choices[0].message 134 | 135 | expect(aircraftResponse!).toEqual({ 136 | manufacturer: 'Lockheed Martin', 137 | type: AircraftType.FixedWing, 138 | application: AircraftApplication.Military, 139 | }) 140 | debug(`responseData: ${JSON.stringify(responseData, null, 2)}`) 141 | debug(`Final result: ${JSON.stringify(result, null, 2)}`) 142 | }, 60000) 143 | }) 144 | -------------------------------------------------------------------------------- /test/TypeSchemaResolver.test.ts: -------------------------------------------------------------------------------- 1 | import { typeOf } from '@deepkit/type' 2 | import { unwrapTypeSchema } from '../src/TypeSchemaResolver' 3 | 4 | test('serialize atomic types', () => { 5 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 6 | __type: 'schema', 7 | type: 'string', 8 | }) 9 | 10 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 11 | __type: 'schema', 12 | type: 'number', 13 | }) 14 | 15 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 16 | __type: 'schema', 17 | type: 'number', 18 | }) 19 | 20 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 21 | __type: 'schema', 22 | type: 'boolean', 23 | }) 24 | 25 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 26 | __type: 'schema', 27 | type: 'null', 28 | }) 29 | }) 30 | 31 | test('serialize enum', () => { 32 | enum E1 { 33 | a = 'a', 34 | b = 'b', 35 | } 36 | 37 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 38 | __type: 'schema', 39 | type: 'string', 40 | enum: ['a', 'b'], 41 | __registryKey: 'E1', 42 | }) 43 | 44 | enum E2 { 45 | a = 1, 46 | b = 2, 47 | } 48 | 49 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 50 | __type: 'schema', 51 | type: 'number', 52 | enum: [1, 2], 53 | __registryKey: 'E2', 54 | }) 55 | }) 56 | 57 | test('serialize union', () => { 58 | type Union = 59 | | { 60 | type: 'push' 61 | branch: string 62 | } 63 | | { 64 | type: 'commit' 65 | diff: string[] 66 | } 67 | 68 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 69 | __type: 'schema', 70 | oneOf: [ 71 | { 72 | __type: 'schema', 73 | type: 'object', 74 | properties: { 75 | type: { __type: 'schema', type: 'string', enum: ['push'] }, 76 | branch: { __type: 'schema', type: 'string' }, 77 | }, 78 | required: ['type', 'branch'], 79 | }, 80 | { 81 | __type: 'schema', 82 | type: 'object', 83 | properties: { 84 | type: { __type: 'schema', type: 'string', enum: ['commit'] }, 85 | diff: { 86 | __type: 'schema', 87 | type: 'array', 88 | items: { __type: 'schema', type: 'string' }, 89 | }, 90 | }, 91 | required: ['type', 'diff'], 92 | }, 93 | ], 94 | }) 95 | 96 | type EnumLike = 'red' | 'black' 97 | 98 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 99 | __type: 'schema', 100 | type: 'string', 101 | enum: ['red', 'black'], 102 | __registryKey: 'EnumLike', 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/aiClassifier.test.ts: -------------------------------------------------------------------------------- 1 | import { toAIClassifier } from '../src/aiClassifier' 2 | import Debug from 'debug' 3 | const debug = Debug('test') 4 | 5 | describe('Build a magic AI classifier from an enum type', () => { 6 | test('it should infer the correct enum value', async () => { 7 | /** @description Customer web application routes */ 8 | enum AppRouteEnum { 9 | USER_PROFILE = '/user-profile', 10 | SEARCH = '/search', 11 | NOTIFICATIONS = '/notifications', 12 | SETTINGS = '/settings', 13 | HELP = '/help', 14 | SUPPORT_CHAT = '/support-chat', 15 | DOCS = '/docs', 16 | PROJECTS = '/projects', 17 | WORKSPACES = '/workspaces', 18 | } 19 | const AppRoute = toAIClassifier() 20 | 21 | let appRouteRes: AppRouteEnum 22 | appRouteRes = await AppRoute('I need to talk to somebody about billing') 23 | debug( 24 | `Classification: AppRouteEnum:${appRouteRes} text:"I need to talk to somebody about billing"`, 25 | ) 26 | expect(appRouteRes).toEqual(AppRouteEnum.SUPPORT_CHAT) 27 | 28 | appRouteRes = await AppRoute('I want to update my password') 29 | debug(`Classification: AppRouteEnum:${appRouteRes} text:"I want to update my password"`) 30 | expect(appRouteRes).toEqual(AppRouteEnum.SETTINGS) 31 | 32 | expect(AppRoute.prototype.description).toEqual('Customer web application routes') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/aiFunction.test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaRegistry } from '../src/SchemaRegistry' 2 | import { toAIFunction } from '../src/aiFunction' 3 | import Debug from 'debug' 4 | const debug = Debug('test') 5 | 6 | describe('Build a magic AI function from a function stub', () => { 7 | test('it should work with primitive-typed single-parameter functions', async () => { 8 | /** @description Given `text`, returns a number between 1 (positive) and -1 (negative) indicating its sentiment score. */ 9 | function sentimentSpec(text: string): number | void {} 10 | 11 | const sentiment = toAIFunction(sentimentSpec) 12 | 13 | const score1 = await sentiment('That was surprisingly easy!') 14 | debug(`sentiment: score:${score1} text:"That was surprisingly easy!"`) 15 | expect(score1).toBeGreaterThan(0.5) 16 | 17 | const score2 = await sentiment("I can't stand that movie.") 18 | debug(`sentiment: score:${score2} text:"I can't stand that movie."`) 19 | expect(score2).toBeLessThan(-0.5) 20 | SchemaRegistry.resetInstance() 21 | }, 20000) 22 | 23 | test('it should work with object-typed single-parameter functions', async () => { 24 | /** @description Returns a list of `n` different fruits that all have the provided `color` */ 25 | function listFruitSpec(input: { n: number; color: string }): string[] | void {} 26 | 27 | const listFruit = toAIFunction(listFruitSpec) 28 | 29 | const fruits = await listFruit({ n: 5, color: 'yellow' }) 30 | const potentialYellowFruits = ['banana', 'lemon', 'pineapple', 'pear', 'apple', 'mango'] 31 | const f = fruits as string[] 32 | debug(`listFruit: n:5 color:yellow results:${fruits}`) 33 | expect(f.length).toEqual(5) 34 | expect(f.every(f => potentialYellowFruits.includes(f))).toEqual(true) 35 | SchemaRegistry.resetInstance() 36 | }, 20000) 37 | 38 | test('it should work with functions that return object-typed values', async () => { 39 | type WordSynonyms = { 40 | word: string 41 | synonyms: string[] 42 | } 43 | 44 | /** @description Returns a list of WordSynonym objects, one for each word passed in. */ 45 | function generateSynonymsForWordsSpec(input: string[]): WordSynonyms[] | void {} 46 | 47 | const generateSynonymsForWords = toAIFunction(generateSynonymsForWordsSpec) 48 | 49 | const synonyms = await generateSynonymsForWords(['clear', 'yellow', 'tasty', 'changable']) 50 | debug(`synonyms: ${JSON.stringify(synonyms, null, 2)}`) 51 | expect(synonyms.length).toEqual(4) 52 | SchemaRegistry.resetInstance() 53 | }, 20000) 54 | 55 | test('it should work with functions that return object-typed values', async () => { 56 | type Organism = { 57 | species: string 58 | genus: string 59 | family: string 60 | commonName: string 61 | } 62 | 63 | /** @description Returns an Organism object with the botanical classification corresponding to the input. */ 64 | function getOrganismInfoSpec(input: string): Organism | void {} 65 | 66 | const getOrganismInfo = toAIFunction(getOrganismInfoSpec) 67 | 68 | const organism = await getOrganismInfo('the plant that produces espresso beans') 69 | debug(`organism: ${JSON.stringify(organism, null, 2)}`) 70 | SchemaRegistry.resetInstance() 71 | }, 20000) 72 | }) 73 | -------------------------------------------------------------------------------- /test/aiFunctionEHRExample.test.ts: -------------------------------------------------------------------------------- 1 | import { toAIFunction } from '../src/aiFunction' 2 | import Debug from 'debug' 3 | const debug = Debug('test') 4 | 5 | type Patient = { 6 | name: string 7 | age: number 8 | isSmoker: boolean 9 | } 10 | type Diagnosis = { 11 | condition: string 12 | diagnosisDate: Date 13 | stage?: string 14 | type?: string 15 | histology?: string 16 | complications?: string 17 | } 18 | type Treatment = { 19 | name: string 20 | startDate: Date 21 | endDate?: Date 22 | } 23 | type Medication = Treatment & { 24 | dose?: string 25 | } 26 | type BloodTest = { 27 | name: string 28 | result: string 29 | testDate: Date 30 | } 31 | type PatientData = { 32 | patient: Patient 33 | diagnoses: Diagnosis[] 34 | treatments: Treatment | Medication[] 35 | bloodTests: BloodTest[] 36 | } 37 | 38 | describe('Build a magic AI function from a function stub', () => { 39 | test('it should work with functions that return object-typed values', async () => { 40 | /** @description Returns a PatientData record generate from the content of doctorsNotes notes. */ 41 | function generateElectronicHealthRecordSpec(input: string): PatientData | void {} 42 | 43 | const generateElectronicHealthRecord = toAIFunction(generateElectronicHealthRecordSpec, { 44 | model: 'gpt-4', 45 | }) 46 | 47 | const notes = ` 48 | Ms. Lee, a 45-year-old patient, was diagnosed with type 2 diabetes mellitus on 06-01-2018. 49 | Unfortunately, Ms. Lee's diabetes has progressed and she developed diabetic retinopathy on 09-01-2019. 50 | Ms. Lee was diagnosed with type 2 diabetes mellitus on 06-01-2018. 51 | Ms. Lee was initially diagnosed with stage I hypertension on 06-01-2018. 52 | Ms. Lee's blood work revealed hyperlipidemia with elevated LDL levels on 06-01-2018. 53 | Ms. Lee was prescribed metformin 1000 mg daily for her diabetes on 06-01-2018. 54 | Ms. Lee's most recent A1C level was 8.5% on 06-15-2020. 55 | Ms. Lee was diagnosed with type 2 diabetes mellitus, with microvascular complications, including diabetic retinopathy, on 09-01-2019. 56 | Ms. Lee's blood pressure remains elevated and she was prescribed lisinopril 10 mg daily on 09-01-2019. 57 | Ms. Lee's most recent lipid panel showed elevated LDL levels, and she was prescribed atorvastatin 40 mg daily on 09-01-2019. 58 | ` 59 | const ehr = await generateElectronicHealthRecord(notes) 60 | debug(`EHR: ${JSON.stringify(ehr, null, 2)}`) 61 | }, 120000) 62 | }) 63 | -------------------------------------------------------------------------------- /test/aiModel.test.ts: -------------------------------------------------------------------------------- 1 | import { toAIModel } from '../src/aiModel' 2 | import { Description } from '../src/types' 3 | import Debug from 'debug' 4 | const debug = Debug('test') 5 | 6 | describe('Build a magic AI model from a function stub', () => { 7 | test('it should work', async () => { 8 | type Organisim = { 9 | species: string & Description<'The principal natural taxonomic unit'> 10 | genus: string & Description<'A principal taxonomic category above species and below family'> 11 | family: string & Description<'A principal taxonomic category above genus and below order'> 12 | commonName: string & Description<'The principal natural taxonomic unit'> 13 | } 14 | const organism = toAIModel() 15 | 16 | const organism1 = await organism('the plant that produces espresso beans') 17 | debug(`organism: ${JSON.stringify(organism1, null, 2)}`) 18 | expect(organism1).toEqual({ 19 | species: 'Coffea arabica', 20 | genus: 'Coffea', 21 | family: 'Rubiaceae', 22 | commonName: 'Coffee', 23 | }) 24 | }) 25 | 26 | test('it should work', async () => { 27 | type Location = { 28 | city: string 29 | stateIso2: string 30 | } 31 | const location = toAIModel() 32 | 33 | const location1 = await location('The Big Apple') 34 | debug(`location: ${JSON.stringify(location1, null, 2)}`) 35 | expect(location1).toEqual({ city: 'New York', stateIso2: 'NY' }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/annotations.test.ts: -------------------------------------------------------------------------------- 1 | import { TypeObjectLiteral, TypePropertySignature, typeOf } from '@deepkit/type' 2 | import { Description, getMetaDescription } from '../src/types' 3 | import { ToolFunction } from '../src/ToolFunction' 4 | import * as util from 'util' 5 | import Debug from 'debug' 6 | const debug = Debug('test') 7 | 8 | describe('Descriptions via annotation types', () => { 9 | test('exit on fields', async () => { 10 | type Organism = { 11 | species: string & Description<'test'> 12 | genus: string 13 | family: string 14 | commonName: string 15 | } 16 | 17 | const a = typeOf() 18 | const tSpecies = ((a as TypeObjectLiteral).types?.[0] as TypePropertySignature) 19 | .type as TypePropertySignature 20 | debug(`tSpecies: ${util.inspect(tSpecies, { depth: 6 })}`) 21 | const md = getMetaDescription(tSpecies) 22 | 23 | expect(md).toEqual('test') 24 | }, 20000) 25 | 26 | test('exist in the serialized JSON schema', async () => { 27 | type Organism = { 28 | species: string & Description<'desc1'> 29 | genus: string & Description<'desc2'> 30 | family: string & Description<'desc3'> 31 | commonName: string & Description<'desc4'> 32 | } 33 | /** @description Accepts an Organism object with the botanical classification corresponding to the input. */ 34 | function inferredOrganismSpec(organism: Organism): string { 35 | return 'OK' 36 | } 37 | 38 | const inferredOrganismTool = ToolFunction.from(inferredOrganismSpec) 39 | const jsonSchema = inferredOrganismTool.schema 40 | const organismProps = jsonSchema.parameters?.$defs?.Organism?.properties 41 | expect(organismProps?.species?.description).toEqual('desc1') 42 | expect(organismProps?.genus?.description).toEqual('desc2') 43 | expect(organismProps?.family?.description).toEqual('desc3') 44 | expect(organismProps?.commonName?.description).toEqual('desc4') 45 | }, 20000) 46 | }) 47 | -------------------------------------------------------------------------------- /test/builder.test.ts: -------------------------------------------------------------------------------- 1 | import { ReflectionFunction } from '@deepkit/type' 2 | import { SchemaRegistry } from '../src/SchemaRegistry' 3 | import { TypeSchemaResolver } from '../src/TypeSchemaResolver' 4 | import { ToolFunction } from '../src/ToolFunction' 5 | import Debug from 'debug' 6 | const debug = Debug('test') 7 | 8 | describe('Build JSON schema description of a TypeScript function', () => { 9 | // type Aircraft = { 10 | // manufacturer: string 11 | // type: 'fixed-wing' | 'rotary-wing' 12 | // application: 'military' | 'civilian' 13 | // } 14 | 15 | test('it should generate a correct description', async () => { 16 | // https://json-schema.org/specification-links.html#2020-12 17 | 18 | /** @description about the temp */ 19 | type TemperatureUnit = 'celsius' | 'fahrenheit' 20 | 21 | /** @description Info about the weather */ 22 | type WeatherInfo = { 23 | location: string 24 | /** @description temp2 */ 25 | temperature: number 26 | unit: TemperatureUnit 27 | forecast: string[] 28 | precipitationPct?: number 29 | pressureMmHg?: number 30 | } 31 | 32 | /** @description Options related to weather info */ 33 | type WeatherOptions = { 34 | flags?: { 35 | includePrecipitation?: boolean 36 | includePressure?: boolean 37 | } 38 | highPriority?: boolean 39 | } 40 | const getCurrentWeather = function getCurrentWeather( 41 | location: string, 42 | unit: TemperatureUnit = 'fahrenheit', 43 | options?: WeatherOptions, 44 | ): WeatherInfo { 45 | const weatherInfo: WeatherInfo = { 46 | location: location, 47 | temperature: 82, 48 | unit: unit, 49 | precipitationPct: options?.flags?.includePrecipitation ? 25 : undefined, 50 | pressureMmHg: options?.flags?.includePressure ? 25 : undefined, 51 | forecast: ['sunny', 'cloudy'], 52 | } 53 | return weatherInfo 54 | } 55 | 56 | const registry = new SchemaRegistry() 57 | const rfn = ReflectionFunction.from(getCurrentWeather) 58 | const tsr = new TypeSchemaResolver(rfn.type, registry) 59 | tsr.resolve() 60 | const oaif = new ToolFunction(getCurrentWeather, registry) 61 | const doc = oaif.serialize() 62 | debug(JSON.stringify(doc, null, 2)) 63 | 64 | expect(doc).toEqual({ 65 | name: 'getCurrentWeather', 66 | parameters: { 67 | type: 'object', 68 | properties: { 69 | location: { 70 | type: 'string', 71 | }, 72 | unit: { 73 | $ref: '#/$defs/TemperatureUnit', 74 | }, 75 | options: { 76 | $ref: '#/$defs/WeatherOptions', 77 | }, 78 | }, 79 | required: ['location', 'options'], 80 | $defs: { 81 | TemperatureUnit: { 82 | type: 'string', 83 | enum: ['celsius', 'fahrenheit'], 84 | }, 85 | WeatherOptions: { 86 | type: 'object', 87 | properties: { 88 | flags: { 89 | type: 'object', 90 | properties: { 91 | includePrecipitation: { 92 | type: 'boolean', 93 | }, 94 | includePressure: { 95 | type: 'boolean', 96 | }, 97 | }, 98 | }, 99 | highPriority: { 100 | type: 'boolean', 101 | }, 102 | }, 103 | description: 'Options related to weather info', 104 | }, 105 | }, 106 | }, 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/chatCompletionFlow.test.ts: -------------------------------------------------------------------------------- 1 | import { handleToolUse } from '../src/ToolFunction' 2 | import { ToolFunction } from '../src/ToolFunction' 3 | import { 4 | CreateChatCompletionRequest, 5 | ChatCompletionRequestMessage, 6 | ChatCompletionRequestMessageRoleEnum, 7 | } from 'openai' 8 | import { Configuration, OpenAIApi } from 'openai' 9 | import util from 'util' 10 | import Debug from 'debug' 11 | const debug = Debug('test') 12 | 13 | describe('Perform a round trip test with the OpenAI API', () => { 14 | // Set up test function and types 15 | type TemperatureUnit = 'celsius' | 'fahrenheit' 16 | type WeatherInfo = { 17 | location: string 18 | temperature: number 19 | unit: TemperatureUnit 20 | forecast: string[] 21 | precipitationPct?: number 22 | pressureMmHg?: number 23 | } 24 | type WeatherOptions = { 25 | flags?: { 26 | includePrecipitation?: boolean 27 | includePressure?: boolean 28 | } 29 | highPriority?: boolean 30 | } 31 | const getCurrentWeather = function getCurrentWeather( 32 | location: string, 33 | unit: TemperatureUnit = 'fahrenheit', 34 | options?: WeatherOptions, 35 | ): WeatherInfo { 36 | const weatherInfo: WeatherInfo = { 37 | location: location, 38 | temperature: 82, 39 | unit: unit, 40 | precipitationPct: options?.flags?.includePrecipitation ? 25 : undefined, 41 | pressureMmHg: options?.flags?.includePressure ? 25 : undefined, 42 | forecast: ['sunny', 'cloudy'], 43 | } 44 | return weatherInfo 45 | } 46 | 47 | const configuration = new Configuration({ 48 | apiKey: process.env.OPENAI_API_KEY, 49 | }) 50 | const openai = new OpenAIApi(configuration) 51 | 52 | test('it should work', async () => { 53 | // Build JSON schema description of the test function 54 | const getCurrentWeatherTool = ToolFunction.from(getCurrentWeather) 55 | const jsonSchemaGetCurrentWeather = getCurrentWeatherTool.schema 56 | const functionMap = { 57 | getCurrentWeather: getCurrentWeather, 58 | } 59 | debug(util.inspect(jsonSchemaGetCurrentWeather, { depth: 6 })) 60 | 61 | // Perform a round trip test with the OpenAI API 62 | const messages: ChatCompletionRequestMessage[] = [ 63 | { 64 | role: ChatCompletionRequestMessageRoleEnum.User, 65 | content: "What's the weather like in Boston? Say it like a weather reporter.", 66 | }, 67 | ] 68 | const request: CreateChatCompletionRequest = { 69 | model: 'gpt-3.5-turbo-0613', 70 | messages, 71 | functions: [jsonSchemaGetCurrentWeather], 72 | stream: false, 73 | max_tokens: 1000, 74 | } 75 | debug(JSON.stringify(request, null, 2)) 76 | 77 | const response = await openai.createChatCompletion(request) 78 | const message = response.data.choices[0].message 79 | if (message?.function_call) { 80 | debug(`function_call: ${JSON.stringify(message.function_call, null, 2)}`) 81 | const function_name = message.function_call.name 82 | const function_args = JSON.parse(message.function_call.arguments || '') 83 | const function_response = functionMap['getCurrentWeather']( 84 | function_args?.location, 85 | function_args?.unit, 86 | function_args?.options, 87 | ) 88 | debug(`function_response: ${JSON.stringify(function_response, null, 2)}`) 89 | 90 | messages.push(message) // extend conversation with assistant's reply 91 | messages.push({ 92 | role: ChatCompletionRequestMessageRoleEnum.Function, 93 | name: function_name, 94 | content: JSON.stringify(function_response), 95 | }) 96 | debug(JSON.stringify(messages, null, 2)) 97 | const second_response = await openai.createChatCompletion({ 98 | model: 'gpt-3.5-turbo-0613', 99 | messages, 100 | }) 101 | const second_message = second_response.data.choices[0].message 102 | debug(`second_response: ${JSON.stringify(second_message, null, 2)}`) 103 | } 104 | expect(1).toEqual(1) 105 | }, 30000) 106 | 107 | test('it should work using handleToolUse', async () => { 108 | // Build JSON schema description of the test function 109 | const getCurrentWeatherTool = ToolFunction.from(getCurrentWeather) 110 | const { registry, schema: jsonSchemaGetCurrentWeather } = getCurrentWeatherTool 111 | 112 | // Run a completion series 113 | const messages: ChatCompletionRequestMessage[] = [ 114 | { 115 | role: ChatCompletionRequestMessageRoleEnum.User, 116 | content: "What's the weather like in Boston? Say it like a weather reporter.", 117 | }, 118 | ] 119 | const request: CreateChatCompletionRequest = { 120 | model: 'gpt-3.5-turbo-0613', 121 | messages, 122 | functions: [jsonSchemaGetCurrentWeather], 123 | stream: false, 124 | max_tokens: 1000, 125 | } 126 | const { data: response } = await openai.createChatCompletion(request) 127 | debug(`API responseWithFnUse: ${JSON.stringify(response, null, 2)}`) 128 | const responseData = await handleToolUse(openai, request, response, { 129 | registry, 130 | }) 131 | const result = responseData?.choices[0].message 132 | debug(`API final result: ${JSON.stringify(result, null, 2)}`) 133 | }, 30000) 134 | }) 135 | -------------------------------------------------------------------------------- /test/typedDataReceiverFlow.test.ts: -------------------------------------------------------------------------------- 1 | import { handleToolUse } from '../src/ToolFunction' 2 | import { ToolFunction } from '../src/ToolFunction' 3 | import { 4 | CreateChatCompletionRequest, 5 | ChatCompletionRequestMessage, 6 | ChatCompletionRequestMessageRoleEnum, 7 | } from 'openai' 8 | import { Configuration, OpenAIApi } from 'openai' 9 | import Debug from 'debug' 10 | const debug = Debug('test') 11 | 12 | // Set up test function and types 13 | type CityData = { 14 | name: string 15 | region: string 16 | country: string 17 | lat: number 18 | lon: number 19 | population: number 20 | } 21 | 22 | let openai: OpenAIApi 23 | describe('Perform a round trip test with the OpenAI API', () => { 24 | beforeAll(() => { 25 | const configuration = new Configuration({ 26 | apiKey: process.env.OPENAI_API_KEY, 27 | }) 28 | openai = new OpenAIApi(configuration) 29 | }) 30 | 31 | test('it should work using handleToolUse', async () => { 32 | let citiesDataResponse: CityData[] = [] 33 | const submitLLMGeneratedData = function submitLLMGeneratedData(citiesData: CityData[]): string { 34 | debug(`citiesData: ${JSON.stringify(citiesData, null, 2)}`) 35 | citiesDataResponse = citiesData 36 | return '{ "status": "ok" }' 37 | } 38 | 39 | // Build JSON schema description of the test function 40 | const submitLLMGeneratedDataTool = ToolFunction.from(submitLLMGeneratedData) 41 | const { registry, schema: jsonSchemaSubmitLLMGeneratedData } = submitLLMGeneratedDataTool 42 | 43 | // Run a completion series 44 | const messages: ChatCompletionRequestMessage[] = [ 45 | { 46 | role: ChatCompletionRequestMessageRoleEnum.User, 47 | content: 48 | 'Generate data for the 10 largest world cities and provide your results via the submitGeneratedData function. When you call submitGeneratedData, you must minify the JSON in the arguments, ie: no extra whitespace, and no newlines.', 49 | }, 50 | ] 51 | const request: CreateChatCompletionRequest = { 52 | model: 'gpt-3.5-turbo-0613', 53 | messages, 54 | functions: [jsonSchemaSubmitLLMGeneratedData], 55 | function_call: { name: 'submitLLMGeneratedData' }, 56 | stream: false, 57 | max_tokens: 1000, 58 | } 59 | const { data: response } = await openai.createChatCompletion(request) 60 | 61 | // Handle function use by the LLM 62 | const responseData = await handleToolUse(openai, request, response, { 63 | registry, 64 | }) 65 | const result = responseData?.choices[0].message 66 | debug(`responseData: ${JSON.stringify(responseData, null, 2)}`) 67 | debug(`Final result: ${JSON.stringify(result, null, 2)}`) 68 | 69 | expect(citiesDataResponse.length).toEqual(10) 70 | }, 30000) 71 | }) 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "commonjs" /* Specify what module code is generated. */, 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | "rootDirs": ["src"] /* Allow multiple folders to be treated as one when resolving modules. */, 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | /* Emit */ 47 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 73 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 75 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */, 76 | "strict": true /* Enable all strict type-checking options. */, 77 | // "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 78 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 83 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | /* Completeness */ 96 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 97 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 98 | "plugins": [], 99 | "strictNullChecks": true 100 | }, 101 | "include": ["src/**/*"], 102 | "reflection": true 103 | } 104 | --------------------------------------------------------------------------------