diff --git a/.changeset/nasty-hairs-destroy.md b/.changeset/nasty-hairs-destroy.md new file mode 100644 index 0000000..8eef368 --- /dev/null +++ b/.changeset/nasty-hairs-destroy.md @@ -0,0 +1,19 @@ +--- +'function-gpt': major +--- + +Revamped public API to provide only the core functionality + +OpenAI has just announced their Assistants API which also allows function +calling. The previous API design of function-gpt was coupled with the chat +completion API thus won't be flexible enough for this library to work well +with the new Assistants API. + +As a result, the public API of this library has been revamped to provide only +the core functionality of generating function calling schema, and executing +function calling on demand. + +The previous ChatGPTSession class was removed, as it was coupled with the chat +completion API. A new class FunctionCallingProvider is introduced and can be +used instead of ChatGPTSession for defining functions to be used by function +calling. diff --git a/.do_tasks b/.do_tasks new file mode 100644 index 0000000..bd61ffb --- /dev/null +++ b/.do_tasks @@ -0,0 +1 @@ +provide example with integration of OpenAI's node.js client diff --git a/.editorconfig b/.editorconfig index e9d2f99..f991e51 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,5 +3,5 @@ indent_style=space indent_size=2 tab_width=2 end_of_line=lf -insert_final_newline=false -charset=utf-8 +insert_final_newline=true +charset=utf-8 \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 0c67073..fa9699b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,6 +2,6 @@ "semi": true, "trailingComma": "all", "singleQuote": true, - "printWidth": 120, + "printWidth": 80, "tabWidth": 2 } diff --git a/README.md b/README.md index edf22f4..cd0b939 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ # Function-GPT -> This is a typescript library that helps handle [function calling](https://platform.openai.com/docs/guides/gpt/function-calling) with OpenAI's ChatGPT API. +> This is a typescript library that helps handle [function calling](https://platform.openai.com/docs/guides/gpt/function-calling) with OpenAI. [![NPM](https://img.shields.io/npm/v/function-gpt.svg)](https://www.npmjs.com/package/function-gpt) [![Build Status](https://github.com/atinylittleshell/function-gpt/actions/workflows/publish.yml/badge.svg)](https://github.com/atinylittleshell/function-gpt/actions/workflows/publish.yml) [![codecov](https://codecov.io/gh/atinylittleshell/function-gpt/graph/badge.svg?token=1R81CX1Z14)](https://codecov.io/gh/atinylittleshell/function-gpt) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/atinylittleshell/function-gpt/blob/main/license) -- Leverages the official [openai](https://www.npmjs.com/package/openai) npm package for communicating with OpenAI's API - Uses typescript decorators to provide metadata for function calling - Automatically generate function calling JSON schema from decorated typescript functions -- Automatically parse function calling response -- Automatically call functions and send back results to OpenAI +- Automatically call functions based on name and JSON-formatted arguments +- Can be used with OpenAI's Chat Completion API as well as the Assistants API ## Example ```typescript -import { gptFunction, gptString, ChatGPTSession } from 'function-gpt'; +import { gptFunction, gptString, FunctionCallingProvider } from 'function-gpt'; // Define the type of the input parameter for functions above. class BrowseParams { @@ -25,9 +24,9 @@ class BrowseParams { public url!: string; } -// Create your own class that extends ChatGPTSession. -class BrowseSession extends ChatGPTSession { - // Define functions that you want to provide to ChatGPT for function calling. +// Create your own class that extends FunctionCallingProvider. +class BrowseProvider extends FunctionCallingProvider { + // Define functions that you want to provide to OpenAI for function calling. // Decorate each function with @gptFunction to provide necessary metadata. // The function should accept a single parameter that is a typed object. @gptFunction('make http request to a url and return its html content', BrowseParams) @@ -37,22 +36,13 @@ class BrowseSession extends ChatGPTSession { } } -const session = new BrowseSession(); -const response = await session.send('count characters in the html content of https://www.google.com.'); +const provider = new BrowseProvider(); -// BrowseSession will first call OpenAI's ChatGPT API with the above prompt -// along with metadata about the browse function. - -// OpenAI's ChatGPT API will then return a function calling response that -// asks for making a call to the browse function. - -// BrowseSession will then call the browse function with the parameters -// specified in OpenAI's function calling response, and then send back the -// result to OpenAI's ChatGPT API. - -// OpenAI's ChatGPT API will then return a message that contains the -// chat response. -expect(response).toBe('There are 4096 characters in the html content of https://www.google.com/.'); +const schema = await provider.getSchema(); +const result = await provider.handleFunctionCall( + 'browse', + JSON.stringify({ url: 'https://www.google.com' }), +); ``` ## API References @@ -71,4 +61,4 @@ pnpm add function-gpt ## Contributing -Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for more info. \ No newline at end of file +Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for more info. diff --git a/doc/README.md b/doc/README.md index 1138138..0a5a9ce 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,14 +6,7 @@ function-gpt ### Classes -- [ChatGPTSession](classes/ChatGPTSession.md) - -### Type Aliases - -- [ChatGPTSessionOptions](README.md#chatgptsessionoptions) -- [ChatGPTFunctionCall](README.md#chatgptfunctioncall) -- [ChatGPTSessionMessage](README.md#chatgptsessionmessage) -- [ChatGPTSendMessageOptions](README.md#chatgptsendmessageoptions) +- [FunctionCallingProvider](classes/FunctionCallingProvider.md) ### Functions @@ -26,112 +19,14 @@ function-gpt - [gptEnum](README.md#gptenum) - [gptArray](README.md#gptarray) -## Type Aliases - -### ChatGPTSessionOptions - -Ƭ **ChatGPTSessionOptions**: `Object` - -Options for the ChatGPTSession constructor. Compatible with the OpenAI node client options. - -**`See`** - -[OpenAI Node Client](https://github.com/openai/openai-node) - -#### Type declaration - -| Name | Type | Description | -| :------ | :------ | :------ | -| `apiKey?` | `string` | Your API key for the OpenAI API. **`Default`** ```ts process.env["OPENAI_API_KEY"] ``` | -| `baseURL?` | `string` | Override the default base URL for the API, e.g., "https://api.example.com/v2/" | -| `systemMessage?` | `string` | A system message to send to the assistant before the user's first message. Useful for setting up the assistant's behavior. **`Default`** ```ts No system message set. ``` | -| `timeout?` | `number` | The maximum amount of time (in milliseconds) that the client should wait for a response from the server before timing out a single request. Note that request timeouts are retried by default, so in a worst-case scenario you may wait much longer than this timeout before the promise succeeds or fails. | -| `maxRetries?` | `number` | The maximum number of times that the client will retry a request in case of a temporary failure, like a network error or a 5XX error from the server. **`Default`** ```ts 2 ``` | -| `dangerouslyAllowBrowser?` | `boolean` | By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. Only set this option to `true` if you understand the risks and have appropriate mitigations in place. | - -#### Defined in - -[src/session.ts:71](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L71) - -___ - -### ChatGPTFunctionCall - -Ƭ **ChatGPTFunctionCall**: `Object` - -Represents a function call requested by ChatGPT. - -#### Type declaration - -| Name | Type | -| :------ | :------ | -| `name` | `string` | -| `arguments` | `string` | - -#### Defined in - -[src/session.ts:119](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L119) - -___ - -### ChatGPTSessionMessage - -Ƭ **ChatGPTSessionMessage**: `Object` - -Represents a message in a ChatGPT session. - -#### Type declaration - -| Name | Type | -| :------ | :------ | -| `role` | ``"system"`` \| ``"user"`` \| ``"assistant"`` \| ``"function"`` | -| `name?` | `string` | -| `content` | `string` \| ``null`` | -| `function_call?` | [`ChatGPTFunctionCall`](README.md#chatgptfunctioncall) | - -#### Defined in - -[src/session.ts:127](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L127) - -___ - -### ChatGPTSendMessageOptions - -Ƭ **ChatGPTSendMessageOptions**: `Object` - -Options for the ChatGPTSession.send method. - -**`See`** - -[OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create). - -#### Type declaration - -| Name | Type | Description | -| :------ | :------ | :------ | -| `function_call_execute_only?` | `boolean` | Stop the session after executing the function call. Useful when you don't need to give ChatGPT the result of the function call. Defaults to `false`. | -| `model` | `string` | ID of the model to use. **`See`** [model endpoint compatibility](https://platform.openai.com/docs/models/overview) | -| `frequency_penalty?` | `number` \| ``null`` | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. **`See`** [See more information about frequency and presence penalties.](https://platform.openai.com/docs/api-reference/parameter-details) | -| `function_call?` | ``"none"`` \| ``"auto"`` \| { `name`: `string` } | Controls how the model responds to function calls. "none" means the model does not call a function, and responds to the end-user. "auto" means the model can pick between an end-user or calling a function. Specifying a particular function via `{"name":\ "my_function"}` forces the model to call that function. "none" is the default when no functions are present. "auto" is the default if functions are present. | -| `logit_bias?` | `Record`<`string`, `number`\> \| ``null`` | Modify the likelihood of specified tokens appearing in the completion. Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. | -| `max_tokens?` | `number` | The maximum number of [tokens](/tokenizer) to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) for counting tokens. | -| `presence_penalty?` | `number` \| ``null`` | Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. [See more information about frequency and presence penalties.](https://platform.openai.com/docs/api-reference/parameter-details) | -| `stop?` | `string` \| ``null`` \| `string`[] | Up to 4 sequences where the API will stop generating further tokens. | -| `temperature?` | `number` \| ``null`` | What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both. | -| `top_p?` | `number` \| ``null`` | An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or `temperature` but not both. | -| `user?` | `string` | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. **`See`** [Learn more](https://platform.openai.com/docs/guides/safety-best-practices). | - -#### Defined in - -[src/session.ts:139](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L139) - ## Functions ### gptFunction ▸ **gptFunction**(`description`, `inputType`): (`target`: `object`, `propertyKey`: `string`, `descriptor`: `PropertyDescriptor`) => `void` -Use this decorator on a method within a ChatGPTSession subclass to enable it for function-calling. +Use this decorator on a method within a FunctionCallingProvider subclass +to enable it for function-calling. #### Parameters @@ -164,7 +59,7 @@ Use this decorator on a method within a ChatGPTSession subclass to enable it for #### Defined in -[src/decorators.ts:19](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L19) +[src/decorators.ts:20](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L20) ___ @@ -201,7 +96,7 @@ Use this decorator on a property within a custom class to include it as a parame #### Defined in -[src/decorators.ts:53](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L53) +[src/decorators.ts:61](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L61) ___ @@ -237,7 +132,7 @@ Use this decorator on a string property within a custom class to include it as a #### Defined in -[src/decorators.ts:142](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L142) +[src/decorators.ts:158](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L158) ___ @@ -273,7 +168,7 @@ Use this decorator on a number property within a custom class to include it as a #### Defined in -[src/decorators.ts:152](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L152) +[src/decorators.ts:168](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L168) ___ @@ -309,7 +204,7 @@ Use this decorator on a boolean property within a custom class to include it as #### Defined in -[src/decorators.ts:162](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L162) +[src/decorators.ts:178](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L178) ___ @@ -346,7 +241,7 @@ Use this decorator on a custom class property within a custom class to include i #### Defined in -[src/decorators.ts:173](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L173) +[src/decorators.ts:189](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L189) ___ @@ -383,7 +278,7 @@ Use this decorator on a custom class property within a custom class to include i #### Defined in -[src/decorators.ts:184](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L184) +[src/decorators.ts:204](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L204) ___ @@ -420,4 +315,4 @@ Use this decorator on an array of strings property within a custom class to incl #### Defined in -[src/decorators.ts:194](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/decorators.ts#L194) +[src/decorators.ts:218](https://github.com/atinylittleshell/function-gpt/blob/51cdc39/src/decorators.ts#L218) diff --git a/doc/classes/ChatGPTSession.md b/doc/classes/ChatGPTSession.md deleted file mode 100644 index 45c6f8f..0000000 --- a/doc/classes/ChatGPTSession.md +++ /dev/null @@ -1,158 +0,0 @@ -[function-gpt](../README.md) / ChatGPTSession - -# Class: ChatGPTSession - -Extend this class to create your own function-calling enabled ChatGPT session. -Provide functions to the assistant by decorating them with the `@gptFunction` decorator. - -**`See`** - -[gptFunction](../README.md#gptfunction) - -## Table of contents - -### Constructors - -- [constructor](ChatGPTSession.md#constructor) - -### Properties - -- [openai](ChatGPTSession.md#openai) -- [metadata](ChatGPTSession.md#metadata) -- [sessionMessages](ChatGPTSession.md#sessionmessages) -- [options](ChatGPTSession.md#options) - -### Accessors - -- [messages](ChatGPTSession.md#messages) - -### Methods - -- [send](ChatGPTSession.md#send) -- [processAssistantMessage](ChatGPTSession.md#processassistantmessage) - -## Constructors - -### constructor - -• **new ChatGPTSession**(`options?`) - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `options` | [`ChatGPTSessionOptions`](../README.md#chatgptsessionoptions) & `ClientOptions` | Options for the ChatGPTSession constructor. | - -**`See`** - -[ChatGPTSessionOptions](../README.md#chatgptsessionoptions) - -#### Defined in - -[src/session.ts:252](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L252) - -## Properties - -### openai - -• `Readonly` **openai**: `OpenAI` - -#### Defined in - -[src/session.ts:243](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L243) - -___ - -### metadata - -• `Private` `Readonly` **metadata**: `GPTClientMetadata` - -#### Defined in - -[src/session.ts:244](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L244) - -___ - -### sessionMessages - -• `Private` **sessionMessages**: [`ChatGPTSessionMessage`](../README.md#chatgptsessionmessage)[] = `[]` - -#### Defined in - -[src/session.ts:245](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L245) - -___ - -### options - -• `Private` `Readonly` **options**: [`ChatGPTSessionOptions`](../README.md#chatgptsessionoptions) & `ClientOptions` = `{}` - -Options for the ChatGPTSession constructor. - -#### Defined in - -[src/session.ts:252](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L252) - -## Accessors - -### messages - -• `get` **messages**(): [`ChatGPTSessionMessage`](../README.md#chatgptsessionmessage)[] - -#### Returns - -[`ChatGPTSessionMessage`](../README.md#chatgptsessionmessage)[] - -The messages sent to and from the assistant so far. - -#### Defined in - -[src/session.ts:302](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L302) - -## Methods - -### send - -▸ **send**(`message`, `options?`): `Promise`<`string`\> - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `message` | `string` | The user message to send to the assistant. | -| `options` | [`ChatGPTSendMessageOptions`](../README.md#chatgptsendmessageoptions) | Options for the ChatGPTSession.send method. | - -#### Returns - -`Promise`<`string`\> - -The assistant's response. - -**`See`** - -[ChatGPTSendMessageOptions](../README.md#chatgptsendmessageoptions) - -#### Defined in - -[src/session.ts:269](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L269) - -___ - -### processAssistantMessage - -▸ `Private` **processAssistantMessage**(`message`, `options`): `Promise`<`string`\> - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `message` | [`ChatGPTSessionMessage`](../README.md#chatgptsessionmessage) | -| `options` | [`ChatGPTSendMessageOptions`](../README.md#chatgptsendmessageoptions) | - -#### Returns - -`Promise`<`string`\> - -#### Defined in - -[src/session.ts:306](https://github.com/atinylittleshell/function-gpt/blob/24758c8/src/session.ts#L306) diff --git a/doc/classes/FunctionCallingProvider.md b/doc/classes/FunctionCallingProvider.md new file mode 100644 index 0000000..ac1b2a4 --- /dev/null +++ b/doc/classes/FunctionCallingProvider.md @@ -0,0 +1,87 @@ +[function-gpt](../README.md) / FunctionCallingProvider + +# Class: FunctionCallingProvider + +Extend this class to create your own function-calling provider. +Provide functions to be called by decorating them with the `@gptFunction` decorator. + +**`See`** + +[gptFunction](../README.md#gptfunction) + +## Table of contents + +### Constructors + +- [constructor](FunctionCallingProvider.md#constructor) + +### Properties + +- [metadata](FunctionCallingProvider.md#metadata) + +### Methods + +- [handleFunctionCalling](FunctionCallingProvider.md#handlefunctioncalling) +- [getSchema](FunctionCallingProvider.md#getschema) + +## Constructors + +### constructor + +• **new FunctionCallingProvider**() + +#### Defined in + +src/public.ts:16 + +## Properties + +### metadata + +• `Private` `Readonly` **metadata**: `FunctionCallingProviderMetadata` + +#### Defined in + +src/public.ts:14 + +## Methods + +### handleFunctionCalling + +▸ **handleFunctionCalling**(`name`, `argumentsJson`): `Promise`<`unknown`\> + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `name` | `string` | Name of the function that is being called. | +| `argumentsJson` | `string` | JSON string of all input arguments to the function call. | + +#### Returns + +`Promise`<`unknown`\> + +Result value of the function call. + +#### Defined in + +src/public.ts:31 + +___ + +### getSchema + +▸ **getSchema**(): `undefined` \| { `name`: `string` = f.name; `description`: `string` = f.description; `parameters`: `Record`<`string`, `unknown`\> }[] + +Generate function schema objects that can be passed directly to +OpenAI's Node.js client whenever function calling schema is needed. + +#### Returns + +`undefined` \| { `name`: `string` = f.name; `description`: `string` = f.description; `parameters`: `Record`<`string`, `unknown`\> }[] + +An array of function schema objects. + +#### Defined in + +src/public.ts:52 diff --git a/index.ts b/index.ts index 7a29f38..4124106 100644 --- a/index.ts +++ b/index.ts @@ -1,2 +1,2 @@ export * from './src/decorators.js'; -export * from './src/session.js'; +export * from './src/public.js'; diff --git a/package.json b/package.json index a35c270..a58bdb8 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,6 @@ "changeset": "changeset", "release": "pnpm build && changeset publish" }, - "dependencies": { - "openai": "^4.0.0" - }, "devDependencies": { "@changesets/cli": "^2.26.2", "@tsconfig/node16": "^16.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f7d4d5..a58d5c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,11 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - openai: - specifier: ^4.0.0 - version: 4.0.0 - devDependencies: '@changesets/cli': specifier: ^2.26.2 @@ -690,23 +685,13 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/node-fetch@2.6.4: - resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} - dependencies: - '@types/node': 20.5.0 - form-data: 3.0.1 - dev: false - /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true - /@types/node@18.17.5: - resolution: {integrity: sha512-xNbS75FxH6P4UXTPUJp/zNPq6/xsfdJKussCWNOnz4aULWIRwMgP1LgaB5RiBnMX1DPCYenuqGZfnIAx5mbFLA==} - dev: false - /@types/node@20.5.0: resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} + dev: true /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -906,13 +891,6 @@ packages: pretty-format: 29.6.2 dev: true - /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - dependencies: - event-target-shim: 5.0.1 - dev: false - /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -932,13 +910,6 @@ packages: hasBin: true dev: true - /agentkeepalive@4.5.0: - resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} - engines: {node: '>= 8.0.0'} - dependencies: - humanize-ms: 1.2.1 - dev: false - /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -1078,10 +1049,6 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -1091,10 +1058,6 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /base-64@0.1.0: - resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} - dev: false - /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1227,10 +1190,6 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - dev: false - /check-error@1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true @@ -1298,13 +1257,6 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: false - /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1335,10 +1287,6 @@ packages: which: 2.0.2 dev: true - /crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - dev: false - /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} dev: true @@ -1445,11 +1393,6 @@ packages: object-keys: 1.1.1 dev: true - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false - /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1460,13 +1403,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /digest-fetch@1.3.0: - resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==} - dependencies: - base-64: 0.1.0 - md5: 2.3.0 - dev: false - /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1831,11 +1767,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - dev: false - /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1967,27 +1898,6 @@ packages: is-callable: 1.2.7 dev: true - /form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - dev: false - - /form-data@3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - - /formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - dev: false - /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2220,12 +2130,6 @@ packages: engines: {node: '>=14.18.0'} dev: true - /humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - dependencies: - ms: 2.1.2 - dev: false - /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2314,10 +2218,6 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - dev: false - /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -2698,14 +2598,6 @@ packages: hasBin: true dev: true - /md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - dev: false - /meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -2740,18 +2632,6 @@ packages: picomatch: 2.3.1 dev: true - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2809,6 +2689,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2832,23 +2713,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - dev: false - - /node-fetch@2.6.12: - resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -2958,22 +2822,6 @@ packages: is-wsl: 2.2.0 dev: true - /openai@4.0.0: - resolution: {integrity: sha512-UHv70gIw20pxu9tiUueE9iS+4U4eTGiTgQr+zlJ5aX4oj6LUUp+7mBn0xAqilawftwUB/biohPth2vcZFmoNYw==} - hasBin: true - dependencies: - '@types/node': 18.17.5 - '@types/node-fetch': 2.6.4 - abort-controller: 3.0.0 - agentkeepalive: 4.5.0 - digest-fetch: 1.3.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.6.12 - transitivePeerDependencies: - - encoding - dev: false - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -3723,10 +3571,6 @@ packages: is-number: 7.0.0 dev: true - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: @@ -4108,26 +3952,10 @@ packages: defaults: 1.0.4 dev: true - /web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - dev: false - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: diff --git a/src/decorators.ts b/src/decorators.ts index 273f168..ebebc23 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -1,14 +1,15 @@ import { - GPT_CLIENT_METADATA, + FUNCTION_CALLING_PROVIDER_METADATA, + FunctionCallingProviderMetadata, GPT_TYPE_METADATA, - GPTClientMetadata, GPTObjectTypeMetadata, GPTTypeMetadata, } from './internals.js'; -import { ChatGPTSession } from './session.js'; +import { FunctionCallingProvider } from './public.js'; /** - * Use this decorator on a method within a ChatGPTSession subclass to enable it for function-calling. + * Use this decorator on a method within a FunctionCallingProvider subclass + * to enable it for function-calling. * * @param description - A description of the function. * @param inputType - Input for the function should be an object instance of a custom class. @@ -17,16 +18,23 @@ import { ChatGPTSession } from './session.js'; * @see {@link gptObjectField} */ export function gptFunction(description: string, inputType: new () => unknown) { - return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) { - const ctor = target.constructor as new () => ChatGPTSession; + return function ( + target: object, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const ctor = target.constructor as new () => FunctionCallingProvider; if (!ctor) { - throw new Error(`@gptFunction decorator was used on '${propertyKey}' which is not a class instance method`); + throw new Error( + `@gptFunction decorator was used on '${propertyKey}' which is not a class instance method`, + ); } - const metadata: GPTClientMetadata = GPT_CLIENT_METADATA.get(ctor) || { - constructor: ctor, - functions: {}, - }; + const metadata: FunctionCallingProviderMetadata = + FUNCTION_CALLING_PROVIDER_METADATA.get(ctor) || { + constructor: ctor, + functions: {}, + }; metadata.functions[propertyKey] = { name: propertyKey, @@ -35,7 +43,7 @@ export function gptFunction(description: string, inputType: new () => unknown) { inputType: GPT_TYPE_METADATA.get(inputType) as GPTObjectTypeMetadata, }; - GPT_CLIENT_METADATA.set(ctor, metadata); + FUNCTION_CALLING_PROVIDER_METADATA.set(ctor, metadata); }; } @@ -57,14 +65,22 @@ export function gptObjectField( | 'boolean' | { enum: string[] } | (new () => unknown) - | ['string' | 'number' | 'boolean' | { enum: string[] } | (new () => unknown)], + | [ + | 'string' + | 'number' + | 'boolean' + | { enum: string[] } + | (new () => unknown), + ], description: string, optional = false, ) { return function (target: object, propertyKey: string) { const ctor = target.constructor as new () => unknown; if (!ctor) { - throw new Error(`@gptObjectField decorator was used on '${propertyKey}' which is not a class instance property`); + throw new Error( + `@gptObjectField decorator was used on '${propertyKey}' which is not a class instance property`, + ); } const metadata = (GPT_TYPE_METADATA.get(ctor) as GPTObjectTypeMetadata) || { @@ -170,7 +186,11 @@ export function gptBoolean(description: string, optional = false) { * @param description - Description of the field. * @param optional - Whether the field is optional. Default to `false`. */ -export function gptObject(type: new () => unknown, description: string, optional = false) { +export function gptObject( + type: new () => unknown, + description: string, + optional = false, +) { return gptObjectField(type, description, optional); } @@ -181,7 +201,11 @@ export function gptObject(type: new () => unknown, description: string, optional * @param description - Description of the field. * @param optional - Whether the field is optional. Default to `false`. */ -export function gptEnum(values: string[], description: string, optional = false) { +export function gptEnum( + values: string[], + description: string, + optional = false, +) { return gptObjectField({ enum: values }, description, optional); } @@ -192,7 +216,12 @@ export function gptEnum(values: string[], description: string, optional = false) * @param optional - Whether the field is optional. Default to `false`. */ export function gptArray( - type: 'string' | 'number' | 'boolean' | { enum: string[] } | (new () => unknown), + type: + | 'string' + | 'number' + | 'boolean' + | { enum: string[] } + | (new () => unknown), description: string, optional = false, ) { diff --git a/src/internals.ts b/src/internals.ts index a9eb890..cc2226a 100644 --- a/src/internals.ts +++ b/src/internals.ts @@ -1,4 +1,4 @@ -import type { ChatGPTSession } from './session.js'; +import { FunctionCallingProvider } from './public.js'; export type GPTPrimitiveTypeMetadata = { type: 'string' | 'number' | 'boolean'; @@ -39,10 +39,78 @@ export type GPTFunctionMetadata = { inputType: GPTObjectTypeMetadata; }; -export type GPTClientMetadata = { - constructor: new () => ChatGPTSession; +export type FunctionCallingProviderMetadata = { + constructor: new () => FunctionCallingProvider; functions: Record; }; -export const GPT_CLIENT_METADATA = new Map ChatGPTSession, GPTClientMetadata>(); export const GPT_TYPE_METADATA = new Map unknown, GPTTypeMetadata>(); + +export const FUNCTION_CALLING_PROVIDER_METADATA = new Map< + new () => FunctionCallingProvider, + FunctionCallingProviderMetadata +>(); + +export const describeField = ( + description: string | null, + fieldType: GPTTypeMetadata, +) => { + let result: Record = + description === null + ? {} + : { + description, + }; + + switch (fieldType.type) { + case 'string': + result = { + ...result, + type: 'string', + }; + break; + case 'number': + result = { + ...result, + type: 'number', + }; + break; + case 'boolean': + result = { + ...result, + type: 'boolean', + }; + break; + case 'object': + result = { + ...result, + type: 'object', + properties: fieldType.fields.reduce((acc, f) => { + return { + ...acc, + [f.name]: describeField(f.description, f.type), + }; + }, {}), + required: fieldType.fields.filter((f) => f.required).map((f) => f.name), + }; + break; + case 'enum': + result = { + ...result, + type: 'string', + enum: fieldType.values, + }; + break; + case 'array': + result = { + ...result, + type: 'array', + items: describeField(null, fieldType.elementType), + }; + break; + default: + throw new Error(`Unknown field type: ${fieldType}`); + } + + return result; +}; diff --git a/src/public.ts b/src/public.ts new file mode 100644 index 0000000..53c6cb8 --- /dev/null +++ b/src/public.ts @@ -0,0 +1,64 @@ +import { + describeField, + FUNCTION_CALLING_PROVIDER_METADATA, + FunctionCallingProviderMetadata, +} from './internals.js'; + +/** + * Extend this class to create your own function-calling provider. + * Provide functions to be called by decorating them with the `@gptFunction` decorator. + * + * @see {@link gptFunction} + */ +export class FunctionCallingProvider { + private readonly metadata: FunctionCallingProviderMetadata; + + constructor() { + const metadata = FUNCTION_CALLING_PROVIDER_METADATA.get( + this.constructor as new () => FunctionCallingProvider, + ); + if (!metadata) { + throw new Error('No metadata found for this class'); + } + this.metadata = metadata; + } + + /** + * @param name - Name of the function that is being called. + * @param argumentsJson - JSON string of all input arguments to the function call. + * @returns Result value of the function call. + */ + public async handleFunctionCalling(name: string, argumentsJson: string) { + const result = this.metadata.functions[name].value.bind(this)( + JSON.parse(argumentsJson), + ); + + let resultValue: unknown; + if (result instanceof Promise) { + resultValue = await result; + } else { + resultValue = result; + } + + return resultValue; + } + + /** + * Generate function schema objects that can be passed directly to + * OpenAI's Node.js client whenever function calling schema is needed. + * + * @returns An array of function schema objects. + */ + public getSchema() { + const schema = Object.values(this.metadata.functions).map((f) => ({ + name: f.name, + description: f.description, + parameters: describeField(null, f.inputType), + })); + + if (schema.length === 0) { + return undefined; + } + return schema; + } +} diff --git a/src/session.ts b/src/session.ts deleted file mode 100644 index 802f233..0000000 --- a/src/session.ts +++ /dev/null @@ -1,372 +0,0 @@ -import OpenAI, { ClientOptions } from 'openai'; - -import { GPT_CLIENT_METADATA, GPTClientMetadata, GPTTypeMetadata } from './internals.js'; - -const describeField = (description: string | null, fieldType: GPTTypeMetadata) => { - let result: Record = - description === null - ? {} - : { - description, - }; - - switch (fieldType.type) { - case 'string': - result = { - ...result, - type: 'string', - }; - break; - case 'number': - result = { - ...result, - type: 'number', - }; - break; - case 'boolean': - result = { - ...result, - type: 'boolean', - }; - break; - case 'object': - result = { - ...result, - type: 'object', - properties: fieldType.fields.reduce((acc, f) => { - return { - ...acc, - [f.name]: describeField(f.description, f.type), - }; - }, {}), - required: fieldType.fields.filter((f) => f.required).map((f) => f.name), - }; - break; - case 'enum': - result = { - ...result, - type: 'string', - enum: fieldType.values, - }; - break; - case 'array': - result = { - ...result, - type: 'array', - items: describeField(null, fieldType.elementType), - }; - break; - default: - throw new Error(`Unknown field type: ${fieldType}`); - } - - return result; -}; - -/** - * Options for the ChatGPTSession constructor. Compatible with the OpenAI node client options. - * - * @see [OpenAI Node Client](https://github.com/openai/openai-node) - */ -export type ChatGPTSessionOptions = { - /** - * Your API key for the OpenAI API. - * - * @default process.env["OPENAI_API_KEY"] - */ - apiKey?: string; - - /** - * Override the default base URL for the API, e.g., "https://api.example.com/v2/" - */ - baseURL?: string; - - /** - * A system message to send to the assistant before the user's first message. - * Useful for setting up the assistant's behavior. - * - * @default No system message set. - */ - systemMessage?: string; - - /** - * The maximum amount of time (in milliseconds) that the client should wait for a response - * from the server before timing out a single request. - * - * Note that request timeouts are retried by default, so in a worst-case scenario you may wait - * much longer than this timeout before the promise succeeds or fails. - */ - timeout?: number; - - /** - * The maximum number of times that the client will retry a request in case of a - * temporary failure, like a network error or a 5XX error from the server. - * - * @default 2 - */ - maxRetries?: number; - - /** - * By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. - * Only set this option to `true` if you understand the risks and have appropriate mitigations in place. - */ - dangerouslyAllowBrowser?: boolean; -}; - -/** - * Represents a function call requested by ChatGPT. - */ -export type ChatGPTFunctionCall = { - name: string; - arguments: string; -}; - -/** - * Represents a message in a ChatGPT session. - */ -export type ChatGPTSessionMessage = { - role: 'system' | 'user' | 'assistant' | 'function'; - name?: string; - content: string | null; - function_call?: ChatGPTFunctionCall; -}; - -/** - * Options for the ChatGPTSession.send method. - * - * @see [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create). - */ -export type ChatGPTSendMessageOptions = { - /** - * Stop the session after executing the function call. - * Useful when you don't need to give ChatGPT the result of the function call. - * Defaults to `false`. - */ - function_call_execute_only?: boolean; - - /** - * ID of the model to use. - * - * @see [model endpoint compatibility](https://platform.openai.com/docs/models/overview) - */ - model: string; - - /** - * Number between -2.0 and 2.0. Positive values penalize new tokens based on their - * existing frequency in the text so far, decreasing the model's likelihood to - * repeat the same line verbatim. - * - * @see [See more information about frequency and presence penalties.](https://platform.openai.com/docs/api-reference/parameter-details) - */ - frequency_penalty?: number | null; - - /** - * Controls how the model responds to function calls. "none" means the model does - * not call a function, and responds to the end-user. "auto" means the model can - * pick between an end-user or calling a function. Specifying a particular function - * via `{"name":\ "my_function"}` forces the model to call that function. "none" is - * the default when no functions are present. "auto" is the default if functions - * are present. - */ - function_call?: 'none' | 'auto' | { name: string }; - - /** - * Modify the likelihood of specified tokens appearing in the completion. - * - * Accepts a json object that maps tokens (specified by their token ID in the - * tokenizer) to an associated bias value from -100 to 100. Mathematically, the - * bias is added to the logits generated by the model prior to sampling. The exact - * effect will vary per model, but values between -1 and 1 should decrease or - * increase likelihood of selection; values like -100 or 100 should result in a ban - * or exclusive selection of the relevant token. - */ - logit_bias?: Record | null; - - /** - * The maximum number of [tokens](/tokenizer) to generate in the chat completion. - * - * The total length of input tokens and generated tokens is limited by the model's - * context length. - * [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) - * for counting tokens. - */ - max_tokens?: number; - - /** - * Number between -2.0 and 2.0. Positive values penalize new tokens based on - * whether they appear in the text so far, increasing the model's likelihood to - * talk about new topics. - * - * [See more information about frequency and presence penalties.](https://platform.openai.com/docs/api-reference/parameter-details) - */ - presence_penalty?: number | null; - - /** - * Up to 4 sequences where the API will stop generating further tokens. - */ - stop?: string | null | Array; - - /** - * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will - * make the output more random, while lower values like 0.2 will make it more - * focused and deterministic. - * - * We generally recommend altering this or `top_p` but not both. - */ - temperature?: number | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or `temperature` but not both. - */ - top_p?: number | null; - - /** - * A unique identifier representing your end-user, which can help OpenAI to monitor - * and detect abuse. - * - * @see [Learn more](https://platform.openai.com/docs/guides/safety-best-practices). - */ - user?: string; -}; - -/** - * Extend this class to create your own function-calling enabled ChatGPT session. - * Provide functions to the assistant by decorating them with the `@gptFunction` decorator. - * - * @see {@link gptFunction} - */ -export class ChatGPTSession { - public readonly openai: OpenAI; - private readonly metadata: GPTClientMetadata; - private sessionMessages: ChatGPTSessionMessage[] = []; - - /** - * @param options - Options for the ChatGPTSession constructor. - * - * @see {@link ChatGPTSessionOptions} - */ - constructor(private readonly options: ChatGPTSessionOptions & ClientOptions = {}) { - this.openai = new OpenAI(options); - - const metadata = GPT_CLIENT_METADATA.get(this.constructor as new () => ChatGPTSession); - if (!metadata) { - throw new Error('No metadata found for this class'); - } - this.metadata = metadata; - } - - /** - * @param message - The user message to send to the assistant. - * @param options - Options for the ChatGPTSession.send method. - * @returns The assistant's response. - * - * @see {@link ChatGPTSendMessageOptions} - */ - public async send( - message: string, - options: ChatGPTSendMessageOptions = { - model: 'gpt-3.5-turbo', - }, - ): Promise { - if (this.sessionMessages.length === 0 && this.options.systemMessage) { - this.sessionMessages.push({ - role: 'system', - content: this.options.systemMessage, - }); - } - - this.sessionMessages.push({ - role: 'user', - content: message, - }); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { function_call_execute_only, ...optionsToSend } = options; - - const response = await this.openai.chat.completions.create({ - ...optionsToSend, - messages: this.sessionMessages, - functions: this.getFunctionSchema(), - }); - - return await this.processAssistantMessage(response.choices[0].message, options); - } - - /** - * @returns The messages sent to and from the assistant so far. - */ - get messages(): ChatGPTSessionMessage[] { - return this.sessionMessages; - } - - private async processAssistantMessage( - message: ChatGPTSessionMessage, - options: ChatGPTSendMessageOptions, - ): Promise { - if (message.role !== 'assistant') { - throw new Error(`Expected assistant message, got ${message.role}`); - } - - if (!message.content && !message.function_call) { - throw new Error('Expected content or function call'); - } - - this.sessionMessages.push(message); - - if (message.function_call) { - const result = this.metadata.functions[message.function_call.name].value.bind(this)( - JSON.parse(message.function_call.arguments), - ); - - let resultValue: unknown; - if (result instanceof Promise) { - resultValue = await result; - } else { - resultValue = result; - } - - this.sessionMessages.push({ - role: 'function', - name: message.function_call.name, - content: JSON.stringify(resultValue), - }); - - if (options.function_call_execute_only) { - return ''; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { function_call_execute_only, ...optionsToSend } = options; - - const response = await this.openai.chat.completions.create({ - ...optionsToSend, - messages: this.sessionMessages, - functions: this.getFunctionSchema(), - }); - - return await this.processAssistantMessage(response.choices[0].message, options); - } - - return message.content!; - } - - /** - * @ignore - */ - public getFunctionSchema() { - const schema = Object.values(this.metadata.functions).map((f) => ({ - name: f.name, - description: f.description, - parameters: describeField(null, f.inputType), - })); - - if (schema.length === 0) { - return undefined; - } - return schema; - } -} diff --git a/tests/decorators.test.ts b/tests/decorators.test.ts index ff77ca9..a387056 100644 --- a/tests/decorators.test.ts +++ b/tests/decorators.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { - ChatGPTSession, + FunctionCallingProvider, gptArray, gptBoolean, gptEnum, @@ -12,8 +12,6 @@ import { gptString, } from '../index.js'; -process.env.OPENAI_API_KEY = 'test'; - test('basic function schema is generated correctly', async () => { class TestFuncInput { @gptObjectField('string', 'this is a test string', true) @@ -23,15 +21,15 @@ test('basic function schema is generated correctly', async () => { public testNumber!: number; } - class TestSession extends ChatGPTSession { + class TestProvider extends FunctionCallingProvider { @gptFunction('this is a test function', TestFuncInput) testFunc(params: TestFuncInput) { return params; } } - const testSession = new TestSession(); - const schema = testSession.getFunctionSchema(); + const testProvider = new TestProvider(); + const schema = testProvider.getSchema(); expect(schema).toEqual([ { name: 'testFunc', @@ -60,15 +58,15 @@ test('input parameter can be an array of strings', () => { words!: string[]; } - class TestSession extends ChatGPTSession { + class TestProvider extends FunctionCallingProvider { @gptFunction('this is a test function', TestParam) testFunc(params: TestParam) { return params; } } - const testSession = new TestSession(); - const schema = testSession.getFunctionSchema(); + const testProvider = new TestProvider(); + const schema = testProvider.getSchema(); expect(schema).toEqual([ { name: 'testFunc', @@ -116,15 +114,15 @@ test('all helper decorators should work', () => { arr!: string[]; } - class TestSession extends ChatGPTSession { + class TestProvider extends FunctionCallingProvider { @gptFunction('this is a test function', TestParam) testFunc(params: TestParam) { return params; } } - const testSession = new TestSession(); - const schema = testSession.getFunctionSchema(); + const testProvider = new TestProvider(); + const schema = testProvider.getSchema(); expect(schema).toEqual([ { name: 'testFunc', diff --git a/tests/provider.test.ts b/tests/provider.test.ts new file mode 100644 index 0000000..817bab7 --- /dev/null +++ b/tests/provider.test.ts @@ -0,0 +1,34 @@ +import { afterEach, expect, test, vi } from 'vitest'; + +import { gptFunction, gptObjectField } from '../index.js'; +import { FunctionCallingProvider } from '../src/public.js'; + +afterEach(() => { + vi.clearAllMocks(); +}); + +const fetch = vi.fn().mockImplementation(() => Promise.resolve()); + +test('function calling should work', async () => { + class BrowseParams { + @gptObjectField('string', 'url of the web page to browse', true) + public url: string = ''; + } + + class BrowseProvider extends FunctionCallingProvider { + @gptFunction('browse a web page and return its html content', BrowseParams) + async browse(params: BrowseParams) { + await fetch(params.url); + return 'this is a test response'; + } + } + + const provider = new BrowseProvider(); + const response = await provider.handleFunctionCalling( + 'browse', + JSON.stringify({ url: 'https://www.google.com' }), + ); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual('this is a test response'); +}); diff --git a/tests/session.test.ts b/tests/session.test.ts deleted file mode 100644 index 7b80e46..0000000 --- a/tests/session.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import OpenAI from 'openai'; -import { afterEach, expect, test, vi } from 'vitest'; - -import { gptFunction, gptObjectField } from '../index.js'; -import { GPTFunctionMetadata } from '../src/internals.js'; -import { ChatGPTSession, ChatGPTSessionMessage } from '../src/session.js'; - -vi.mock('openai', () => { - const mod = vi.importMock('openai'); - return { - ...mod, - default: vi.fn().mockImplementation(() => ({ - chat: { - completions: { - create: vi - .fn() - .mockImplementation( - ({ messages, functions }: { messages: ChatGPTSessionMessage[]; functions: GPTFunctionMetadata[] }) => { - if (messages[messages.length - 1].role === 'user') { - return Promise.resolve({ - choices: [ - { - message: { - role: 'assistant', - content: null, - function_call: { - name: functions[0].name, - arguments: '{}', - }, - }, - }, - ], - }); - } else if (messages[messages.length - 1].role === 'function') { - return Promise.resolve({ - choices: [ - { - message: { - role: 'assistant', - content: 'this is a test response', - }, - }, - ], - }); - } else { - return Promise.reject(new Error('Unexpected message role')); - } - }, - ), - }, - }, - })), - }; -}); - -afterEach(() => { - vi.clearAllMocks(); -}); - -const fetch = vi.fn().mockImplementation(() => Promise.resolve()); - -test('function calling should work', async () => { - class BrowseParams { - @gptObjectField('string', 'url of the web page to browse', true) - public url: string = ''; - } - - class BrowseSession extends ChatGPTSession { - @gptFunction('browse a web page and return its html content', BrowseParams) - async browse(params: BrowseParams) { - await fetch(params.url); - } - } - - const session = new BrowseSession(); - const response = await session.send('this is a test message'); - - expect(OpenAI).toHaveBeenCalledTimes(1); - expect(session.openai.chat.completions.create).toHaveBeenCalledTimes(2); - expect(fetch).toHaveBeenCalledTimes(1); - expect(response).toEqual('this is a test response'); -}); - -test('execute_only mode should work', async () => { - class BrowseParams { - @gptObjectField('string', 'url of the web page to browse', true) - public url: string = ''; - } - - class BrowseSession extends ChatGPTSession { - @gptFunction('browse a web page and return its html content', BrowseParams) - async browse(params: BrowseParams) { - await fetch(params.url); - } - } - - const session = new BrowseSession(); - const response = await session.send('this is a test message', { - model: 'gpt-3.5-turbo', - function_call_execute_only: true, - }); - - expect(OpenAI).toHaveBeenCalledTimes(1); - expect(session.openai.chat.completions.create).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledTimes(1); - expect(session.messages.length).toEqual(3); - expect(response).toEqual(''); -});