├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 
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: 
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 |
--------------------------------------------------------------------------------