+> ⚠⚠⚠ Since v5, this implementation is only maintained to make the build pass. Some cases may not work as expected. ⚠⚠⚠
+>
+> The Nest implementation was more of a proof of concept when GPT Turbo started, rather than an actual useable project like the other implementations.
+> For this reason, not much effort will be put into maintaining it. It's kept here for historical purposes, and may be reworked in the future or removed entirely.
+> You're welcome to contribute to it if you want to keep it alive!
+>
+> *Note: this implementation has been tested thoroughly for the last time on v5.0.0 and seemed to work fine, apart from not using new features beyond v1.4, such as callable functions and plugins*
+
A NestJS app that interacts with the gpt-turbo library.
## Disclaimer
@@ -79,10 +87,12 @@ For streamed conversation prompts (`POST /conversations/{id}`), Swagger will onl
## Postman Collection
-While Swagger is the most up-to-date way of testing the API, since it is generated at runtime, you can also use the Postman collection included this directory: `GPT Turbo - Nest.postman_collection.json`. Bear in mind that this collection is not automatically updated, so it may be out of date. To use it, simply import it into Postman.
+While Swagger is the most up-to-date way of testing the API, since it is generated at runtime, you can also use the Postman collection/environment included this directory:
+- `GPT Turbo - Nest.postman_collection.json`
+- `GPT Turbo - Nest.postman_environment.json`
+
+Bear in mind that this collection is not automatically updated, so it may be out of date. To use it, simply import it into Postman.
The Postman collection includes automated scripts to assign variables so testing is relatively easier than Swagger. For example, after creating a conversation in Postman, the `conversationId` variable will be automatically assigned to the ID of the conversation. This means that you can then use the `conversationId` variable in subsequent requests, such as sending a prompt to the conversation.
Postman is also a great way to test streamed conversation prompts, since Swagger will only show the data stream after the stream has ended.
-
-**Note: If you want to use your API key with these requests, set them in the collection's variables instead of directly in the body. This has already been setup for you to prevent accidental leaks if you open a PR with this file changed! (as I did when the Nest implementation was first released!)**
\ No newline at end of file
diff --git a/packages/nest/nest-cli.json b/packages/implementations/nest/nest-cli.json
similarity index 100%
rename from packages/nest/nest-cli.json
rename to packages/implementations/nest/nest-cli.json
diff --git a/packages/nest/package.json b/packages/implementations/nest/package.json
similarity index 96%
rename from packages/nest/package.json
rename to packages/implementations/nest/package.json
index 2d9d913..1c6e254 100644
--- a/packages/nest/package.json
+++ b/packages/implementations/nest/package.json
@@ -40,7 +40,7 @@
"nestjs"
],
"author": "Tristan Chin ",
- "homepage": "https://github.com/maxijonson/gpt-turbo/tree/develop/packages/nest#readme",
+ "homepage": "https://github.com/maxijonson/gpt-turbo/tree/develop/packages/implementations/nest#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/maxijonson/gpt-turbo.git"
@@ -59,6 +59,7 @@
"@nestjs/swagger": "^6.3.0",
"dotenv": "^16.0.3",
"gpt-turbo": "^4.5.0",
+ "gpt-turbo-plugin-stats": "^4.5.0",
"lowdb": "^5.1.0",
"nestjs-zod": "^2.0.3",
"reflect-metadata": "^0.1.13",
diff --git a/packages/nest/src/app.controller.ts b/packages/implementations/nest/src/app.controller.ts
similarity index 57%
rename from packages/nest/src/app.controller.ts
rename to packages/implementations/nest/src/app.controller.ts
index 313b136..fe78376 100644
--- a/packages/nest/src/app.controller.ts
+++ b/packages/implementations/nest/src/app.controller.ts
@@ -1,7 +1,12 @@
-import { Controller } from "@nestjs/common";
+import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service.js";
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
+
+ @Get()
+ getOk() {
+ return { status: "OK" };
+ }
}
diff --git a/packages/nest/src/app.module.ts b/packages/implementations/nest/src/app.module.ts
similarity index 100%
rename from packages/nest/src/app.module.ts
rename to packages/implementations/nest/src/app.module.ts
diff --git a/packages/nest/src/app.service.ts b/packages/implementations/nest/src/app.service.ts
similarity index 100%
rename from packages/nest/src/app.service.ts
rename to packages/implementations/nest/src/app.service.ts
diff --git a/packages/nest/src/config/env.ts b/packages/implementations/nest/src/config/env.ts
similarity index 100%
rename from packages/nest/src/config/env.ts
rename to packages/implementations/nest/src/config/env.ts
diff --git a/packages/implementations/nest/src/config/gpt-turbo.ts b/packages/implementations/nest/src/config/gpt-turbo.ts
new file mode 100644
index 0000000..60d1755
--- /dev/null
+++ b/packages/implementations/nest/src/config/gpt-turbo.ts
@@ -0,0 +1,10 @@
+import { Conversation } from "gpt-turbo";
+import statsPlugin from "gpt-turbo-plugin-stats";
+
+const globalPlugins = [statsPlugin];
+Conversation.globalPlugins = [statsPlugin];
+declare module "gpt-turbo" {
+ export interface ConversationGlobalPluginsOverride {
+ globalPlugins: typeof globalPlugins;
+ }
+}
diff --git a/packages/nest/src/conversations/conversations.controller.ts b/packages/implementations/nest/src/conversations/conversations.controller.ts
similarity index 94%
rename from packages/nest/src/conversations/conversations.controller.ts
rename to packages/implementations/nest/src/conversations/conversations.controller.ts
index e7d2af3..6897bf6 100644
--- a/packages/nest/src/conversations/conversations.controller.ts
+++ b/packages/implementations/nest/src/conversations/conversations.controller.ts
@@ -20,7 +20,6 @@ import { ZodValidationPipe } from "nestjs-zod";
import { uuidSchema } from "../schemas/uuidSchema.js";
import { UseZodApiOperation } from "../decorators/zod-api-operation.decorator.js";
import { getConversationsResponseDtoSchema } from "./dtos/getConversations.dto.js";
-import messageToJson from "../utils/messageToJson.js";
import { Response } from "express";
import { z } from "nestjs-zod/z";
import { conversationDtoSchema } from "./dtos/conversation.dto.js";
@@ -180,13 +179,13 @@ export class ConversationsController {
throw new NotFoundException("Conversation not found");
}
- return conversation.getMessages().map(messageToJson);
+ return conversation.history.getMessages().map((m) => m.toJSON());
}
private handlePromptResponse(message: Message, res: Response) {
// Non-streaming message
if (!message.isStreaming && message.content) {
- res.json(messageToJson(message));
+ res.json(message.toJSON());
return;
}
@@ -198,14 +197,12 @@ export class ConversationsController {
});
res.status(200);
- message.onMessageUpdate((_, m) => {
- const data = JSON.stringify(
- messageDtoSchema.parse(messageToJson(m))
- );
+ message.onUpdate((_, m) => {
+ const data = JSON.stringify(messageDtoSchema.parse(m.toJSON()));
res.write(`data: ${data}\n\n`);
});
- message.onMessageStreamingStop(() => {
+ message.onStreamingStop(() => {
res.end();
});
}
diff --git a/packages/nest/src/conversations/conversations.module.ts b/packages/implementations/nest/src/conversations/conversations.module.ts
similarity index 100%
rename from packages/nest/src/conversations/conversations.module.ts
rename to packages/implementations/nest/src/conversations/conversations.module.ts
diff --git a/packages/nest/src/conversations/conversations.service.ts b/packages/implementations/nest/src/conversations/conversations.service.ts
similarity index 85%
rename from packages/nest/src/conversations/conversations.service.ts
rename to packages/implementations/nest/src/conversations/conversations.service.ts
index ec6af1a..8f5124a 100644
--- a/packages/nest/src/conversations/conversations.service.ts
+++ b/packages/implementations/nest/src/conversations/conversations.service.ts
@@ -1,12 +1,12 @@
import { Injectable, NotFoundException } from "@nestjs/common";
-import { Conversation, ConversationConfigParameters } from "gpt-turbo";
+import { Conversation, ConversationConfigModel } from "gpt-turbo";
@Injectable()
export class ConversationsService {
private conversations: Conversation[] = [];
- createConversation(config: ConversationConfigParameters) {
- const conversation = new Conversation(config);
+ createConversation(config: ConversationConfigModel) {
+ const conversation = new Conversation({ config });
this.addConversation(conversation);
return conversation;
}
@@ -41,7 +41,7 @@ export class ConversationsService {
throw new NotFoundException("Conversation not found");
}
- const message = conversation
+ const message = conversation.history
.getMessages()
.find((m) => m.id === messageId);
diff --git a/packages/implementations/nest/src/conversations/dtos/conversation.dto.ts b/packages/implementations/nest/src/conversations/dtos/conversation.dto.ts
new file mode 100644
index 0000000..baf28e7
--- /dev/null
+++ b/packages/implementations/nest/src/conversations/dtos/conversation.dto.ts
@@ -0,0 +1,11 @@
+import { z } from "nestjs-zod/z";
+import { createZodDto } from "nestjs-zod";
+import { conversationSchema } from "gpt-turbo";
+
+export const conversationDtoSchema = conversationSchema;
+
+export type ConversationDto = z.infer;
+
+export class ConversationDtoEntity extends createZodDto(
+ conversationDtoSchema
+) {}
diff --git a/packages/implementations/nest/src/conversations/dtos/conversationConfig.dto.ts b/packages/implementations/nest/src/conversations/dtos/conversationConfig.dto.ts
new file mode 100644
index 0000000..eb866cb
--- /dev/null
+++ b/packages/implementations/nest/src/conversations/dtos/conversationConfig.dto.ts
@@ -0,0 +1,11 @@
+import { conversationConfigSchema } from "gpt-turbo";
+import { createZodDto } from "nestjs-zod";
+import { z } from "nestjs-zod/z";
+
+export const conversationConfigDtoSchema = conversationConfigSchema;
+
+export type ConversationConfigDto = z.infer;
+
+export class ConversationConfigDtoEntity extends createZodDto(
+ conversationConfigDtoSchema
+) {}
diff --git a/packages/nest/src/conversations/dtos/createConversation.dto.ts b/packages/implementations/nest/src/conversations/dtos/createConversation.dto.ts
similarity index 100%
rename from packages/nest/src/conversations/dtos/createConversation.dto.ts
rename to packages/implementations/nest/src/conversations/dtos/createConversation.dto.ts
diff --git a/packages/nest/src/conversations/dtos/getConversations.dto.ts b/packages/implementations/nest/src/conversations/dtos/getConversations.dto.ts
similarity index 100%
rename from packages/nest/src/conversations/dtos/getConversations.dto.ts
rename to packages/implementations/nest/src/conversations/dtos/getConversations.dto.ts
diff --git a/packages/implementations/nest/src/conversations/dtos/message.dto.ts b/packages/implementations/nest/src/conversations/dtos/message.dto.ts
new file mode 100644
index 0000000..9a11afd
--- /dev/null
+++ b/packages/implementations/nest/src/conversations/dtos/message.dto.ts
@@ -0,0 +1,9 @@
+import { z } from "nestjs-zod/z";
+import { createZodDto } from "nestjs-zod";
+import { messageSchema } from "gpt-turbo";
+
+export const messageDtoSchema = messageSchema;
+
+export type MessageDto = z.infer;
+
+export class MessageDtoEntity extends createZodDto(messageDtoSchema) {}
diff --git a/packages/nest/src/conversations/dtos/prompt.dto.ts b/packages/implementations/nest/src/conversations/dtos/prompt.dto.ts
similarity index 100%
rename from packages/nest/src/conversations/dtos/prompt.dto.ts
rename to packages/implementations/nest/src/conversations/dtos/prompt.dto.ts
diff --git a/packages/nest/src/conversations/dtos/repromptMessage.dto.ts b/packages/implementations/nest/src/conversations/dtos/repromptMessage.dto.ts
similarity index 100%
rename from packages/nest/src/conversations/dtos/repromptMessage.dto.ts
rename to packages/implementations/nest/src/conversations/dtos/repromptMessage.dto.ts
diff --git a/packages/nest/src/db/db.controller.ts b/packages/implementations/nest/src/db/db.controller.ts
similarity index 100%
rename from packages/nest/src/db/db.controller.ts
rename to packages/implementations/nest/src/db/db.controller.ts
diff --git a/packages/nest/src/db/db.module.ts b/packages/implementations/nest/src/db/db.module.ts
similarity index 100%
rename from packages/nest/src/db/db.module.ts
rename to packages/implementations/nest/src/db/db.module.ts
diff --git a/packages/nest/src/db/db.service.ts b/packages/implementations/nest/src/db/db.service.ts
similarity index 100%
rename from packages/nest/src/db/db.service.ts
rename to packages/implementations/nest/src/db/db.service.ts
diff --git a/packages/nest/src/decorators/zod-api-operation.decorator.ts b/packages/implementations/nest/src/decorators/zod-api-operation.decorator.ts
similarity index 100%
rename from packages/nest/src/decorators/zod-api-operation.decorator.ts
rename to packages/implementations/nest/src/decorators/zod-api-operation.decorator.ts
diff --git a/packages/nest/src/decorators/zod-response-validation.decorator.ts b/packages/implementations/nest/src/decorators/zod-response-validation.decorator.ts
similarity index 100%
rename from packages/nest/src/decorators/zod-response-validation.decorator.ts
rename to packages/implementations/nest/src/decorators/zod-response-validation.decorator.ts
diff --git a/packages/nest/src/filters/zod-validation-exception.filter.ts b/packages/implementations/nest/src/filters/zod-validation-exception.filter.ts
similarity index 100%
rename from packages/nest/src/filters/zod-validation-exception.filter.ts
rename to packages/implementations/nest/src/filters/zod-validation-exception.filter.ts
diff --git a/packages/nest/src/index.ts b/packages/implementations/nest/src/index.ts
similarity index 96%
rename from packages/nest/src/index.ts
rename to packages/implementations/nest/src/index.ts
index 08cd6a1..4c52537 100644
--- a/packages/nest/src/index.ts
+++ b/packages/implementations/nest/src/index.ts
@@ -12,6 +12,7 @@ import {
PORT,
} from "./config/env.js";
import { ZodValidationExceptionFilter } from "./filters/zod-validation-exception.filter.js";
+import "./config/gpt-turbo.js";
patchNestJsSwagger();
@@ -67,6 +68,7 @@ const bootstrap = async () => {
app.useGlobalFilters(new ZodValidationExceptionFilter());
+ console.info(`🚀 Listening on port ${PORT}`);
await app.listen(PORT);
};
diff --git a/packages/nest/src/interceptors/class-transform.interceptor.ts b/packages/implementations/nest/src/interceptors/class-transform.interceptor.ts
similarity index 80%
rename from packages/nest/src/interceptors/class-transform.interceptor.ts
rename to packages/implementations/nest/src/interceptors/class-transform.interceptor.ts
index 024dc0d..c18a69b 100644
--- a/packages/nest/src/interceptors/class-transform.interceptor.ts
+++ b/packages/implementations/nest/src/interceptors/class-transform.interceptor.ts
@@ -5,9 +5,9 @@ import {
NestInterceptor,
} from "@nestjs/common";
import { Conversation, Message } from "gpt-turbo";
+import { statsPluginName } from "gpt-turbo-plugin-stats";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
-import messageToJson from "../utils/messageToJson.js";
@Injectable()
export class ClassTransformInterceptor implements NestInterceptor {
@@ -45,22 +45,23 @@ export class ClassTransformInterceptor implements NestInterceptor {
}
private handleConversationInstance(instance: Conversation) {
- const { apiKey, ...config } = instance.getConfig();
+ const { apiKey, ...config } = instance.config.getConfig();
+ const stats = instance.plugins.getPluginOutput(statsPluginName);
return {
id: instance.id,
config,
- messages: instance
+ messages: instance.history
.getMessages()
.map((message) => this.handleMessageInstance(message)),
- cost: instance.getCost(),
- cumulativeCost: instance.getCumulativeCost(),
- size: instance.getSize(),
- cumulativeSize: instance.getCumulativeSize(),
+ cost: stats.cost,
+ cumulativeCost: stats.cumulativeCost,
+ size: stats.size,
+ cumulativeSize: stats.cumulativeSize,
};
}
private handleMessageInstance(instance: Message) {
- return messageToJson(instance);
+ return instance.toJSON();
}
}
diff --git a/packages/nest/src/interceptors/zod-validation.interceptor.ts b/packages/implementations/nest/src/interceptors/zod-validation.interceptor.ts
similarity index 100%
rename from packages/nest/src/interceptors/zod-validation.interceptor.ts
rename to packages/implementations/nest/src/interceptors/zod-validation.interceptor.ts
diff --git a/packages/nest/src/schemas/uuidSchema.ts b/packages/implementations/nest/src/schemas/uuidSchema.ts
similarity index 100%
rename from packages/nest/src/schemas/uuidSchema.ts
rename to packages/implementations/nest/src/schemas/uuidSchema.ts
diff --git a/packages/nest/src/utils/getOpenAPIRequestBody.ts b/packages/implementations/nest/src/utils/getOpenAPIRequestBody.ts
similarity index 100%
rename from packages/nest/src/utils/getOpenAPIRequestBody.ts
rename to packages/implementations/nest/src/utils/getOpenAPIRequestBody.ts
diff --git a/packages/nest/tsconfig.build.json b/packages/implementations/nest/tsconfig.build.json
similarity index 100%
rename from packages/nest/tsconfig.build.json
rename to packages/implementations/nest/tsconfig.build.json
diff --git a/packages/nest/tsconfig.json b/packages/implementations/nest/tsconfig.json
similarity index 100%
rename from packages/nest/tsconfig.json
rename to packages/implementations/nest/tsconfig.json
diff --git a/packages/web/.eslintrc.cjs b/packages/implementations/web/.eslintrc.cjs
similarity index 97%
rename from packages/web/.eslintrc.cjs
rename to packages/implementations/web/.eslintrc.cjs
index b88703d..2320ba9 100644
--- a/packages/web/.eslintrc.cjs
+++ b/packages/implementations/web/.eslintrc.cjs
@@ -33,6 +33,7 @@ module.exports = {
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
+ "@typescript-eslint/await-thenable": "warn",
"max-classes-per-file": "off",
"no-useless-constructor": "off",
"class-methods-use-this": "off",
diff --git a/packages/web/.gitignore b/packages/implementations/web/.gitignore
similarity index 100%
rename from packages/web/.gitignore
rename to packages/implementations/web/.gitignore
diff --git a/packages/web/.prettierrc b/packages/implementations/web/.prettierrc
similarity index 100%
rename from packages/web/.prettierrc
rename to packages/implementations/web/.prettierrc
diff --git a/packages/web/LICENSE.md b/packages/implementations/web/LICENSE.md
similarity index 100%
rename from packages/web/LICENSE.md
rename to packages/implementations/web/LICENSE.md
diff --git a/packages/web/README.md b/packages/implementations/web/README.md
similarity index 97%
rename from packages/web/README.md
rename to packages/implementations/web/README.md
index 23f8f86..26d3c66 100644
--- a/packages/web/README.md
+++ b/packages/implementations/web/README.md
@@ -1,4 +1,4 @@
-# GPT Turbo - Web
+# GPT Turbo - Implementation - Web
@@ -52,7 +52,7 @@ The following instructions are for deploying the web app to [Render](https://ren
4. Select the `master` branch as the branch to deploy.
5. Leave the root directory blank.
6. Set the build command to `npm run build:web`.
-7. Set the publish directory to `packages/web/dist`.
+7. Set the publish directory to `packages/implementations/web/dist`.
8. Add an environment variable `NODE_VERSION` with `lts` as the value.
9. (Optional) If you chose to link this repository (option 2), you may want to disable the "Auto-Deploy" option under the "Advanced" tab. This will prevent Render from automatically deploying the web app every time a change is made to it. You can then manually deploy the web app later.
10. Click "Create Static Site".
diff --git a/packages/web/index.html b/packages/implementations/web/index.html
similarity index 100%
rename from packages/web/index.html
rename to packages/implementations/web/index.html
diff --git a/packages/web/package.json b/packages/implementations/web/package.json
similarity index 86%
rename from packages/web/package.json
rename to packages/implementations/web/package.json
index 28a6d5f..55b2c0f 100644
--- a/packages/web/package.json
+++ b/packages/implementations/web/package.json
@@ -38,7 +38,7 @@
"react"
],
"author": "Tristan Chin ",
- "homepage": "https://github.com/maxijonson/gpt-turbo/tree/develop/packages/web#readme",
+ "homepage": "https://github.com/maxijonson/gpt-turbo/tree/develop/packages/implementations/web#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/maxijonson/gpt-turbo.git"
@@ -52,14 +52,14 @@
],
"dependencies": {
"@emotion/react": "^11.11.1",
- "@mantine/core": "^6.0.16",
- "@mantine/dropzone": "6.0.16",
- "@mantine/form": "^6.0.16",
- "@mantine/hooks": "^6.0.16",
- "@mantine/modals": "^6.0.16",
- "@mantine/notifications": "^6.0.16",
- "@mantine/prism": "^6.0.16",
- "@mantine/tiptap": "^6.0.16",
+ "@mantine/core": "^6.0.17",
+ "@mantine/dropzone": "6.0.17",
+ "@mantine/form": "^6.0.17",
+ "@mantine/hooks": "^6.0.17",
+ "@mantine/modals": "^6.0.17",
+ "@mantine/notifications": "^6.0.17",
+ "@mantine/prism": "^6.0.17",
+ "@mantine/tiptap": "^6.0.17",
"@tabler/icons-react": "^2.25.0",
"@tiptap/extension-code-block-lowlight": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
@@ -67,6 +67,7 @@
"@tiptap/react": "^2.0.3",
"@tiptap/starter-kit": "^2.0.3",
"gpt-turbo": "^4.5.0",
+ "gpt-turbo-plugin-stats": "^4.5.0",
"lowlight": "^2.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/packages/web/src/changelog/index.ts b/packages/implementations/web/src/changelog/index.ts
similarity index 82%
rename from packages/web/src/changelog/index.ts
rename to packages/implementations/web/src/changelog/index.ts
index d55b9e4..b6073cb 100644
--- a/packages/web/src/changelog/index.ts
+++ b/packages/implementations/web/src/changelog/index.ts
@@ -2,6 +2,7 @@ import { ReactNode } from "react";
import v4_4_0 from "./v4-4-0";
import v4_4_1 from "./v4-4-1";
import v4_5_0 from "./v4-5-0";
+import v5_0_0 from "./v5-0-0";
export type ChangelogEntrySection = {
label: ReactNode;
@@ -17,4 +18,4 @@ export type ChangelogEntry = {
};
// First entry is the latest version
-export const changelog = [v4_5_0, v4_4_1, v4_4_0];
+export const changelog = [v5_0_0, v4_5_0, v4_4_1, v4_4_0];
diff --git a/packages/web/src/changelog/v4-4-0.ts b/packages/implementations/web/src/changelog/v4-4-0.ts
similarity index 100%
rename from packages/web/src/changelog/v4-4-0.ts
rename to packages/implementations/web/src/changelog/v4-4-0.ts
diff --git a/packages/web/src/changelog/v4-4-1.ts b/packages/implementations/web/src/changelog/v4-4-1.ts
similarity index 100%
rename from packages/web/src/changelog/v4-4-1.ts
rename to packages/implementations/web/src/changelog/v4-4-1.ts
diff --git a/packages/web/src/changelog/v4-5-0.ts b/packages/implementations/web/src/changelog/v4-5-0.ts
similarity index 100%
rename from packages/web/src/changelog/v4-5-0.ts
rename to packages/implementations/web/src/changelog/v4-5-0.ts
diff --git a/packages/implementations/web/src/changelog/v5-0-0.ts b/packages/implementations/web/src/changelog/v5-0-0.ts
new file mode 100644
index 0000000..56f22eb
--- /dev/null
+++ b/packages/implementations/web/src/changelog/v5-0-0.ts
@@ -0,0 +1,26 @@
+import { ChangelogEntry } from ".";
+import { CHANGELOG_SECTION } from "../config/constants";
+
+const v5_0_0: ChangelogEntry = {
+ version: "5.0.0 - Library rewrite and plugin system",
+ date: new Date("july 30 2023"),
+ description:
+ "This new update is primarily focused on the library side of the project, so there shouldn't be any major noticeable changes here. However, under the hood, the library has been completely rewritten to be more modular and extensible with plugins. The first plugin to exist is actually the usage stats feature, which has been moved out from the underlying library and into a conversation plugin!",
+ sections: [
+ {
+ label: CHANGELOG_SECTION.IMPROVEMENTS,
+ items: [
+ "Integrated the stats plugin",
+ "Stats are now updated in real time",
+ "Improve the crash screen to show the error message and stack trace",
+ "Improve the crash screen to pre-fill the bug report on GitHub",
+ ],
+ },
+ {
+ label: CHANGELOG_SECTION.FIXES,
+ items: ["Fixed the usage stats not updating correctly sometimes"],
+ },
+ ],
+};
+
+export default v5_0_0;
diff --git a/packages/web/src/components/About/About.tsx b/packages/implementations/web/src/components/About/About.tsx
similarity index 100%
rename from packages/web/src/components/About/About.tsx
rename to packages/implementations/web/src/components/About/About.tsx
diff --git a/packages/web/src/components/About/AboutButton.tsx b/packages/implementations/web/src/components/About/AboutButton.tsx
similarity index 100%
rename from packages/web/src/components/About/AboutButton.tsx
rename to packages/implementations/web/src/components/About/AboutButton.tsx
diff --git a/packages/web/src/components/AddConversation.tsx b/packages/implementations/web/src/components/AddConversation.tsx
similarity index 76%
rename from packages/web/src/components/AddConversation.tsx
rename to packages/implementations/web/src/components/AddConversation.tsx
index 2ceb4c4..39513c4 100644
--- a/packages/web/src/components/AddConversation.tsx
+++ b/packages/implementations/web/src/components/AddConversation.tsx
@@ -40,24 +40,35 @@ const AddConversation = () => {
const { classes } = useStyles();
const dropzoneOpenRef = React.useRef<() => void>(null);
- const onSubmit = React.useCallback(
- ({
- save,
- headers,
- proxy,
- functionIds,
- ...values
- }: ConversationFormValues) => {
- const newConversation = addConversation(
- values,
- { headers, proxy },
- functionIds,
- save
- );
- setActiveConversation(newConversation.id, true);
- },
- []
- );
+ const onSubmit = React.useCallback((values: ConversationFormValues) => {
+ const newConversation = addConversation(
+ {
+ config: {
+ apiKey: values.apiKey,
+ model: values.model,
+ context: values.context,
+ dry: values.dry,
+ disableModeration: values.disableModeration,
+ stream: values.stream,
+ temperature: values.temperature,
+ top_p: values.top_p,
+ frequency_penalty: values.frequency_penalty,
+ presence_penalty: values.presence_penalty,
+ stop: values.stop,
+ max_tokens: values.max_tokens,
+ logit_bias: values.logit_bias,
+ user: values.user,
+ },
+ requestOptions: {
+ headers: values.headers,
+ proxy: values.proxy,
+ },
+ },
+ values.functionIds,
+ values.save
+ );
+ setActiveConversation(newConversation.id, true);
+ }, []);
const onDrop = React.useCallback(
async (importedConversations) => {
diff --git a/packages/web/src/components/AppSettings/AppSettings.tsx b/packages/implementations/web/src/components/AppSettings/AppSettings.tsx
similarity index 100%
rename from packages/web/src/components/AppSettings/AppSettings.tsx
rename to packages/implementations/web/src/components/AppSettings/AppSettings.tsx
diff --git a/packages/web/src/components/AppSettings/AppSettingsDangerZone.tsx b/packages/implementations/web/src/components/AppSettings/AppSettingsDangerZone.tsx
similarity index 100%
rename from packages/web/src/components/AppSettings/AppSettingsDangerZone.tsx
rename to packages/implementations/web/src/components/AppSettings/AppSettingsDangerZone.tsx
diff --git a/packages/web/src/components/AppSettings/AppStorageUsage.tsx b/packages/implementations/web/src/components/AppSettings/AppStorageUsage.tsx
similarity index 100%
rename from packages/web/src/components/AppSettings/AppStorageUsage.tsx
rename to packages/implementations/web/src/components/AppSettings/AppStorageUsage.tsx
diff --git a/packages/web/src/components/AppSettings/DataSettings.tsx b/packages/implementations/web/src/components/AppSettings/DataSettings.tsx
similarity index 100%
rename from packages/web/src/components/AppSettings/DataSettings.tsx
rename to packages/implementations/web/src/components/AppSettings/DataSettings.tsx
diff --git a/packages/web/src/components/AppSettings/GeneralSettings.tsx b/packages/implementations/web/src/components/AppSettings/GeneralSettings.tsx
similarity index 100%
rename from packages/web/src/components/AppSettings/GeneralSettings.tsx
rename to packages/implementations/web/src/components/AppSettings/GeneralSettings.tsx
diff --git a/packages/web/src/components/CallableFunctionCard/CallableFunctionCard.tsx b/packages/implementations/web/src/components/CallableFunctionCard/CallableFunctionCard.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionCard/CallableFunctionCard.tsx
rename to packages/implementations/web/src/components/CallableFunctionCard/CallableFunctionCard.tsx
diff --git a/packages/web/src/components/CallableFunctionCard/CallableFunctionCardMenu.tsx b/packages/implementations/web/src/components/CallableFunctionCard/CallableFunctionCardMenu.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionCard/CallableFunctionCardMenu.tsx
rename to packages/implementations/web/src/components/CallableFunctionCard/CallableFunctionCardMenu.tsx
diff --git a/packages/web/src/components/CallableFunctionCreateButton.tsx b/packages/implementations/web/src/components/CallableFunctionCreateButton.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionCreateButton.tsx
rename to packages/implementations/web/src/components/CallableFunctionCreateButton.tsx
diff --git a/packages/web/src/components/CallableFunctionImport/CallableFunctionImport.tsx b/packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImport.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionImport/CallableFunctionImport.tsx
rename to packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImport.tsx
diff --git a/packages/web/src/components/CallableFunctionImport/CallableFunctionImportButton.tsx b/packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImportButton.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionImport/CallableFunctionImportButton.tsx
rename to packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImportButton.tsx
diff --git a/packages/web/src/components/CallableFunctionImport/CallableFunctionImportDropzone.tsx b/packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImportDropzone.tsx
similarity index 100%
rename from packages/web/src/components/CallableFunctionImport/CallableFunctionImportDropzone.tsx
rename to packages/implementations/web/src/components/CallableFunctionImport/CallableFunctionImportDropzone.tsx
diff --git a/packages/web/src/components/Changelog/Changelog.tsx b/packages/implementations/web/src/components/Changelog/Changelog.tsx
similarity index 100%
rename from packages/web/src/components/Changelog/Changelog.tsx
rename to packages/implementations/web/src/components/Changelog/Changelog.tsx
diff --git a/packages/web/src/components/Changelog/ChangelogButton.tsx b/packages/implementations/web/src/components/Changelog/ChangelogButton.tsx
similarity index 100%
rename from packages/web/src/components/Changelog/ChangelogButton.tsx
rename to packages/implementations/web/src/components/Changelog/ChangelogButton.tsx
diff --git a/packages/web/src/components/ConversationNavbar/ConversationNavbar.tsx b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbar.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/ConversationNavbar.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/ConversationNavbar.tsx
diff --git a/packages/web/src/components/ConversationNavbar/ConversationNavbarBurger.tsx b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarBurger.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/ConversationNavbarBurger.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarBurger.tsx
diff --git a/packages/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx
similarity index 97%
rename from packages/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx
index 06b8532..bf01c32 100644
--- a/packages/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx
+++ b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarFooter/ConversationNavbarFooter.tsx
@@ -12,7 +12,7 @@ const ConversationNavbarFooter = () => {
diff --git a/packages/web/src/components/ConversationNavbar/ConversationNavbarHeader/ConversationNavbarHeader.tsx b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarHeader/ConversationNavbarHeader.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/ConversationNavbarHeader/ConversationNavbarHeader.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarHeader/ConversationNavbarHeader.tsx
diff --git a/packages/web/src/components/ConversationNavbar/ConversationNavbarHeader/SettingsButton.tsx b/packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarHeader/SettingsButton.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/ConversationNavbarHeader/SettingsButton.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/ConversationNavbarHeader/SettingsButton.tsx
diff --git a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConverationInfoIcon.tsx b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConverationInfoIcon.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConverationInfoIcon.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConverationInfoIcon.tsx
diff --git a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversation.tsx b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversation.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversation.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversation.tsx
diff --git a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversationMenu.tsx b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversationMenu.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversationMenu.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversation/NavbarConversationMenu.tsx
diff --git a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx
similarity index 96%
rename from packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx
index 673d2fd..b791d42 100644
--- a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx
+++ b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversationInfo.tsx
@@ -28,7 +28,8 @@ const SIZE = 14;
const NavbarConversationInfo = ({
conversation,
}: NavbarConversationInfoProps) => {
- const { model, dry, disableModeration, stream } = conversation.getConfig();
+ const { model, dry, disableModeration, stream } =
+ conversation.config.getConfig();
const persistedConversationIds = useAppStore(
(state) => state.persistedConversationIds
);
diff --git a/packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversations.tsx b/packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversations.tsx
similarity index 100%
rename from packages/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversations.tsx
rename to packages/implementations/web/src/components/ConversationNavbar/NavbarConversations/NavbarConversations.tsx
diff --git a/packages/web/src/components/ConversationPageShell.tsx b/packages/implementations/web/src/components/ConversationPageShell.tsx
similarity index 100%
rename from packages/web/src/components/ConversationPageShell.tsx
rename to packages/implementations/web/src/components/ConversationPageShell.tsx
diff --git a/packages/web/src/components/Messages/CodeBlock.tsx b/packages/implementations/web/src/components/Messages/CodeBlock.tsx
similarity index 100%
rename from packages/web/src/components/Messages/CodeBlock.tsx
rename to packages/implementations/web/src/components/Messages/CodeBlock.tsx
diff --git a/packages/web/src/components/Messages/Message.tsx b/packages/implementations/web/src/components/Messages/Message.tsx
similarity index 100%
rename from packages/web/src/components/Messages/Message.tsx
rename to packages/implementations/web/src/components/Messages/Message.tsx
diff --git a/packages/web/src/components/Messages/Messages.tsx b/packages/implementations/web/src/components/Messages/Messages.tsx
similarity index 84%
rename from packages/web/src/components/Messages/Messages.tsx
rename to packages/implementations/web/src/components/Messages/Messages.tsx
index f6941b3..f0c5c9f 100644
--- a/packages/web/src/components/Messages/Messages.tsx
+++ b/packages/implementations/web/src/components/Messages/Messages.tsx
@@ -23,7 +23,7 @@ const useStyles = createStyles(() => ({
const Messages = () => {
const conversation = useActiveConversation();
const [messages, setMessages] = React.useState(
- conversation?.getMessages() ?? []
+ conversation?.history.getMessages() ?? []
);
const callFunction = useCallFunction();
const viewport = React.useRef(null);
@@ -73,7 +73,7 @@ const Messages = () => {
React.useEffect(() => {
if (!conversation) return;
const unsubscribes: (() => void)[] = [];
- const unsubscribeMessageAdded = conversation.onMessageAdded(
+ const unsubscribeMessageAdded = conversation.history.onMessageAdded(
(message) => {
if (message.role === "system") return;
setMessages((messages) => [...messages, message]);
@@ -83,26 +83,30 @@ const Messages = () => {
if (message.role !== "assistant") return;
unsubscribes.push(
- message.onMessageUpdate(() => {
+ message.onUpdate(() => {
setMessages((messages) => [...messages]); // Force re-render by creating a new array
if (isSticky) scrollToBottom();
})
);
- const isStreaming = conversation.getConfig().stream;
+ const isStreaming = conversation.config.getConfig().stream;
if (isStreaming) {
- const unsubscribeStreamingStop =
- message.onMessageStreamingStop((message) => {
+ const unsubscribeStreamingStop = message.onStreamingStop(
+ (message) => {
unsubscribeStreamingStop();
if (!message.isFunctionCall()) return;
handleFunctionCall(
message,
- conversation.getFunctions()
+ conversation.callableFunctions.getFunctions()
);
- });
+ }
+ );
unsubscribes.push(unsubscribeStreamingStop);
} else if (message.isFunctionCall()) {
- handleFunctionCall(message, conversation.getFunctions());
+ handleFunctionCall(
+ message,
+ conversation.callableFunctions.getFunctions()
+ );
}
}
);
@@ -115,7 +119,7 @@ const Messages = () => {
React.useEffect(() => {
if (!conversation) return;
- return conversation.onMessageRemoved((message) => {
+ return conversation.history.onMessageRemoved((message) => {
setMessages((messages) =>
messages.filter((m) => m.id !== message.id)
);
@@ -123,7 +127,7 @@ const Messages = () => {
}, [conversation]);
React.useEffect(() => {
- setMessages(conversation?.getMessages() ?? []);
+ setMessages(conversation?.history.getMessages() ?? []);
}, [conversation]);
return (
diff --git a/packages/web/src/components/Usage/Usage.tsx b/packages/implementations/web/src/components/Usage/Usage.tsx
similarity index 77%
rename from packages/web/src/components/Usage/Usage.tsx
rename to packages/implementations/web/src/components/Usage/Usage.tsx
index a0db6b8..db1aca8 100644
--- a/packages/web/src/components/Usage/Usage.tsx
+++ b/packages/implementations/web/src/components/Usage/Usage.tsx
@@ -4,6 +4,9 @@ import UsageMetric from "./UsageMetric";
import getPriceString from "../../utils/getPriceString";
import { useGetConversationName } from "../../store/hooks/conversations/useGetConversationName";
import { DEFAULT_CONVERSATION_NAME } from "../../config/constants";
+import { statsPluginName } from "gpt-turbo-plugin-stats";
+import React from "react";
+import { useForceUpdate } from "@mantine/hooks";
interface UsageProps {
conversation: Conversation;
@@ -11,6 +14,8 @@ interface UsageProps {
const Usage = ({ conversation }: UsageProps) => {
const getConversationName = useGetConversationName();
+ const forceUpdate = useForceUpdate();
+ const stats = conversation.plugins.getPluginOutput(statsPluginName);
const metrics: {
label: React.ReactNode;
@@ -19,26 +24,30 @@ const Usage = ({ conversation }: UsageProps) => {
}[] = [
{
label: "Cumulative Size",
- value: `${conversation.getCumulativeSize()} tkns`,
+ value: `${stats.cumulativeSize} tkns`,
description: "Cumulative number of tokens sent to OpenAI",
},
{
label: "Cumulative Cost",
- value: getPriceString(conversation.getCumulativeCost()),
+ value: getPriceString(stats.cumulativeCost),
description: "Cumulative cost of tokens sent to OpenAI",
},
{
label: "Conversation Size",
- value: `${conversation.getSize()} tkns`,
+ value: `${stats.size} tkns`,
description: "Current number of tokens in this conversation",
},
{
label: "Conversation Cost",
- value: getPriceString(conversation.getCost()),
+ value: getPriceString(stats.cost),
description: "Current cost of tokens in this conversation",
},
];
+ React.useEffect(() => {
+ stats.onStatsUpdate(() => forceUpdate());
+ }, [forceUpdate, stats]);
+
return (
diff --git a/packages/web/src/components/Usage/UsageMetric.tsx b/packages/implementations/web/src/components/Usage/UsageMetric.tsx
similarity index 100%
rename from packages/web/src/components/Usage/UsageMetric.tsx
rename to packages/implementations/web/src/components/Usage/UsageMetric.tsx
diff --git a/packages/web/src/components/common/ContentLoader.tsx b/packages/implementations/web/src/components/common/ContentLoader.tsx
similarity index 100%
rename from packages/web/src/components/common/ContentLoader.tsx
rename to packages/implementations/web/src/components/common/ContentLoader.tsx
diff --git a/packages/web/src/components/common/InlineConfirmButton.tsx b/packages/implementations/web/src/components/common/InlineConfirmButton.tsx
similarity index 100%
rename from packages/web/src/components/common/InlineConfirmButton.tsx
rename to packages/implementations/web/src/components/common/InlineConfirmButton.tsx
diff --git a/packages/web/src/components/common/TippedActionIcon.tsx b/packages/implementations/web/src/components/common/TippedActionIcon.tsx
similarity index 100%
rename from packages/web/src/components/common/TippedActionIcon.tsx
rename to packages/implementations/web/src/components/common/TippedActionIcon.tsx
diff --git a/packages/implementations/web/src/components/error-handling/AppCatcher.tsx b/packages/implementations/web/src/components/error-handling/AppCatcher.tsx
new file mode 100644
index 0000000..91867fc
--- /dev/null
+++ b/packages/implementations/web/src/components/error-handling/AppCatcher.tsx
@@ -0,0 +1,38 @@
+import React, { ErrorInfo } from "react";
+import AppErrorModal from "./AppErrorModal";
+
+export interface AppCatcherProps {
+ children?: React.ReactNode;
+}
+
+export interface AppCatcherState {
+ error: Error | null;
+}
+
+class AppCatcher extends React.Component {
+ constructor(props: AppCatcherProps) {
+ super(props);
+ this.state = { error: null };
+ }
+
+ static getDerivedStateFromError(error: Error) {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error(error, errorInfo);
+ }
+
+ render() {
+ const { error } = this.state;
+ const { children } = this.props;
+
+ if (!error) {
+ return children;
+ }
+
+ return ;
+ }
+}
+
+export default AppCatcher;
diff --git a/packages/implementations/web/src/components/error-handling/AppErrorModal.tsx b/packages/implementations/web/src/components/error-handling/AppErrorModal.tsx
new file mode 100644
index 0000000..b4ecf1b
--- /dev/null
+++ b/packages/implementations/web/src/components/error-handling/AppErrorModal.tsx
@@ -0,0 +1,162 @@
+import {
+ Modal,
+ Title,
+ ScrollArea,
+ Stack,
+ Container,
+ List,
+ Group,
+ Button,
+ Text,
+ Collapse,
+ Box,
+ Divider,
+ Textarea,
+} from "@mantine/core";
+import { useDisclosure } from "@mantine/hooks";
+import React from "react";
+import { BiBug, BiTrash, BiRefresh, BiLogoGithub } from "react-icons/bi";
+import TippedActionIcon from "../common/TippedActionIcon";
+import getErrorInfo from "../../utils/getErrorInfo";
+import { storeVersion } from "../../store/persist/migrations";
+import getIssueLink, { blockTicks } from "../../utils/getIssueLink";
+
+interface AppErrorModalProps {
+ error: Error;
+}
+
+const AppErrorModal = ({ error }: AppErrorModalProps) => {
+ const [showError, { toggle: toggleShowError }] = useDisclosure();
+
+ const reportLink = React.useMemo(() => {
+ const title = "[Bug]: Web Fatal Error";
+
+ const info = getErrorInfo(error);
+ const content = {
+ "App Version": APP_VERSION,
+ "Store Version": storeVersion,
+ "Error Info": `${blockTicks}\n${info.title}\n${info.message}\n${blockTicks}`,
+ "Stack Trace": `${blockTicks}\n${error.stack}\n${blockTicks}`,
+ };
+ return getIssueLink(title, content);
+ }, [error]);
+
+ return (
+ {}}
+ centered
+ title={
+
+ Fatal error
+
+ }
+ color="red"
+ size="xl"
+ scrollAreaComponent={ScrollArea.Autosize}
+ >
+
+
+
+ The application encountered a fatal error. Try to reload
+ the page. If the issue persists, try opening the app in
+ a private tab:
+
+
+ If the app now works in a private tab, you might
+ have invalid or outdated local storage data in
+ your browser. Consider clearing your local
+ storage data{" "}
+
+ which will clear all your settings and
+ conversations
+
+
+
+ If the app still does not work in a private tab,
+ you might have encountered a bug. You may report
+ it on the project's project's GitHub issues
+ page. In order to help us resolve this issue,
+ please include the error information below by
+ clicking on the{" "}
+
+
+ {" "}
+ icon or the "Report issue on GitHub" button to
+ pre-fill this information!
+
+
+
+
+
+
+
+
+ }
+ labelPosition="center"
+ mb="xs"
+ />
+
+
+
+
+
+ }
+ component="a"
+ target="_blank"
+ href={reportLink}
+ >
+ Report on GitHub
+
+ }
+ onClick={() => {
+ localStorage.clear();
+ window.location.reload();
+ }}
+ >
+ Clear local storage
+
+ }
+ onClick={() => window.location.reload()}
+ >
+ Reload
+
+
+
+
+ );
+};
+
+export default AppErrorModal;
diff --git a/packages/implementations/web/src/components/error-handling/RouterCatcher.tsx b/packages/implementations/web/src/components/error-handling/RouterCatcher.tsx
new file mode 100644
index 0000000..afc00fd
--- /dev/null
+++ b/packages/implementations/web/src/components/error-handling/RouterCatcher.tsx
@@ -0,0 +1,9 @@
+import { useRouteError } from "react-router-dom";
+import AppErrorModal from "./AppErrorModal";
+
+const RouterCatcher = () => {
+ const error = useRouteError();
+ return ;
+};
+
+export default RouterCatcher;
diff --git a/packages/web/src/components/error-handling/StorageLoadError.tsx b/packages/implementations/web/src/components/error-handling/StorageLoadError.tsx
similarity index 85%
rename from packages/web/src/components/error-handling/StorageLoadError.tsx
rename to packages/implementations/web/src/components/error-handling/StorageLoadError.tsx
index 2374fa3..f76b575 100644
--- a/packages/web/src/components/error-handling/StorageLoadError.tsx
+++ b/packages/implementations/web/src/components/error-handling/StorageLoadError.tsx
@@ -10,9 +10,10 @@ import {
import React from "react";
import getErrorInfo from "../../utils/getErrorInfo";
import { STORAGE_PERSISTENCE_KEY } from "../../config/constants";
-import { BiBug } from "react-icons/bi";
+import { BiLogoGithub } from "react-icons/bi";
import { BsFire } from "react-icons/bs";
import { storeVersion } from "../../store/persist/migrations";
+import getIssueLink, { blockTicks } from "../../utils/getIssueLink";
interface StorageLoadErrorProps {
isMigrationError: boolean;
@@ -26,26 +27,20 @@ const StorageLoadError = ({
currentData,
}: StorageLoadErrorProps) => {
const reportLink = React.useMemo(() => {
- const link = "https://github.com/maxijonson/gpt-turbo/issues/new";
-
const title = `[Bug]: Web Storage ${
isMigrationError ? "Migration" : "Load"
} Error`;
const info = getErrorInfo(error);
- let body = `**App Version**:\n${APP_VERSION}\n\n`;
- body += `**Store Version**:\n${storeVersion}\n\n`;
- body += `**Is Migration Error**:\n${isMigrationError}\n\n`;
- body += `**Error Info**:\n\`\`\`\n${info.title}\n${info.message}\n\`\`\`\n\n`;
- body += `**Stack Trace**:\n\`\`\`\n${error.stack}\n\`\`\`\n\n`;
- body += `**Current Data**:\n\`\`\`json\n${currentData}\n\`\`\``;
-
- const params = new URLSearchParams({
- title,
- body,
- });
-
- return `${link}?${params.toString()}`;
+ const content = {
+ "App Version": APP_VERSION,
+ "Store Version": storeVersion,
+ "Is Migration Error": isMigrationError,
+ "Error Info": `${blockTicks}\n${info.title}\n${info.message}\n${blockTicks}`,
+ "Stack Trace": `${blockTicks}\n${error.stack}\n${blockTicks}`,
+ "Current Data": `${blockTicks}json\n${currentData}\n${blockTicks}`,
+ };
+ return getIssueLink(title, content);
}, [currentData, error, isMigrationError]);
return (
@@ -108,7 +103,7 @@ const StorageLoadError = ({
}
+ leftIcon={}
component="a"
target="_blank"
href={reportLink}
diff --git a/packages/web/src/components/forms/CallableFunctionForm/CallableFunctionForm.tsx b/packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionForm.tsx
similarity index 100%
rename from packages/web/src/components/forms/CallableFunctionForm/CallableFunctionForm.tsx
rename to packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionForm.tsx
diff --git a/packages/web/src/components/forms/CallableFunctionForm/CallableFunctionFormCode.tsx b/packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionFormCode.tsx
similarity index 100%
rename from packages/web/src/components/forms/CallableFunctionForm/CallableFunctionFormCode.tsx
rename to packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionFormCode.tsx
diff --git a/packages/web/src/components/forms/CallableFunctionForm/CallableFunctionFormParameters.tsx b/packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionFormParameters.tsx
similarity index 100%
rename from packages/web/src/components/forms/CallableFunctionForm/CallableFunctionFormParameters.tsx
rename to packages/implementations/web/src/components/forms/CallableFunctionForm/CallableFunctionFormParameters.tsx
diff --git a/packages/web/src/components/forms/CallableFunctionForm/CodeEditor.tsx b/packages/implementations/web/src/components/forms/CallableFunctionForm/CodeEditor.tsx
similarity index 100%
rename from packages/web/src/components/forms/CallableFunctionForm/CodeEditor.tsx
rename to packages/implementations/web/src/components/forms/CallableFunctionForm/CodeEditor.tsx
diff --git a/packages/web/src/components/forms/CallableFunctionParameterForm/CallableFunctionParameterForm.tsx b/packages/implementations/web/src/components/forms/CallableFunctionParameterForm/CallableFunctionParameterForm.tsx
similarity index 100%
rename from packages/web/src/components/forms/CallableFunctionParameterForm/CallableFunctionParameterForm.tsx
rename to packages/implementations/web/src/components/forms/CallableFunctionParameterForm/CallableFunctionParameterForm.tsx
diff --git a/packages/web/src/components/forms/ConversationForm/ConversationForm.tsx b/packages/implementations/web/src/components/forms/ConversationForm/ConversationForm.tsx
similarity index 100%
rename from packages/web/src/components/forms/ConversationForm/ConversationForm.tsx
rename to packages/implementations/web/src/components/forms/ConversationForm/ConversationForm.tsx
diff --git a/packages/web/src/components/forms/ConversationForm/ConversationFormAdvancedTab.tsx b/packages/implementations/web/src/components/forms/ConversationForm/ConversationFormAdvancedTab.tsx
similarity index 100%
rename from packages/web/src/components/forms/ConversationForm/ConversationFormAdvancedTab.tsx
rename to packages/implementations/web/src/components/forms/ConversationForm/ConversationFormAdvancedTab.tsx
diff --git a/packages/web/src/components/forms/ConversationForm/ConversationFormConversationTab.tsx b/packages/implementations/web/src/components/forms/ConversationForm/ConversationFormConversationTab.tsx
similarity index 100%
rename from packages/web/src/components/forms/ConversationForm/ConversationFormConversationTab.tsx
rename to packages/implementations/web/src/components/forms/ConversationForm/ConversationFormConversationTab.tsx
diff --git a/packages/web/src/components/forms/ConversationForm/ConversationFormFunctionsTab.tsx b/packages/implementations/web/src/components/forms/ConversationForm/ConversationFormFunctionsTab.tsx
similarity index 100%
rename from packages/web/src/components/forms/ConversationForm/ConversationFormFunctionsTab.tsx
rename to packages/implementations/web/src/components/forms/ConversationForm/ConversationFormFunctionsTab.tsx
diff --git a/packages/web/src/components/forms/ConversationForm/ConversationFormRequestTab.tsx b/packages/implementations/web/src/components/forms/ConversationForm/ConversationFormRequestTab.tsx
similarity index 100%
rename from packages/web/src/components/forms/ConversationForm/ConversationFormRequestTab.tsx
rename to packages/implementations/web/src/components/forms/ConversationForm/ConversationFormRequestTab.tsx
diff --git a/packages/web/src/components/inputs/ApiKeyInput.tsx b/packages/implementations/web/src/components/inputs/ApiKeyInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/ApiKeyInput.tsx
rename to packages/implementations/web/src/components/inputs/ApiKeyInput.tsx
diff --git a/packages/web/src/components/inputs/ContextInput.tsx b/packages/implementations/web/src/components/inputs/ContextInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/ContextInput.tsx
rename to packages/implementations/web/src/components/inputs/ContextInput.tsx
diff --git a/packages/web/src/components/inputs/ConversationDropzone.tsx b/packages/implementations/web/src/components/inputs/ConversationDropzone.tsx
similarity index 100%
rename from packages/web/src/components/inputs/ConversationDropzone.tsx
rename to packages/implementations/web/src/components/inputs/ConversationDropzone.tsx
diff --git a/packages/web/src/components/inputs/DisableModerationInput.tsx b/packages/implementations/web/src/components/inputs/DisableModerationInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/DisableModerationInput.tsx
rename to packages/implementations/web/src/components/inputs/DisableModerationInput.tsx
diff --git a/packages/web/src/components/inputs/DryInput.tsx b/packages/implementations/web/src/components/inputs/DryInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/DryInput.tsx
rename to packages/implementations/web/src/components/inputs/DryInput.tsx
diff --git a/packages/web/src/components/inputs/HeadersInput.tsx b/packages/implementations/web/src/components/inputs/HeadersInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/HeadersInput.tsx
rename to packages/implementations/web/src/components/inputs/HeadersInput.tsx
diff --git a/packages/web/src/components/inputs/LogitBiasInput.tsx b/packages/implementations/web/src/components/inputs/LogitBiasInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/LogitBiasInput.tsx
rename to packages/implementations/web/src/components/inputs/LogitBiasInput.tsx
diff --git a/packages/web/src/components/inputs/ModelSelectInput/ModelSelectInput.tsx b/packages/implementations/web/src/components/inputs/ModelSelectInput/ModelSelectInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/ModelSelectInput/ModelSelectInput.tsx
rename to packages/implementations/web/src/components/inputs/ModelSelectInput/ModelSelectInput.tsx
diff --git a/packages/web/src/components/inputs/ModelSelectInput/ModelSelectItem.tsx b/packages/implementations/web/src/components/inputs/ModelSelectInput/ModelSelectItem.tsx
similarity index 100%
rename from packages/web/src/components/inputs/ModelSelectInput/ModelSelectItem.tsx
rename to packages/implementations/web/src/components/inputs/ModelSelectInput/ModelSelectItem.tsx
diff --git a/packages/web/src/components/inputs/OptionalBooleanInput.tsx b/packages/implementations/web/src/components/inputs/OptionalBooleanInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/OptionalBooleanInput.tsx
rename to packages/implementations/web/src/components/inputs/OptionalBooleanInput.tsx
diff --git a/packages/web/src/components/inputs/OptionalNumberInput.tsx b/packages/implementations/web/src/components/inputs/OptionalNumberInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/OptionalNumberInput.tsx
rename to packages/implementations/web/src/components/inputs/OptionalNumberInput.tsx
diff --git a/packages/web/src/components/inputs/OptionalTextInput.tsx b/packages/implementations/web/src/components/inputs/OptionalTextInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/OptionalTextInput.tsx
rename to packages/implementations/web/src/components/inputs/OptionalTextInput.tsx
diff --git a/packages/web/src/components/inputs/Prompt/Prompt.tsx b/packages/implementations/web/src/components/inputs/Prompt/Prompt.tsx
similarity index 98%
rename from packages/web/src/components/inputs/Prompt/Prompt.tsx
rename to packages/implementations/web/src/components/inputs/Prompt/Prompt.tsx
index 35c7add..7539c36 100644
--- a/packages/web/src/components/inputs/Prompt/Prompt.tsx
+++ b/packages/implementations/web/src/components/inputs/Prompt/Prompt.tsx
@@ -73,7 +73,7 @@ const Prompt = () => {
const message = await conversation.prompt(values.prompt);
setConversationLastEdit(conversation.id);
if (message) {
- message.onMessageStreamingUpdate((isStreaming) => {
+ message.onStreamingUpdate((isStreaming) => {
setIsStreaming(isStreaming);
});
if (message.isStreaming) {
diff --git a/packages/web/src/components/inputs/Prompt/PromptUsage.tsx b/packages/implementations/web/src/components/inputs/Prompt/PromptUsage.tsx
similarity index 87%
rename from packages/web/src/components/inputs/Prompt/PromptUsage.tsx
rename to packages/implementations/web/src/components/inputs/Prompt/PromptUsage.tsx
index e9142ee..042ba65 100644
--- a/packages/web/src/components/inputs/Prompt/PromptUsage.tsx
+++ b/packages/implementations/web/src/components/inputs/Prompt/PromptUsage.tsx
@@ -1,5 +1,6 @@
import { Tooltip, Stack, Group, Button, Text } from "@mantine/core";
-import { getMessageSize, getMessageCost, Conversation } from "gpt-turbo";
+import { Conversation } from "gpt-turbo";
+import { getMessageSize, getMessageCost } from "gpt-turbo-plugin-stats";
import getPriceString from "../../../utils/getPriceString";
import React from "react";
@@ -17,7 +18,7 @@ const PromptUsage = ({ prompt, conversation }: PromptUsageProps) => {
: getPriceString(
getMessageCost(
prompt,
- conversation.getConfig().model,
+ conversation.config.getConfig().model,
"prompt"
)
);
@@ -37,7 +38,7 @@ const PromptUsage = ({ prompt, conversation }: PromptUsageProps) => {
{getPriceString(
getMessageCost(
prompt,
- conversation.getConfig().model,
+ conversation.config.getConfig().model,
"prompt"
)
)}
diff --git a/packages/web/src/components/inputs/ProxyInput.tsx b/packages/implementations/web/src/components/inputs/ProxyInput.tsx
similarity index 90%
rename from packages/web/src/components/inputs/ProxyInput.tsx
rename to packages/implementations/web/src/components/inputs/ProxyInput.tsx
index 5590348..d02845c 100644
--- a/packages/web/src/components/inputs/ProxyInput.tsx
+++ b/packages/implementations/web/src/components/inputs/ProxyInput.tsx
@@ -10,12 +10,14 @@ import {
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form";
-import { RequestOptionsProxy } from "gpt-turbo";
+import { ConversationRequestOptionsModel } from "gpt-turbo";
import React from "react";
interface ProxyInputProps {
- value: RequestOptionsProxy | undefined;
- onChange: (value: RequestOptionsProxy | undefined) => void;
+ value: ConversationRequestOptionsModel["proxy"] | undefined;
+ onChange: (
+ value: ConversationRequestOptionsModel["proxy"] | undefined
+ ) => void;
}
const ProxyInput = ({ value, onChange }: ProxyInputProps) => {
@@ -31,7 +33,7 @@ const ProxyInput = ({ value, onChange }: ProxyInputProps) => {
React.useEffect(() => {
const { host, port, protocol, username, password } = form.values;
- const next: RequestOptionsProxy | undefined = host
+ const next: ConversationRequestOptionsModel["proxy"] | undefined = host
? {
host,
port,
diff --git a/packages/web/src/components/inputs/SaveInput.tsx b/packages/implementations/web/src/components/inputs/SaveInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/SaveInput.tsx
rename to packages/implementations/web/src/components/inputs/SaveInput.tsx
diff --git a/packages/web/src/components/inputs/StopInput.tsx b/packages/implementations/web/src/components/inputs/StopInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/StopInput.tsx
rename to packages/implementations/web/src/components/inputs/StopInput.tsx
diff --git a/packages/web/src/components/inputs/StreamInput.tsx b/packages/implementations/web/src/components/inputs/StreamInput.tsx
similarity index 100%
rename from packages/web/src/components/inputs/StreamInput.tsx
rename to packages/implementations/web/src/components/inputs/StreamInput.tsx
diff --git a/packages/web/src/components/modals/ConversationNameEditModal.tsx b/packages/implementations/web/src/components/modals/ConversationNameEditModal.tsx
similarity index 100%
rename from packages/web/src/components/modals/ConversationNameEditModal.tsx
rename to packages/implementations/web/src/components/modals/ConversationNameEditModal.tsx
diff --git a/packages/web/src/components/modals/SavePromptModalBody.tsx b/packages/implementations/web/src/components/modals/SavePromptModalBody.tsx
similarity index 100%
rename from packages/web/src/components/modals/SavePromptModalBody.tsx
rename to packages/implementations/web/src/components/modals/SavePromptModalBody.tsx
diff --git a/packages/web/src/components/modals/SavedPromptsModalBody.tsx b/packages/implementations/web/src/components/modals/SavedPromptsModalBody.tsx
similarity index 100%
rename from packages/web/src/components/modals/SavedPromptsModalBody.tsx
rename to packages/implementations/web/src/components/modals/SavedPromptsModalBody.tsx
diff --git a/packages/web/src/components/warnings/FunctionsImportWarning.tsx b/packages/implementations/web/src/components/warnings/FunctionsImportWarning.tsx
similarity index 100%
rename from packages/web/src/components/warnings/FunctionsImportWarning.tsx
rename to packages/implementations/web/src/components/warnings/FunctionsImportWarning.tsx
diff --git a/packages/web/src/components/warnings/FunctionsWarning.tsx b/packages/implementations/web/src/components/warnings/FunctionsWarning.tsx
similarity index 100%
rename from packages/web/src/components/warnings/FunctionsWarning.tsx
rename to packages/implementations/web/src/components/warnings/FunctionsWarning.tsx
diff --git a/packages/web/src/config/constants.ts b/packages/implementations/web/src/config/constants.ts
similarity index 100%
rename from packages/web/src/config/constants.ts
rename to packages/implementations/web/src/config/constants.ts
diff --git a/packages/implementations/web/src/config/gpt-turbo.ts b/packages/implementations/web/src/config/gpt-turbo.ts
new file mode 100644
index 0000000..60d1755
--- /dev/null
+++ b/packages/implementations/web/src/config/gpt-turbo.ts
@@ -0,0 +1,10 @@
+import { Conversation } from "gpt-turbo";
+import statsPlugin from "gpt-turbo-plugin-stats";
+
+const globalPlugins = [statsPlugin];
+Conversation.globalPlugins = [statsPlugin];
+declare module "gpt-turbo" {
+ export interface ConversationGlobalPluginsOverride {
+ globalPlugins: typeof globalPlugins;
+ }
+}
diff --git a/packages/web/src/contexts/CallableFunctionFormContext.ts b/packages/implementations/web/src/contexts/CallableFunctionFormContext.ts
similarity index 100%
rename from packages/web/src/contexts/CallableFunctionFormContext.ts
rename to packages/implementations/web/src/contexts/CallableFunctionFormContext.ts
diff --git a/packages/web/src/contexts/ConversationFormContext.ts b/packages/implementations/web/src/contexts/ConversationFormContext.ts
similarity index 78%
rename from packages/web/src/contexts/ConversationFormContext.ts
rename to packages/implementations/web/src/contexts/ConversationFormContext.ts
index 9c06fb6..17cdc7a 100644
--- a/packages/web/src/contexts/ConversationFormContext.ts
+++ b/packages/implementations/web/src/contexts/ConversationFormContext.ts
@@ -1,16 +1,19 @@
import { createFormContext } from "@mantine/form";
-import { CreateChatCompletionRequest, RequestOptionsProxy } from "gpt-turbo";
+import {
+ CreateChatCompletionRequest,
+ ConversationRequestOptionsModel,
+} from "gpt-turbo";
export interface ConversationFormValues {
save: boolean;
+ // Config
apiKey: string;
model: string;
context: string;
dry: boolean;
disableModeration: boolean | "soft";
stream: boolean;
-
temperature: CreateChatCompletionRequest["temperature"];
top_p: CreateChatCompletionRequest["top_p"];
frequency_penalty: CreateChatCompletionRequest["frequency_penalty"];
@@ -20,10 +23,12 @@ export interface ConversationFormValues {
logit_bias: CreateChatCompletionRequest["logit_bias"];
user: CreateChatCompletionRequest["user"];
- functionIds: string[];
+ // Request options
+ headers: ConversationRequestOptionsModel["headers"];
+ proxy: ConversationRequestOptionsModel["proxy"];
- headers: Record | undefined;
- proxy: RequestOptionsProxy | undefined;
+ // Callable functions
+ functionIds: string[];
}
const [FormProvider, useFormContext, useForm] =
diff --git a/packages/web/src/contexts/ConversationNavbarContext.ts b/packages/implementations/web/src/contexts/ConversationNavbarContext.ts
similarity index 100%
rename from packages/web/src/contexts/ConversationNavbarContext.ts
rename to packages/implementations/web/src/contexts/ConversationNavbarContext.ts
diff --git a/packages/web/src/contexts/hooks/useCallableFunctionForm.ts b/packages/implementations/web/src/contexts/hooks/useCallableFunctionForm.ts
similarity index 100%
rename from packages/web/src/contexts/hooks/useCallableFunctionForm.ts
rename to packages/implementations/web/src/contexts/hooks/useCallableFunctionForm.ts
diff --git a/packages/web/src/contexts/hooks/useConversationForm.ts b/packages/implementations/web/src/contexts/hooks/useConversationForm.ts
similarity index 100%
rename from packages/web/src/contexts/hooks/useConversationForm.ts
rename to packages/implementations/web/src/contexts/hooks/useConversationForm.ts
diff --git a/packages/web/src/contexts/hooks/useConversationNavbar.ts b/packages/implementations/web/src/contexts/hooks/useConversationNavbar.ts
similarity index 100%
rename from packages/web/src/contexts/hooks/useConversationNavbar.ts
rename to packages/implementations/web/src/contexts/hooks/useConversationNavbar.ts
diff --git a/packages/web/src/contexts/providers/CallableFunctionFormProvider.tsx b/packages/implementations/web/src/contexts/providers/CallableFunctionFormProvider.tsx
similarity index 100%
rename from packages/web/src/contexts/providers/CallableFunctionFormProvider.tsx
rename to packages/implementations/web/src/contexts/providers/CallableFunctionFormProvider.tsx
diff --git a/packages/web/src/contexts/providers/ConversationFormProvider.tsx b/packages/implementations/web/src/contexts/providers/ConversationFormProvider.tsx
similarity index 93%
rename from packages/web/src/contexts/providers/ConversationFormProvider.tsx
rename to packages/implementations/web/src/contexts/providers/ConversationFormProvider.tsx
index 3842f1d..005156a 100644
--- a/packages/web/src/contexts/providers/ConversationFormProvider.tsx
+++ b/packages/implementations/web/src/contexts/providers/ConversationFormProvider.tsx
@@ -32,13 +32,13 @@ const ConversationFormProvider = ({
initialValues: {
save: settings.save,
+ // Config
apiKey: settings.apiKey,
model: settings.model,
context: settings.context,
dry: settings.dry,
disableModeration: settings.disableModeration,
stream: settings.stream,
-
temperature: settings.temperature,
top_p: settings.top_p,
frequency_penalty: settings.frequency_penalty,
@@ -48,10 +48,12 @@ const ConversationFormProvider = ({
logit_bias: settings.logit_bias,
user: settings.user,
- functionIds: settings.functionIds,
-
+ // Request options
headers: settings.headers,
proxy: settings.proxy,
+
+ // Callable functions
+ functionIds: settings.functionIds,
},
transformValues: (values) => ({
...values,
@@ -70,20 +72,20 @@ const ConversationFormProvider = ({
hasEditInit.current = true;
const {
config = {},
- functions = [],
+ callableFunctions: { functions = [] } = {},
requestOptions = {},
} = conversation.toJSON();
form.setValues({
save: persistedConversationIds.includes(conversation.id),
- apiKey: config.apiKey,
+ // Config
+ apiKey: config.apiKey,
model: config.model,
context: config.context,
dry: config.dry,
disableModeration: config.disableModeration,
stream: config.stream,
-
temperature: config.temperature,
top_p: config.top_p,
frequency_penalty: config.frequency_penalty,
@@ -93,12 +95,14 @@ const ConversationFormProvider = ({
logit_bias: config.logit_bias,
user: config.user,
+ // Request options
+ headers: requestOptions.headers,
+ proxy: requestOptions.proxy,
+
+ // Callable functions
functionIds: functions
.map((f) => f.id)
.filter((id): id is string => !!id),
-
- headers: requestOptions.headers,
- proxy: requestOptions.proxy,
});
}, [conversation, form, persistedConversationIds]);
diff --git a/packages/web/src/contexts/providers/ConversationNavbarProvider.tsx b/packages/implementations/web/src/contexts/providers/ConversationNavbarProvider.tsx
similarity index 100%
rename from packages/web/src/contexts/providers/ConversationNavbarProvider.tsx
rename to packages/implementations/web/src/contexts/providers/ConversationNavbarProvider.tsx
diff --git a/packages/web/src/contexts/providers/MantineProviders.tsx b/packages/implementations/web/src/contexts/providers/MantineProviders.tsx
similarity index 100%
rename from packages/web/src/contexts/providers/MantineProviders.tsx
rename to packages/implementations/web/src/contexts/providers/MantineProviders.tsx
diff --git a/packages/web/src/contexts/providers/index.tsx b/packages/implementations/web/src/contexts/providers/index.tsx
similarity index 100%
rename from packages/web/src/contexts/providers/index.tsx
rename to packages/implementations/web/src/contexts/providers/index.tsx
diff --git a/packages/web/src/entities/callableFunctionExport.ts b/packages/implementations/web/src/entities/callableFunctionExport.ts
similarity index 100%
rename from packages/web/src/entities/callableFunctionExport.ts
rename to packages/implementations/web/src/entities/callableFunctionExport.ts
diff --git a/packages/web/src/entities/conversationExport.ts b/packages/implementations/web/src/entities/conversationExport.ts
similarity index 100%
rename from packages/web/src/entities/conversationExport.ts
rename to packages/implementations/web/src/entities/conversationExport.ts
diff --git a/packages/web/src/entities/persistence.ts b/packages/implementations/web/src/entities/persistence.ts
similarity index 100%
rename from packages/web/src/entities/persistence.ts
rename to packages/implementations/web/src/entities/persistence.ts
diff --git a/packages/web/src/entities/persistenceAppSettings.ts b/packages/implementations/web/src/entities/persistenceAppSettings.ts
similarity index 100%
rename from packages/web/src/entities/persistenceAppSettings.ts
rename to packages/implementations/web/src/entities/persistenceAppSettings.ts
diff --git a/packages/web/src/entities/persistenceCallableFunction.ts b/packages/implementations/web/src/entities/persistenceCallableFunction.ts
similarity index 100%
rename from packages/web/src/entities/persistenceCallableFunction.ts
rename to packages/implementations/web/src/entities/persistenceCallableFunction.ts
diff --git a/packages/web/src/entities/persistenceCallableFunctions.ts b/packages/implementations/web/src/entities/persistenceCallableFunctions.ts
similarity index 100%
rename from packages/web/src/entities/persistenceCallableFunctions.ts
rename to packages/implementations/web/src/entities/persistenceCallableFunctions.ts
diff --git a/packages/web/src/entities/persistenceConversation.ts b/packages/implementations/web/src/entities/persistenceConversation.ts
similarity index 100%
rename from packages/web/src/entities/persistenceConversation.ts
rename to packages/implementations/web/src/entities/persistenceConversation.ts
diff --git a/packages/web/src/entities/persistenceDefaultSettings.ts b/packages/implementations/web/src/entities/persistenceDefaultSettings.ts
similarity index 100%
rename from packages/web/src/entities/persistenceDefaultSettings.ts
rename to packages/implementations/web/src/entities/persistenceDefaultSettings.ts
diff --git a/packages/web/src/entities/persistenceSavedContext.ts b/packages/implementations/web/src/entities/persistenceSavedContext.ts
similarity index 100%
rename from packages/web/src/entities/persistenceSavedContext.ts
rename to packages/implementations/web/src/entities/persistenceSavedContext.ts
diff --git a/packages/web/src/entities/persistenceSavedContexts.ts b/packages/implementations/web/src/entities/persistenceSavedContexts.ts
similarity index 100%
rename from packages/web/src/entities/persistenceSavedContexts.ts
rename to packages/implementations/web/src/entities/persistenceSavedContexts.ts
diff --git a/packages/web/src/entities/persistenceSavedPrompt.ts b/packages/implementations/web/src/entities/persistenceSavedPrompt.ts
similarity index 100%
rename from packages/web/src/entities/persistenceSavedPrompt.ts
rename to packages/implementations/web/src/entities/persistenceSavedPrompt.ts
diff --git a/packages/web/src/entities/persistenceSavedPrompts.ts b/packages/implementations/web/src/entities/persistenceSavedPrompts.ts
similarity index 100%
rename from packages/web/src/entities/persistenceSavedPrompts.ts
rename to packages/implementations/web/src/entities/persistenceSavedPrompts.ts
diff --git a/packages/web/src/index.tsx b/packages/implementations/web/src/index.tsx
similarity index 96%
rename from packages/web/src/index.tsx
rename to packages/implementations/web/src/index.tsx
index be246a8..efbcc0a 100644
--- a/packages/web/src/index.tsx
+++ b/packages/implementations/web/src/index.tsx
@@ -1,3 +1,4 @@
+import "./config/gpt-turbo";
import React from "react";
import ReactDOM from "react-dom/client";
import Providers from "./contexts/providers";
diff --git a/packages/web/src/pages/ConversationPage.tsx b/packages/implementations/web/src/pages/ConversationPage.tsx
similarity index 88%
rename from packages/web/src/pages/ConversationPage.tsx
rename to packages/implementations/web/src/pages/ConversationPage.tsx
index dad16a1..958f678 100644
--- a/packages/web/src/pages/ConversationPage.tsx
+++ b/packages/implementations/web/src/pages/ConversationPage.tsx
@@ -15,18 +15,18 @@ const ConversationPage = () => {
const unsubs: (() => void)[] = [];
unsubs.push(
- activeConversation.onMessageAdded((message) => {
+ activeConversation.history.onMessageAdded((message) => {
if (message.content) {
persistStore();
}
unsubs.push(
- message.onMessageStreamingStop(() => {
+ message.onStreamingStop(() => {
persistStore();
})
);
}),
- activeConversation.onMessageRemoved(() => {
+ activeConversation.history.onMessageRemoved(() => {
persistStore();
})
);
diff --git a/packages/web/src/pages/FunctionEditorPage.tsx b/packages/implementations/web/src/pages/FunctionEditorPage.tsx
similarity index 100%
rename from packages/web/src/pages/FunctionEditorPage.tsx
rename to packages/implementations/web/src/pages/FunctionEditorPage.tsx
diff --git a/packages/web/src/pages/FunctionsPage.tsx b/packages/implementations/web/src/pages/FunctionsPage.tsx
similarity index 100%
rename from packages/web/src/pages/FunctionsPage.tsx
rename to packages/implementations/web/src/pages/FunctionsPage.tsx
diff --git a/packages/web/src/public/assets/images/favicon/android-chrome-192x192.png b/packages/implementations/web/src/public/assets/images/favicon/android-chrome-192x192.png
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/android-chrome-192x192.png
rename to packages/implementations/web/src/public/assets/images/favicon/android-chrome-192x192.png
diff --git a/packages/web/src/public/assets/images/favicon/android-chrome-512x512.png b/packages/implementations/web/src/public/assets/images/favicon/android-chrome-512x512.png
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/android-chrome-512x512.png
rename to packages/implementations/web/src/public/assets/images/favicon/android-chrome-512x512.png
diff --git a/packages/web/src/public/assets/images/favicon/apple-touch-icon.png b/packages/implementations/web/src/public/assets/images/favicon/apple-touch-icon.png
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/apple-touch-icon.png
rename to packages/implementations/web/src/public/assets/images/favicon/apple-touch-icon.png
diff --git a/packages/web/src/public/assets/images/favicon/favicon-16x16.png b/packages/implementations/web/src/public/assets/images/favicon/favicon-16x16.png
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/favicon-16x16.png
rename to packages/implementations/web/src/public/assets/images/favicon/favicon-16x16.png
diff --git a/packages/web/src/public/assets/images/favicon/favicon-32x32.png b/packages/implementations/web/src/public/assets/images/favicon/favicon-32x32.png
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/favicon-32x32.png
rename to packages/implementations/web/src/public/assets/images/favicon/favicon-32x32.png
diff --git a/packages/web/src/public/assets/images/favicon/favicon.ico b/packages/implementations/web/src/public/assets/images/favicon/favicon.ico
similarity index 100%
rename from packages/web/src/public/assets/images/favicon/favicon.ico
rename to packages/implementations/web/src/public/assets/images/favicon/favicon.ico
diff --git a/packages/web/src/routers/AppRouter.tsx b/packages/implementations/web/src/routers/AppRouter.tsx
similarity index 74%
rename from packages/web/src/routers/AppRouter.tsx
rename to packages/implementations/web/src/routers/AppRouter.tsx
index 9d2992e..ab17a58 100644
--- a/packages/web/src/routers/AppRouter.tsx
+++ b/packages/implementations/web/src/routers/AppRouter.tsx
@@ -6,14 +6,23 @@ import {
} from "react-router-dom";
import ContentLoader from "../components/common/ContentLoader";
import { Text } from "@mantine/core";
+import RouterCatcher from "../components/error-handling/RouterCatcher";
const ConversationPage = React.lazy(() => import("../pages/ConversationPage"));
const FunctionsRouter = React.lazy(() => import("./FunctionsRouter"));
const router = createBrowserRouter([
- { path: "/", element: },
- { path: "/functions/*", element: },
- { path: "*", element: },
+ { path: "/", element: , ErrorBoundary: RouterCatcher },
+ {
+ path: "/functions/*",
+ element: ,
+ ErrorBoundary: RouterCatcher,
+ },
+ {
+ path: "*",
+ element: ,
+ ErrorBoundary: RouterCatcher,
+ },
]);
const AppRouter = () => {
diff --git a/packages/web/src/routers/FunctionsRouter.tsx b/packages/implementations/web/src/routers/FunctionsRouter.tsx
similarity index 100%
rename from packages/web/src/routers/FunctionsRouter.tsx
rename to packages/implementations/web/src/routers/FunctionsRouter.tsx
diff --git a/packages/web/src/store/actions/appSettings/setLastChangelog.ts b/packages/implementations/web/src/store/actions/appSettings/setLastChangelog.ts
similarity index 100%
rename from packages/web/src/store/actions/appSettings/setLastChangelog.ts
rename to packages/implementations/web/src/store/actions/appSettings/setLastChangelog.ts
diff --git a/packages/web/src/store/actions/appSettings/toggleColorScheme.ts b/packages/implementations/web/src/store/actions/appSettings/toggleColorScheme.ts
similarity index 100%
rename from packages/web/src/store/actions/appSettings/toggleColorScheme.ts
rename to packages/implementations/web/src/store/actions/appSettings/toggleColorScheme.ts
diff --git a/packages/web/src/store/actions/appSettings/toggleShowConversationImport.ts b/packages/implementations/web/src/store/actions/appSettings/toggleShowConversationImport.ts
similarity index 100%
rename from packages/web/src/store/actions/appSettings/toggleShowConversationImport.ts
rename to packages/implementations/web/src/store/actions/appSettings/toggleShowConversationImport.ts
diff --git a/packages/web/src/store/actions/appSettings/toggleShowUsage.ts b/packages/implementations/web/src/store/actions/appSettings/toggleShowUsage.ts
similarity index 100%
rename from packages/web/src/store/actions/appSettings/toggleShowUsage.ts
rename to packages/implementations/web/src/store/actions/appSettings/toggleShowUsage.ts
diff --git a/packages/web/src/store/actions/callableFunctions/addCallableFunction.ts b/packages/implementations/web/src/store/actions/callableFunctions/addCallableFunction.ts
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/addCallableFunction.ts
rename to packages/implementations/web/src/store/actions/callableFunctions/addCallableFunction.ts
diff --git a/packages/web/src/store/actions/callableFunctions/deleteCallableFunction.tsx b/packages/implementations/web/src/store/actions/callableFunctions/deleteCallableFunction.tsx
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/deleteCallableFunction.tsx
rename to packages/implementations/web/src/store/actions/callableFunctions/deleteCallableFunction.tsx
diff --git a/packages/web/src/store/actions/callableFunctions/dismissFunctionsImportWarning.ts b/packages/implementations/web/src/store/actions/callableFunctions/dismissFunctionsImportWarning.ts
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/dismissFunctionsImportWarning.ts
rename to packages/implementations/web/src/store/actions/callableFunctions/dismissFunctionsImportWarning.ts
diff --git a/packages/web/src/store/actions/callableFunctions/dismissFunctionsWarning.ts b/packages/implementations/web/src/store/actions/callableFunctions/dismissFunctionsWarning.ts
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/dismissFunctionsWarning.ts
rename to packages/implementations/web/src/store/actions/callableFunctions/dismissFunctionsWarning.ts
diff --git a/packages/web/src/store/actions/callableFunctions/duplicateCallableFunction.tsx b/packages/implementations/web/src/store/actions/callableFunctions/duplicateCallableFunction.tsx
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/duplicateCallableFunction.tsx
rename to packages/implementations/web/src/store/actions/callableFunctions/duplicateCallableFunction.tsx
diff --git a/packages/web/src/store/actions/callableFunctions/removeAllCallableFunctions.ts b/packages/implementations/web/src/store/actions/callableFunctions/removeAllCallableFunctions.ts
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/removeAllCallableFunctions.ts
rename to packages/implementations/web/src/store/actions/callableFunctions/removeAllCallableFunctions.ts
diff --git a/packages/web/src/store/actions/callableFunctions/resetCallableFunctionWarnings.ts b/packages/implementations/web/src/store/actions/callableFunctions/resetCallableFunctionWarnings.ts
similarity index 100%
rename from packages/web/src/store/actions/callableFunctions/resetCallableFunctionWarnings.ts
rename to packages/implementations/web/src/store/actions/callableFunctions/resetCallableFunctionWarnings.ts
diff --git a/packages/web/src/store/actions/conversations/addConversation.ts b/packages/implementations/web/src/store/actions/conversations/addConversation.ts
similarity index 77%
rename from packages/web/src/store/actions/conversations/addConversation.ts
rename to packages/implementations/web/src/store/actions/conversations/addConversation.ts
index 9cc341c..cbf044e 100644
--- a/packages/web/src/store/actions/conversations/addConversation.ts
+++ b/packages/implementations/web/src/store/actions/conversations/addConversation.ts
@@ -1,16 +1,11 @@
-import {
- Conversation,
- ConversationConfigParameters,
- RequestOptions,
-} from "gpt-turbo";
+import { Conversation, ConversationOptions } from "gpt-turbo";
import { createAction } from "../createAction";
import { addPersistedConversationId } from "../persistence/addPersistedConversationId";
export const addConversation = createAction(
(
{ set, get },
- conversation: Conversation | ConversationConfigParameters,
- requestOptions?: RequestOptions,
+ conversation: Conversation | ConversationOptions,
functionIds: string[] = [],
save = false
) => {
@@ -18,14 +13,14 @@ export const addConversation = createAction(
const newConversation =
conversation instanceof Conversation
? conversation
- : new Conversation(conversation, requestOptions);
+ : new Conversation(conversation);
for (const functionId of functionIds) {
const callableFunction = callableFunctions.find(
(callableFunction) => callableFunction.id === functionId
);
if (callableFunction) {
- newConversation.addFunction(callableFunction);
+ newConversation.callableFunctions.addFunction(callableFunction);
}
}
diff --git a/packages/implementations/web/src/store/actions/conversations/duplicateConversation.ts b/packages/implementations/web/src/store/actions/conversations/duplicateConversation.ts
new file mode 100644
index 0000000..1cfea9e
--- /dev/null
+++ b/packages/implementations/web/src/store/actions/conversations/duplicateConversation.ts
@@ -0,0 +1,28 @@
+import { Conversation } from "gpt-turbo";
+import { createAction } from "../createAction";
+import { addConversation } from "./addConversation";
+import { setConversationName } from "./setConversationName";
+
+export const duplicateConversation = createAction(({ get }, id: string) => {
+ const { conversations, persistedConversationIds, conversationNames } =
+ get();
+ const conversation = conversations.find((c) => c.id === id);
+
+ if (!conversation) {
+ throw new Error(`Conversation with id ${id} not found`);
+ }
+
+ const { id: _, ...json } = conversation.toJSON();
+ const copy = Conversation.fromJSON(json);
+
+ const newConversation = addConversation(
+ copy,
+ undefined,
+ persistedConversationIds.includes(id)
+ );
+
+ const name = conversationNames.get(id);
+ if (name) {
+ setConversationName(newConversation.id, name);
+ }
+}, "duplicateConversation");
diff --git a/packages/web/src/store/actions/conversations/editConversation.ts b/packages/implementations/web/src/store/actions/conversations/editConversation.ts
similarity index 84%
rename from packages/web/src/store/actions/conversations/editConversation.ts
rename to packages/implementations/web/src/store/actions/conversations/editConversation.ts
index dc2d495..8d2c5ae 100644
--- a/packages/web/src/store/actions/conversations/editConversation.ts
+++ b/packages/implementations/web/src/store/actions/conversations/editConversation.ts
@@ -26,20 +26,22 @@ export const editConversation = createAction(
throw new Error(`Conversation with id ${id} not found`);
}
- conversation.setConfig(config, true);
- conversation.setRequestOptions({
+ conversation.config.setConfig(config, true);
+ conversation.requestOptions.setRequestOptions({
headers,
proxy,
});
if (functionIds) {
- conversation.clearFunctions();
+ conversation.callableFunctions.clearFunctions();
for (const functionId of functionIds) {
const callableFunction = callableFunctions.find(
(callableFunction) => callableFunction.id === functionId
);
if (callableFunction) {
- conversation.addFunction(callableFunction);
+ conversation.callableFunctions.addFunction(
+ callableFunction
+ );
}
}
}
diff --git a/packages/web/src/store/actions/conversations/importConversations.ts b/packages/implementations/web/src/store/actions/conversations/importConversations.ts
similarity index 94%
rename from packages/web/src/store/actions/conversations/importConversations.ts
rename to packages/implementations/web/src/store/actions/conversations/importConversations.ts
index d6dbff8..284e44c 100644
--- a/packages/web/src/store/actions/conversations/importConversations.ts
+++ b/packages/implementations/web/src/store/actions/conversations/importConversations.ts
@@ -11,7 +11,7 @@ export const importConversations = createAction(
const imported: Conversation[] = [];
for (const { conversation: json, name } of conversationExports) {
- const conversation = await Conversation.fromJSON({
+ const conversation = Conversation.fromJSON({
...json,
config: {
...json.config,
diff --git a/packages/web/src/store/actions/conversations/removeAllConversations.ts b/packages/implementations/web/src/store/actions/conversations/removeAllConversations.ts
similarity index 100%
rename from packages/web/src/store/actions/conversations/removeAllConversations.ts
rename to packages/implementations/web/src/store/actions/conversations/removeAllConversations.ts
diff --git a/packages/web/src/store/actions/conversations/removeConversation.ts b/packages/implementations/web/src/store/actions/conversations/removeConversation.ts
similarity index 100%
rename from packages/web/src/store/actions/conversations/removeConversation.ts
rename to packages/implementations/web/src/store/actions/conversations/removeConversation.ts
diff --git a/packages/web/src/store/actions/conversations/setActiveConversation.ts b/packages/implementations/web/src/store/actions/conversations/setActiveConversation.ts
similarity index 100%
rename from packages/web/src/store/actions/conversations/setActiveConversation.ts
rename to packages/implementations/web/src/store/actions/conversations/setActiveConversation.ts
diff --git a/packages/web/src/store/actions/conversations/setConversationLastEdit.ts b/packages/implementations/web/src/store/actions/conversations/setConversationLastEdit.ts
similarity index 100%
rename from packages/web/src/store/actions/conversations/setConversationLastEdit.ts
rename to packages/implementations/web/src/store/actions/conversations/setConversationLastEdit.ts
diff --git a/packages/web/src/store/actions/conversations/setConversationName.ts b/packages/implementations/web/src/store/actions/conversations/setConversationName.ts
similarity index 100%
rename from packages/web/src/store/actions/conversations/setConversationName.ts
rename to packages/implementations/web/src/store/actions/conversations/setConversationName.ts
diff --git a/packages/web/src/store/actions/createAction.ts b/packages/implementations/web/src/store/actions/createAction.ts
similarity index 100%
rename from packages/web/src/store/actions/createAction.ts
rename to packages/implementations/web/src/store/actions/createAction.ts
diff --git a/packages/web/src/store/actions/defaultConversationSettings/resetDefaultSettings.ts b/packages/implementations/web/src/store/actions/defaultConversationSettings/resetDefaultSettings.ts
similarity index 100%
rename from packages/web/src/store/actions/defaultConversationSettings/resetDefaultSettings.ts
rename to packages/implementations/web/src/store/actions/defaultConversationSettings/resetDefaultSettings.ts
diff --git a/packages/web/src/store/actions/defaultConversationSettings/setDefaultSettings.ts b/packages/implementations/web/src/store/actions/defaultConversationSettings/setDefaultSettings.ts
similarity index 100%
rename from packages/web/src/store/actions/defaultConversationSettings/setDefaultSettings.ts
rename to packages/implementations/web/src/store/actions/defaultConversationSettings/setDefaultSettings.ts
diff --git a/packages/web/src/store/actions/persistence/addPersistedConversationId.ts b/packages/implementations/web/src/store/actions/persistence/addPersistedConversationId.ts
similarity index 100%
rename from packages/web/src/store/actions/persistence/addPersistedConversationId.ts
rename to packages/implementations/web/src/store/actions/persistence/addPersistedConversationId.ts
diff --git a/packages/web/src/store/actions/persistence/removePersistedConversationId.ts b/packages/implementations/web/src/store/actions/persistence/removePersistedConversationId.ts
similarity index 100%
rename from packages/web/src/store/actions/persistence/removePersistedConversationId.ts
rename to packages/implementations/web/src/store/actions/persistence/removePersistedConversationId.ts
diff --git a/packages/web/src/store/actions/savedContexts/removeAllSavedContexts.ts b/packages/implementations/web/src/store/actions/savedContexts/removeAllSavedContexts.ts
similarity index 100%
rename from packages/web/src/store/actions/savedContexts/removeAllSavedContexts.ts
rename to packages/implementations/web/src/store/actions/savedContexts/removeAllSavedContexts.ts
diff --git a/packages/web/src/store/actions/savedContexts/removeSavedContext.ts b/packages/implementations/web/src/store/actions/savedContexts/removeSavedContext.ts
similarity index 100%
rename from packages/web/src/store/actions/savedContexts/removeSavedContext.ts
rename to packages/implementations/web/src/store/actions/savedContexts/removeSavedContext.ts
diff --git a/packages/web/src/store/actions/savedContexts/saveContext.ts b/packages/implementations/web/src/store/actions/savedContexts/saveContext.ts
similarity index 100%
rename from packages/web/src/store/actions/savedContexts/saveContext.ts
rename to packages/implementations/web/src/store/actions/savedContexts/saveContext.ts
diff --git a/packages/web/src/store/actions/savedPrompts/removeAllSavedPrompts.ts b/packages/implementations/web/src/store/actions/savedPrompts/removeAllSavedPrompts.ts
similarity index 100%
rename from packages/web/src/store/actions/savedPrompts/removeAllSavedPrompts.ts
rename to packages/implementations/web/src/store/actions/savedPrompts/removeAllSavedPrompts.ts
diff --git a/packages/web/src/store/actions/savedPrompts/removeSavedPrompt.ts b/packages/implementations/web/src/store/actions/savedPrompts/removeSavedPrompt.ts
similarity index 100%
rename from packages/web/src/store/actions/savedPrompts/removeSavedPrompt.ts
rename to packages/implementations/web/src/store/actions/savedPrompts/removeSavedPrompt.ts
diff --git a/packages/web/src/store/actions/savedPrompts/savePrompt.ts b/packages/implementations/web/src/store/actions/savedPrompts/savePrompt.ts
similarity index 100%
rename from packages/web/src/store/actions/savedPrompts/savePrompt.ts
rename to packages/implementations/web/src/store/actions/savedPrompts/savePrompt.ts
diff --git a/packages/web/src/store/hooks/callableFunctions/useCallFunction.tsx b/packages/implementations/web/src/store/hooks/callableFunctions/useCallFunction.tsx
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useCallFunction.tsx
rename to packages/implementations/web/src/store/hooks/callableFunctions/useCallFunction.tsx
diff --git a/packages/web/src/store/hooks/callableFunctions/useGetFunction.ts b/packages/implementations/web/src/store/hooks/callableFunctions/useGetFunction.ts
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useGetFunction.ts
rename to packages/implementations/web/src/store/hooks/callableFunctions/useGetFunction.ts
diff --git a/packages/web/src/store/hooks/callableFunctions/useGetFunctionCode.ts b/packages/implementations/web/src/store/hooks/callableFunctions/useGetFunctionCode.ts
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useGetFunctionCode.ts
rename to packages/implementations/web/src/store/hooks/callableFunctions/useGetFunctionCode.ts
diff --git a/packages/web/src/store/hooks/callableFunctions/useGetFunctionDisplayName.ts b/packages/implementations/web/src/store/hooks/callableFunctions/useGetFunctionDisplayName.ts
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useGetFunctionDisplayName.ts
rename to packages/implementations/web/src/store/hooks/callableFunctions/useGetFunctionDisplayName.ts
diff --git a/packages/web/src/store/hooks/callableFunctions/useGetUniqueFunctionDisplayName.ts b/packages/implementations/web/src/store/hooks/callableFunctions/useGetUniqueFunctionDisplayName.ts
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useGetUniqueFunctionDisplayName.ts
rename to packages/implementations/web/src/store/hooks/callableFunctions/useGetUniqueFunctionDisplayName.ts
diff --git a/packages/web/src/store/hooks/callableFunctions/useGetUniqueFunctionName.ts b/packages/implementations/web/src/store/hooks/callableFunctions/useGetUniqueFunctionName.ts
similarity index 100%
rename from packages/web/src/store/hooks/callableFunctions/useGetUniqueFunctionName.ts
rename to packages/implementations/web/src/store/hooks/callableFunctions/useGetUniqueFunctionName.ts
diff --git a/packages/web/src/store/hooks/conversations/useActiveConversation.ts b/packages/implementations/web/src/store/hooks/conversations/useActiveConversation.ts
similarity index 100%
rename from packages/web/src/store/hooks/conversations/useActiveConversation.ts
rename to packages/implementations/web/src/store/hooks/conversations/useActiveConversation.ts
diff --git a/packages/web/src/store/hooks/conversations/useGenerateConversationName.ts b/packages/implementations/web/src/store/hooks/conversations/useGenerateConversationName.ts
similarity index 86%
rename from packages/web/src/store/hooks/conversations/useGenerateConversationName.ts
rename to packages/implementations/web/src/store/hooks/conversations/useGenerateConversationName.ts
index 6685c1a..b168151 100644
--- a/packages/web/src/store/hooks/conversations/useGenerateConversationName.ts
+++ b/packages/implementations/web/src/store/hooks/conversations/useGenerateConversationName.ts
@@ -43,7 +43,7 @@ export const useGenerateConversationName = (conversationId: string) => {
const conversation = conversations.find((c) => c.id === conversationId);
const apiKey = settings.apiKey;
- const messages = conversation?.getMessages() ?? [];
+ const messages = conversation?.history.getMessages() ?? [];
const userMessage = getFirstMessageOfRole(messages, "user");
const assistantMessage = getFirstMessageOfRole(messages, "assistant");
@@ -58,15 +58,17 @@ export const useGenerateConversationName = (conversationId: string) => {
setIsGenerating(true);
const generateConversation = new Conversation({
- apiKey,
- disableModeration: true,
+ config: {
+ apiKey,
+ disableModeration: true,
+ },
});
- await generateConversation.addUserMessage(userMessage!.content);
- await generateConversation.addAssistantMessage(
+ generateConversation.history.addUserMessage(userMessage!.content);
+ generateConversation.history.addAssistantMessage(
assistantMessage!.content
);
- generateConversation.addFunction(generateFn);
+ generateConversation.callableFunctions.addFunction(generateFn);
const result = await generateConversation.prompt(
"Give a name for this conversation based on the two previous messages.",
diff --git a/packages/web/src/store/hooks/conversations/useGetConversationLastEdit.ts b/packages/implementations/web/src/store/hooks/conversations/useGetConversationLastEdit.ts
similarity index 100%
rename from packages/web/src/store/hooks/conversations/useGetConversationLastEdit.ts
rename to packages/implementations/web/src/store/hooks/conversations/useGetConversationLastEdit.ts
diff --git a/packages/web/src/store/hooks/conversations/useGetConversationName.ts b/packages/implementations/web/src/store/hooks/conversations/useGetConversationName.ts
similarity index 100%
rename from packages/web/src/store/hooks/conversations/useGetConversationName.ts
rename to packages/implementations/web/src/store/hooks/conversations/useGetConversationName.ts
diff --git a/packages/web/src/store/index.ts b/packages/implementations/web/src/store/index.ts
similarity index 100%
rename from packages/web/src/store/index.ts
rename to packages/implementations/web/src/store/index.ts
diff --git a/packages/web/src/store/persist/migrations/1689216775706_changelog.ts b/packages/implementations/web/src/store/persist/migrations/1689216775706_changelog.ts
similarity index 100%
rename from packages/web/src/store/persist/migrations/1689216775706_changelog.ts
rename to packages/implementations/web/src/store/persist/migrations/1689216775706_changelog.ts
diff --git a/packages/web/src/store/persist/migrations/1689537326770_conversation-export.ts b/packages/implementations/web/src/store/persist/migrations/1689537326770_conversation-export.ts
similarity index 100%
rename from packages/web/src/store/persist/migrations/1689537326770_conversation-export.ts
rename to packages/implementations/web/src/store/persist/migrations/1689537326770_conversation-export.ts
diff --git a/packages/implementations/web/src/store/persist/migrations/1690579089990_v5.ts b/packages/implementations/web/src/store/persist/migrations/1690579089990_v5.ts
new file mode 100644
index 0000000..70b6133
--- /dev/null
+++ b/packages/implementations/web/src/store/persist/migrations/1690579089990_v5.ts
@@ -0,0 +1,27 @@
+import { produce } from "immer";
+import { StoreMigration } from ".";
+
+export const migrationV5: StoreMigration = produce((persistedState) => {
+ for (const conversation of persistedState.conversations) {
+ // History
+ conversation.history = {
+ messages: conversation.messages,
+ };
+ conversation.messages = undefined;
+
+ // Config
+ conversation.config = conversation.config;
+
+ // Callable functions
+ conversation.callableFunctions = {
+ functions: conversation.functions,
+ };
+ conversation.functions = undefined;
+
+ // Request options
+ conversation.requestOptions = conversation.requestOptions;
+
+ // Plugins data
+ conversation.pluginsData = {};
+ }
+});
diff --git a/packages/web/src/store/persist/migrations/index.ts b/packages/implementations/web/src/store/persist/migrations/index.ts
similarity index 90%
rename from packages/web/src/store/persist/migrations/index.ts
rename to packages/implementations/web/src/store/persist/migrations/index.ts
index d0a16fa..780a90d 100644
--- a/packages/web/src/store/persist/migrations/index.ts
+++ b/packages/implementations/web/src/store/persist/migrations/index.ts
@@ -2,12 +2,14 @@ import { AppPersistedState } from "../..";
import { parsePersistedState } from "../parsePersistedState";
import { migrationChangelog } from "./1689216775706_changelog";
import { migrationConversationExport } from "./1689537326770_conversation-export";
+import { migrationV5 } from "./1690579089990_v5";
export type StoreMigration = (persistedState: any) => any | Promise;
export const storeMigrations: StoreMigration[] = [
migrationChangelog,
migrationConversationExport,
+ migrationV5,
];
export const storeVersion = storeMigrations.length;
@@ -31,7 +33,7 @@ export const migrateStore = async (
migratedState = await migration(migratedState);
}
- const parsedState = await parsePersistedState(migratedState);
+ const parsedState = parsePersistedState(migratedState);
return parsedState;
} catch (e) {
console.error(e);
diff --git a/packages/web/src/store/persist/onStoreRehydrate.tsx b/packages/implementations/web/src/store/persist/onStoreRehydrate.tsx
similarity index 92%
rename from packages/web/src/store/persist/onStoreRehydrate.tsx
rename to packages/implementations/web/src/store/persist/onStoreRehydrate.tsx
index 2fcd7af..aa57624 100644
--- a/packages/web/src/store/persist/onStoreRehydrate.tsx
+++ b/packages/implementations/web/src/store/persist/onStoreRehydrate.tsx
@@ -5,15 +5,11 @@ import StorageLoadError from "../../components/error-handling/StorageLoadError";
import { STORAGE_PERSISTENCE_KEY } from "../../config/constants";
import { StoreMigrationError } from "./migrations";
import getErrorInfo from "../../utils/getErrorInfo";
-import { migrateOldData } from "./migrateOldData";
export const onStoreRehydrate = (
_state: AppState
): void | ((state?: AppState, error?: unknown) => void) => {
return (_hydratedState, e) => {
- // TODO: Remove this after a while to give time for users to migrate to new storage
- migrateOldData();
-
if (!e) return;
const error = e as Error;
const isMigrationError = error instanceof StoreMigrationError;
diff --git a/packages/web/src/store/persist/parsePersistedState.ts b/packages/implementations/web/src/store/persist/parsePersistedState.ts
similarity index 96%
rename from packages/web/src/store/persist/parsePersistedState.ts
rename to packages/implementations/web/src/store/persist/parsePersistedState.ts
index 6cc8c8c..52ed9f6 100644
--- a/packages/web/src/store/persist/parsePersistedState.ts
+++ b/packages/implementations/web/src/store/persist/parsePersistedState.ts
@@ -10,7 +10,7 @@ const notify = (title: string, message: string) => {
}, 100);
};
-export const parsePersistedState = async (persistedState: any) => {
+export const parsePersistedState = (persistedState: any) => {
const state: AppState = {
...initialAppState,
};
@@ -55,7 +55,7 @@ export const parsePersistedState = async (persistedState: any) => {
const { conversations } = persistence;
for (const { lastEdited, name, ...conversation } of conversations) {
try {
- const c = await Conversation.fromJSON({
+ const c = Conversation.fromJSON({
...conversation,
config: {
...conversation.config,
diff --git a/packages/web/src/store/persist/partializeStore.ts b/packages/implementations/web/src/store/persist/partializeStore.ts
similarity index 97%
rename from packages/web/src/store/persist/partializeStore.ts
rename to packages/implementations/web/src/store/persist/partializeStore.ts
index 84be476..55882ba 100644
--- a/packages/web/src/store/persist/partializeStore.ts
+++ b/packages/implementations/web/src/store/persist/partializeStore.ts
@@ -30,7 +30,7 @@ export const partializeStore = (state: AppState): AppPersistedState => {
const conversations = persistenceSchema.shape.conversations.parse(
state.conversations
.filter((c) => state.persistedConversationIds.includes(c.id))
- .filter((c) => c.getMessages().length > 0)
+ .filter((c) => c.history.getMessages().length > 0)
.map((c) => {
const lastEdited =
state.conversationLastEdits.get(c.id) ?? Date.now();
diff --git a/packages/web/src/store/persist/storeStorage.ts b/packages/implementations/web/src/store/persist/storeStorage.ts
similarity index 100%
rename from packages/web/src/store/persist/storeStorage.ts
rename to packages/implementations/web/src/store/persist/storeStorage.ts
diff --git a/packages/web/src/store/persist/triggerPersist.ts b/packages/implementations/web/src/store/persist/triggerPersist.ts
similarity index 100%
rename from packages/web/src/store/persist/triggerPersist.ts
rename to packages/implementations/web/src/store/persist/triggerPersist.ts
diff --git a/packages/web/src/store/slices/appSettingsSlice.ts b/packages/implementations/web/src/store/slices/appSettingsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/appSettingsSlice.ts
rename to packages/implementations/web/src/store/slices/appSettingsSlice.ts
diff --git a/packages/web/src/store/slices/callableFunctionsSlice.ts b/packages/implementations/web/src/store/slices/callableFunctionsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/callableFunctionsSlice.ts
rename to packages/implementations/web/src/store/slices/callableFunctionsSlice.ts
diff --git a/packages/web/src/store/slices/conversationsSlice.ts b/packages/implementations/web/src/store/slices/conversationsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/conversationsSlice.ts
rename to packages/implementations/web/src/store/slices/conversationsSlice.ts
diff --git a/packages/web/src/store/slices/defaultConversationSettingsSlice.ts b/packages/implementations/web/src/store/slices/defaultConversationSettingsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/defaultConversationSettingsSlice.ts
rename to packages/implementations/web/src/store/slices/defaultConversationSettingsSlice.ts
diff --git a/packages/web/src/store/slices/persistenceSlice.ts b/packages/implementations/web/src/store/slices/persistenceSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/persistenceSlice.ts
rename to packages/implementations/web/src/store/slices/persistenceSlice.ts
diff --git a/packages/web/src/store/slices/savedContextsSlice.ts b/packages/implementations/web/src/store/slices/savedContextsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/savedContextsSlice.ts
rename to packages/implementations/web/src/store/slices/savedContextsSlice.ts
diff --git a/packages/web/src/store/slices/savedPromptsSlice.ts b/packages/implementations/web/src/store/slices/savedPromptsSlice.ts
similarity index 100%
rename from packages/web/src/store/slices/savedPromptsSlice.ts
rename to packages/implementations/web/src/store/slices/savedPromptsSlice.ts
diff --git a/packages/web/src/utils/functionTemplates/asyncTemplate.ts b/packages/implementations/web/src/utils/functionTemplates/asyncTemplate.ts
similarity index 100%
rename from packages/web/src/utils/functionTemplates/asyncTemplate.ts
rename to packages/implementations/web/src/utils/functionTemplates/asyncTemplate.ts
diff --git a/packages/web/src/utils/functionTemplates/fetchTemplate.ts b/packages/implementations/web/src/utils/functionTemplates/fetchTemplate.ts
similarity index 100%
rename from packages/web/src/utils/functionTemplates/fetchTemplate.ts
rename to packages/implementations/web/src/utils/functionTemplates/fetchTemplate.ts
diff --git a/packages/web/src/utils/functionTemplates/index.ts b/packages/implementations/web/src/utils/functionTemplates/index.ts
similarity index 100%
rename from packages/web/src/utils/functionTemplates/index.ts
rename to packages/implementations/web/src/utils/functionTemplates/index.ts
diff --git a/packages/web/src/utils/getErrorInfo.ts b/packages/implementations/web/src/utils/getErrorInfo.ts
similarity index 100%
rename from packages/web/src/utils/getErrorInfo.ts
rename to packages/implementations/web/src/utils/getErrorInfo.ts
diff --git a/packages/web/src/utils/getFunctionParameters.ts b/packages/implementations/web/src/utils/getFunctionParameters.ts
similarity index 100%
rename from packages/web/src/utils/getFunctionParameters.ts
rename to packages/implementations/web/src/utils/getFunctionParameters.ts
diff --git a/packages/web/src/utils/getFunctionSignature.ts b/packages/implementations/web/src/utils/getFunctionSignature.ts
similarity index 100%
rename from packages/web/src/utils/getFunctionSignature.ts
rename to packages/implementations/web/src/utils/getFunctionSignature.ts
diff --git a/packages/implementations/web/src/utils/getIssueLink.ts b/packages/implementations/web/src/utils/getIssueLink.ts
new file mode 100644
index 0000000..4819441
--- /dev/null
+++ b/packages/implementations/web/src/utils/getIssueLink.ts
@@ -0,0 +1,24 @@
+export const blockTicks = "```";
+
+const MAX_URL_LENGTH = 2 ** 13 - 1;
+
+export default (
+ title: string,
+ content: string | { [header: string]: string | number | boolean }
+) => {
+ const link = "https://github.com/maxijonson/gpt-turbo/issues/new";
+
+ const body = (() => {
+ if (typeof content === "string") return content;
+ return Object.entries(content)
+ .map(([header, value]) => `## ${header}\n${value}`)
+ .join("\n");
+ })();
+
+ const params = new URLSearchParams({
+ title,
+ body,
+ });
+
+ return `${link}?${params.toString()}`.slice(0, MAX_URL_LENGTH);
+};
diff --git a/packages/web/src/utils/getPriceString.ts b/packages/implementations/web/src/utils/getPriceString.ts
similarity index 82%
rename from packages/web/src/utils/getPriceString.ts
rename to packages/implementations/web/src/utils/getPriceString.ts
index c889b79..bea5a65 100644
--- a/packages/web/src/utils/getPriceString.ts
+++ b/packages/implementations/web/src/utils/getPriceString.ts
@@ -2,6 +2,6 @@ export default (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
- minimumFractionDigits: 4,
+ minimumFractionDigits: 5,
}).format(price);
};
diff --git a/packages/web/src/utils/getUniqueString.ts b/packages/implementations/web/src/utils/getUniqueString.ts
similarity index 100%
rename from packages/web/src/utils/getUniqueString.ts
rename to packages/implementations/web/src/utils/getUniqueString.ts
diff --git a/packages/web/src/utils/makeNotImplemented.ts b/packages/implementations/web/src/utils/makeNotImplemented.ts
similarity index 100%
rename from packages/web/src/utils/makeNotImplemented.ts
rename to packages/implementations/web/src/utils/makeNotImplemented.ts
diff --git a/packages/web/src/utils/readJsonFile.ts b/packages/implementations/web/src/utils/readJsonFile.ts
similarity index 100%
rename from packages/web/src/utils/readJsonFile.ts
rename to packages/implementations/web/src/utils/readJsonFile.ts
diff --git a/packages/web/src/utils/types.ts b/packages/implementations/web/src/utils/types.ts
similarity index 100%
rename from packages/web/src/utils/types.ts
rename to packages/implementations/web/src/utils/types.ts
diff --git a/packages/web/src/vite-env.d.ts b/packages/implementations/web/src/vite-env.d.ts
similarity index 100%
rename from packages/web/src/vite-env.d.ts
rename to packages/implementations/web/src/vite-env.d.ts
diff --git a/packages/web/tsconfig.json b/packages/implementations/web/tsconfig.json
similarity index 100%
rename from packages/web/tsconfig.json
rename to packages/implementations/web/tsconfig.json
diff --git a/packages/web/tsconfig.node.json b/packages/implementations/web/tsconfig.node.json
similarity index 100%
rename from packages/web/tsconfig.node.json
rename to packages/implementations/web/tsconfig.node.json
diff --git a/packages/web/vite.config.ts b/packages/implementations/web/vite.config.ts
similarity index 97%
rename from packages/web/vite.config.ts
rename to packages/implementations/web/vite.config.ts
index ace978a..213d464 100644
--- a/packages/web/vite.config.ts
+++ b/packages/implementations/web/vite.config.ts
@@ -73,6 +73,7 @@ export default defineConfig({
"lowlight",
],
"gpt-turbo": ["gpt-turbo"],
+ "gpt-turbo-plugin-stats": ["gpt-turbo-plugin-stats"],
react: ["react"],
"react-dom": ["react-dom"],
"react-icons": ["react-icons"],
diff --git a/packages/lib/.eslintrc.cjs b/packages/lib/.eslintrc.cjs
index d68ff96..ad7d668 100644
--- a/packages/lib/.eslintrc.cjs
+++ b/packages/lib/.eslintrc.cjs
@@ -28,6 +28,8 @@ module.exports = {
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/await-thenable": "warn",
"max-classes-per-file": "off",
"no-useless-constructor": "off",
"class-methods-use-this": "off",
diff --git a/packages/lib/README.md b/packages/lib/README.md
index 4a0ba7b..ba2c8b6 100644
--- a/packages/lib/README.md
+++ b/packages/lib/README.md
@@ -15,21 +15,18 @@ A library for interacting with OpenAI's Chat Completion API. Its main goal is to
npm install gpt-turbo
```
-## Usage
+## Basic Usage
If you want to jump right in and start a conversation with the GPT model, this is the most straightforward way to use this library.
```ts
import { Conversation } from 'gpt-turbo';
-(async () => {
- const conversation = new Conversation({
- apiKey: "sk-1234", /* Your OpenAI API key */
- });
+const apiKey = "sk-1234"; /* Your API key */
- const response = await conversation.prompt("What's the best way to make my code as precise as a Stormtrooper's aim?");
- console.log(`Response: ${response.content}`);
-})();
+const conversation = new Conversation({ config: { apiKey } });
+const response = await conversation.prompt("What's the best way to make my code as precise as a Stormtrooper's aim?");
+console.log(`Response: ${response.content}`);
```
## Advanced Usage
@@ -42,12 +39,21 @@ By default, messsage responses are returned in a single string. However, you can
import { Conversation } from "gpt-turbo";
const conversation = new Conversation({
- apiKey: "sk-1234",
- stream: true,
+ config: {
+ apiKey,
+ stream: true,
+ },
});
const response = await conversation.prompt("How can I make my code more efficient than a droid army?");
-const unsubscribeUpdate = response.onMessageUpdate((content) => {
+
+// New method (v5). Recommended for streamed responses as it automatically unsubscribes when the streaming stops.
+response.onContentStream(async (content, isStreaming) => {
+ console.log(content);
+});
+
+// Legacy method (v4). Still supported, but you need to unsubscribe manually.
+const unsubscribeUpdate = response.onUpdate((content) => {
console.log(content);
});
@@ -63,7 +69,7 @@ Save and load conversations are a breeze with these Conversation methods.
```ts
import { Conversation } from "gpt-turbo";
-import { save, load } from "./utils/db";
+import { save, load } from "./utils/db.js";
const conversationJson = await load();
const conversation = await Conversation.fromJSON(conversationJson);
@@ -76,33 +82,44 @@ await save(conversation.toJSON());
### Manual History Management
-The `prompt` method is the recommended way to prompt the GPT model, as it takes care of managing the message history for you (i.e. add a "user" message to the history, get the chat completion and add the "assistant" response to the history). However, if you need to do some intermediate steps, you can do this manually.
+The `prompt` method is the recommended way to prompt the GPT model, as it takes care of managing the message history for you (i.e. add a "user" message to the history, get the chat completion and add the "assistant" response to the history). However, if you need to do some intermediate steps, you can do this manually. The following code is roughly the equivalent of what the `prompt` method does.
```ts
import { Conversation } from "gpt-turbo";
-import { getRemainingCredits } from "./utils/quota";
+import { getRemainingCredits } from "./utils/quota.js";
-const conversation = new Conversation({ apiKey: "sk-1234" });
-const userMessage = await conversation.addUserMessage("How can I make my database more scalable than the Galactic Empire?");
+const conversation = new Conversation({ config: { apiKey } });
+const userMessage = conversation.history.addUserMessage("How can I make my database more scalable than the Galactic Empire?");
try {
+ const userFlags = await userMessage.moderate(apiKey);
+ if (userFlags.length > 0) {
+ throw new Error(userFlags.join(", "));
+ }
+
const remainingCredits = await getRemainingCredits();
- if (conversation.cost > remainingCredits) {
+ if (userMessage.content.length > remainingCredits) {
throw new Error("Insufficient credits, you have. Strong with the Force, your wallet is not.");
}
const assistantMessage = await conversation.getChatCompletionResponse();
+ await assistantMessage.moderate(apiKey);
+ if (assistantMessage.flags.length > 0) {
+ throw new Error(assistantMessage.flags.join(", "));
+ }
+
console.log(`Response: ${assistantMessage.content}`);
- await conversation.addAssistantMessage(assistantMessage.content);
+
+ await conversation.history.addAssistantMessage(assistantMessage.content);
} catch (e) {
- this.removeMessage(userMessage);
+ conversation.history.removeMessage(userMessage);
throw e;
}
```
### Message Moderation
-> Message moderation is also done in dry mode!
+> ⚠ Message moderation is also done in dry mode if you've specified an API key. This is because the moderation endpoint is free of charge and does not count towards your API usage quota.
By default, GPT Turbo will use your API key to call OpenAI's Moderation endpoint to make sure the message complies with their terms of service **before** prompting the Chat Completion API. This endpoint is free of charge and does not count towards your API usage quota. If it doesn't pass the moderation check, an error will be thrown. However, you can disable this behavior completely or still moderate the message without throwing an error (flags will be added to the message instead).
@@ -112,24 +129,30 @@ import { Conversation } from "gpt-turbo";
// Moderation enabled (default)
const conversation = new Conversation({
- apiKey: "sk-1234",
- disableModeration: false,
+ config: {
+ apiKey,
+ disableModeration: false, // Default
+ },
});
const response = await conversation.prompt("Execute Order 66."); // ModerationException: Message flagged for violence
// Soft moderation
const conversation = new Conversation({
- apiKey: "sk-1234",
- disableModeration: "soft",
+ config: {
+ apiKey,
+ disableModeration: "soft",
+ },
});
const response = await conversation.prompt("Execute Order 66."); // response.flags = ["violence"]
// Disable moderation
const conversation = new Conversation({
- apiKey: "sk-1234",
- disableModeration: true,
+ config: {
+ apiKey,
+ disableModeration: true,
+ },
});
const response = await conversation.prompt("Execute Order 66."); // "Yes my Lord."
```
@@ -141,7 +164,7 @@ Just like on ChatGPT, you can edit previous user messages or re-prompt the assis
```ts
import { Conversation } from "gpt-turbo";
-const conversation = new Conversation({ apiKey: "sk-1234" });
+const conversation = new Conversation({ config: { apiKey } });
const first = await conversation.prompt("We do not grant you the rank of Master."); // "How can you do this?!"
const second = await conversation.prompt("Take a seat, young Skywalker."); // "I will slaughter padawans!"
const edit = await conversation.reprompt(first, "We grant you the rank of Master.");
@@ -149,11 +172,7 @@ const edit = await conversation.reprompt(first, "We grant you the rank of Master
### Function Calling
-> ⚠ Function calling is relatively new and the implementation in this library may change as more is discovered about it.
->
-> Limitations (of the GPT Turbo library) with function calling:
-> - Token count is not currently calculated for assistant function calls and context. This means the cost of function calls are not taken into account at the moment. This will be fixed in a future release, as I learn more about how function call tokens are calculated by OpenAI.
-> - Function calls are not currently supported in dry mode. There is no planned support for this in the near future.
+> Function calls are not currently supported in dry mode. There is no planned support for this either.
You can use OpenAI's Function Calling feature with GPT Turbo through the `functionPrompt` method. Just define your functions in the conversation configuration just like you would normally with the Chat Completion API.
@@ -174,10 +193,11 @@ locateJediFn.addParameter(new CallableFunctionString("jedi", "The name of the Je
locateJediFn.addParameter(new CallableFunctionString("locationType", { enum: ["planet", "city"] }));
const conversation = new Conversation({
- apiKey: /** Your API key */,
- model: "gpt-3.5-turbo",
+ config: {
+ apiKey,
+ },
});
-conversation.addFunction(locateJediFn);
+conversation.callableFunctions.addFunction(locateJediFn);
const r1 = await conversation.prompt("Where can I find Obi-Wan Kenobi?");
@@ -193,22 +213,53 @@ if (r1.isCompletion()) {
}
```
-For streamed completions and function calls, it gets a bit more complicated, but still supported! Hopefully, a better flow will be implemented in the future.
+Function calls can also be streamed!
```ts
-const conversation = new Conversation({ /* ... */, stream: true });
-conversation.addFunction(locateJediFn);
+const conversation = new Conversation({
+ config: {
+ apiKey,
+ stream: true,
+ },
+});
+conversation.callableFunctions.addFunction(locateJediFn);
const r1 = await conversation.prompt("In which city is Obi-Wan Kenobi?");
-const unsubscribeUpdates = r1.onMessageUpdate((_, message) => {
+// New method (v5). Recommended for streamed responses as it automatically unsubscribes when the streaming stops, making it easier to manage.
+r1.onContentStream(async (_, isStreaming, message) => {
+ // Gracefully handle non-function call completions
+ // Optional if you're certain that all completions will be function calls (i.e, specifying `function_call` in the `config`)
+ if (message.isCompletion() && message.content) {
+ console.info("Completion", message.content);
+ return;
+ }
+
+ // Wait for function call to complete
+ if (isStreaming) return;
+ // Not a function call. Stop here.
+ if (!message.isFunctionCall()) return;
+
+ const { jedi, locationType } = message.functionCall.arguments;
+ const r2 = await conversation.functionPrompt(
+ message.functionCall.name,
+ locateJedi(jedi, locationType)
+ );
+
+ r2.onContentStream((content) => {
+ if (!content) return;
+ console.info("Function Call:", content); // "Obi-Wan Kenobi is located in the city of Mos Eisley."
+ });
+});
+
+// Legacy method (v4). Still supported, but it's much more complicated.
+const unsubscribeUpdates = r1.onUpdate((_, message) => {
if (!message.isCompletion()) {
return;
}
console.info(message.content);
});
-
-const unsubscribeStop = r1.onMessageStreamingStop(async (message) => {
+const unsubscribeStop = r1.onStreamingStop(async (message) => {
if (message.isFunctionCall()) {
const { jedi, locationType } = message.functionCall.arguments;
const r2 = await conversation.functionPrompt(
@@ -216,11 +267,11 @@ const unsubscribeStop = r1.onMessageStreamingStop(async (message) => {
locateJedi(jedi, locationType)
);
- const unsubscribeFunctionUpdate = r2.onMessageUpdate((content) => {
+ const unsubscribeFunctionUpdate = r2.onUpdate((content) => {
console.info(content); // "Obi-Wan Kenobi is located in the city of Mos Eisley."
});
- const unsubscribeFunctionStop = r2.onMessageStreamingStop(() => {
+ const unsubscribeFunctionStop = r2.onStreamingStop(() => {
unsubscribeFunctionUpdate();
unsubscribeFunctionStop();
});
@@ -243,7 +294,7 @@ locateJediFn.addParameter(new CallableFunctionString("jedi", "The name of the Je
locateJediFn.addParameter(new CallableFunctionString("locationType", { enum: ["planet", "city"] }));
// Create the parameters using CallableFunctionObject. we're passing a random name to the object because it is generally required for the parameters, but it won't be used in this case. Notice the "addProperty" instead of "addParameter" method.
-const parameters = new CallableFunctionObject("some_random_name");
+const parameters = new CallableFunctionObject("_");
parameters.addProperty(new CallableFunctionString("jedi", "The name of the Jedi to locate"), true);
parameters.addProperty(new CallableFunctionString("locationType", { enum: ["planet", "city"] }));
const locateJediFn = new CallableFunction(name, description, parameters);
@@ -285,24 +336,31 @@ const locateJediFn = CallableFunction.fromJSON({
});
// Finally, you also can totally ignore the CallableFunction class and pass your raw functions to the Conversation constructor
-const conversation = new Conversation({ apiKey: /** ... */, functions: [{
- name,
- description,
- parameters: {
- type: "object",
- properties: {
- jedi: {
- type: "string",
- description: "The name of the Jedi to locate",
- },
- locationType: {
- type: "string",
- enum: ["planet", "city"],
+const conversation = new Conversation({
+ config: { apiKey },
+ callableFunctions: {
+ functions: [
+ {
+ name,
+ description,
+ parameters: {
+ type: "object", // Notice that "type" will ALWAYS be "object", no matter what your function takes as parameters. This follows the JSON Object Schema specification.
+ properties: {
+ jedi: {
+ type: "string",
+ description: "The name of the Jedi to locate",
+ },
+ locationType: {
+ type: "string",
+ enum: ["planet", "city"],
+ },
+ },
+ required: ["jedi"],
+ },
},
- },
- required: ["jedi"],
+ ],
},
-}] })
+});
```
Just like every other class in this library, the `CallableFunction` and subclasses of `CallableFunctionParameter` all have a `toJSON` method and `fromJSON` static method. Each `CallableFunctionParameter` subclass also have Zod schemas exported so that you can validate their JSON representation. Here are all subclasses of `CallableFunctionParameter`:
@@ -318,6 +376,113 @@ Just like every other class in this library, the `CallableFunction` and subclass
There is also a `CallableFunctionParameterFactory.fromJSON` method which is used internally by the `CallableFunctionObject` and `CallableFunctionArray` classes to create their properties/items dynamically from a JSON object.
+## Conversation Plugins
+
+Conversation Plugins allow you to extend the functionality of GPT Turbo or simply to attach listeners just like you would without plugins. Throughout the conversation's lifecycle, several events will be triggered by the library so that plugins may tap into them. For example, they can modify the content of a user message during a `prompt` or `reprompt` call.
+
+You can find a detailed example with home-made [`gpt-turbo-plugin-stats` plugin](../plugins/gpt-turbo-plugin-stats/README.md).
+
+### Using a plugin
+
+Plugins can be injected in the `Conversation` constructor through the `plugins` option, or defined as a global plugin that will be used by all conversations.
+
+```ts
+import { Conversation } from "gpt-turbo";
+import stats from "gpt-turbo-plugin-stats";
+
+// Injected in the constructor
+const conversation = new Conversation({
+ plugins: [stats],
+});
+
+// Global plugin
+const globalPlugins = [stats];
+Conversation.globalPlugins = globalPlugins;
+const conversation = new Conversation();
+
+// Typing the global plugins for type safety
+declare module "gpt-turbo" {
+ interface ConversationGlobalPluginsOverride {
+ globalPlugins: typeof globalPlugins;
+ }
+}
+```
+
+If your plugin exposes an output meant to be used by client code, you can use the `getPluginOutput` method to retrieve it. If you're using TypeScript, the plugin output will be fully typed as long as you're accessing the plugin through a literal string or a constant. You should also notice you get automatic intellisense when typing the plugin name. If you're getting the plugin dynamically, the plugin output defaults to `any` and could be `undefined`. Usually, it's recommended that plugin authors export a type guard to properly type the plugin output for dynamic use (more on that later).
+
+```ts
+const pluginOutput = conversation.plugins.getPluginOutput("pluginName");
+const pluginOutput = conversation.plugins.getPlugin("pluginName").out; // Same as the above
+```
+
+### Authoring a plugin
+
+Plugins can be authored for a specific project only or be published as a standalone package. If you're publishing a standalone package, it's recommended you follow the `gpt-turbo-plugin-` naming convention. This will make it easier for users to find your plugin and will also make it easier for you to find a name that isn't already taken. You can also add the `gpt-turbo-plugin` to your tags on npm to make it easier to find.
+
+Plugins are functions that receive every `Conversation` properties, even the ones that are normally private to regular usage, such as `ChatCompletionService` and `PluginService`. They also receive optional plugin data persisted through the `getPluginData` property of your plugin. Your plugin function returns the **plugin definition**, which is an object with the properties you want your plugin to react to during the conversation lifecycle.
+
+For convenience, it's recommended to use the `createConversationPlugin` function, but you could technically define it manually with the `ConversationPlugin` interface exported by the library.
+
+```ts
+import { createConversationPlugin } from "gpt-turbo";
+
+// Recommended to expose the plugin name as a constant
+export const myPluginName = "myPlugin";
+
+export const myPlugin = createConversationPlugin(myPluginName, ({ conversation, history, /* ... */ }, pluginData?: number) => {
+ if (pluginData) {
+ console.log("Plugin data was persisted from a previous conversation (new Conversation(prevConversation.toJSON()))");
+ console.log(`Plugin data: ${pluginData}`);
+ } else {
+ console.log("First time plugin is used (new **Conversation**)");
+ }
+
+ return {
+ onUserPrompt: async (message) => {
+ console.log(`User prompt: ${message.content}`);
+ message.content = await yodaSpeak(message.content);
+ },
+ getPluginData: () => (pluginData ?? 0) + 1,
+ out: "Hello there!", // Plugin output available through `conversation.plugins.getPluginOutput("myPlugin")`. Can be virtually anything you want! (usually a function or a class instance though!)
+ /* Many more methods you can tap into... */
+ }
+});
+```
+
+#### Recommended practices for TypeScript
+
+For TypeScript users, the `conversation.getPlugin` method (and other alike such as `getPluginOutput`) has strong type inference to infer the plugin output and data type when called with a literal. However, in some cases, users may want to dynamically get plugins by their name. To help differentiate your plugin from other plugins in these cases while preserving types, it's recommended to ship a type guard with your plugin.
+
+```ts
+/* ... */
+import { ConversationPluginDefinitionFromPlugin, ConversationPluginDefinition } from "gpt-turbo";
+
+export type MyPluginDefinition = ConversationPluginDefinitionFromPlugin;
+export const isMyPlugin = (plugin?: ConversationPluginDefinition): plugin is MyPluginDefinition => {
+ return plugin?.name === myPluginName;
+};
+```
+
+#### Custom plugin configuration
+
+Because plugins are functions, you can allow custom options to be passed to your plugin by client code. This isn't a feature of GPT Turbo, it's just JavaScript! As long as your plugin function returns a plugin definition, you can do whatever you want with it.
+
+```ts
+const myPlugin = (options) => {
+ // do something with options, maybe
+ return createConversationPlugin(myPluginName, ({ conversation, history, /* ... */ }, pluginData?: number) => {
+ /* do something else with options, maybe */
+ return { /* ... */ };
+ });
+}
+
+// Usage
+const options = { /* ... */ };
+const conversation = new Conversation({
+ plugins: [myPlugin(options)],
+});
+```
+
## Documentation
View the full documentation [here](https://gpt-turbo.chintristan.io/). The documentation website is auto-generated based on the TSdoc comments in the source code for the latest version of the library.
diff --git a/packages/lib/package.json b/packages/lib/package.json
index 8e99dff..8019432 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -8,6 +8,7 @@
"lint": "eslint --ext .ts src",
"lint:strict": "npm run lint -- --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
+ "tscheck": "tsc --noEmit",
"build": "npm run lint:strict && rimraf dist && tsc -p tsconfig.build.json && copyfiles -u 1 -e \"src/**/*.ts\" \"src/**/*\" dist",
"docs": "typedoc"
},
@@ -55,21 +56,20 @@
"access": "public"
},
"devDependencies": {
- "@types/uuid": "^9.0.1",
- "@typescript-eslint/eslint-plugin": "^5.54.0",
- "@typescript-eslint/parser": "^5.54.0",
+ "@types/uuid": "^9.0.2",
+ "@typescript-eslint/eslint-plugin": "^6.2.0",
+ "@typescript-eslint/parser": "^6.2.0",
"copyfiles": "^2.4.1",
- "eslint": "^8.35.0",
- "eslint-config-prettier": "^8.6.0",
- "eslint-plugin-prettier": "^4.2.1",
- "prettier": "^2.8.4",
- "rimraf": "^4.2.0",
+ "eslint": "^8.46.0",
+ "eslint-config-prettier": "^8.9.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "prettier": "^3.0.0",
+ "rimraf": "^5.0.1",
"ts-node": "^10.9.1",
- "typedoc": "^0.24.1",
- "typescript": "^4.9.5"
+ "typedoc": "^0.24.8",
+ "typescript": "^5.1.6"
},
"dependencies": {
- "gpt-token-utils": "^1.2.0",
"uuid": "^9.0.0",
"zod": "^3.21.4"
},
diff --git a/packages/lib/src/classes/CallableFunction.ts b/packages/lib/src/classes/CallableFunction.ts
index 32c93cc..d329687 100644
--- a/packages/lib/src/classes/CallableFunction.ts
+++ b/packages/lib/src/classes/CallableFunction.ts
@@ -4,9 +4,9 @@ import {
} from "../schemas/callableFunction.schema.js";
import { v4 as uuid } from "uuid";
import { CallableFunctionObject } from "./CallableFunctionParameters/CallableFunctionObject.js";
-import { CreateChatCompletionFunction } from "../utils/types.js";
import { CallableFunctionParameter } from "./CallableFunctionParameters/CallableFunctionParameter.js";
-import { JsonSchema, JsonSchemaObject } from "../index.js";
+import { JsonSchema, JsonSchemaObject } from "../schemas/jsonSchema.schema.js";
+import { CreateChatCompletionFunction } from "../utils/types/index.js";
export class CallableFunction {
/**
diff --git a/packages/lib/src/classes/CallableFunctionParameters/CallableFunctionParameter.ts b/packages/lib/src/classes/CallableFunctionParameters/CallableFunctionParameter.ts
index 66b27b1..0559298 100644
--- a/packages/lib/src/classes/CallableFunctionParameters/CallableFunctionParameter.ts
+++ b/packages/lib/src/classes/CallableFunctionParameters/CallableFunctionParameter.ts
@@ -5,7 +5,7 @@ import {
} from "../../schemas/jsonSchema.schema.js";
export abstract class CallableFunctionParameter<
- TModel extends JsonSchemaBase = JsonSchemaBase
+ TModel extends JsonSchemaBase = JsonSchemaBase,
> {
abstract readonly type: string;
@@ -23,7 +23,10 @@ export abstract class CallableFunctionParameter<
deprecated?: JsonSchemaBase["deprecated"];
$comment?: JsonSchemaBase["$comment"];
- constructor(public name: string, options: JsonSchemaBase = {}) {
+ constructor(
+ public name: string,
+ options: JsonSchemaBase = {}
+ ) {
this.name = name;
this.title = options.title;
this.description = options.description;
diff --git a/packages/lib/src/classes/ChatCompletionService.ts b/packages/lib/src/classes/ChatCompletionService.ts
new file mode 100644
index 0000000..297833d
--- /dev/null
+++ b/packages/lib/src/classes/ChatCompletionService.ts
@@ -0,0 +1,171 @@
+import { Message } from "./Message.js";
+import { ConversationConfig } from "./ConversationConfig.js";
+import { ConversationRequestOptions } from "./ConversationRequestOptions.js";
+import { ConversationHistory } from "./ConversationHistory.js";
+import { ConversationCallableFunctions } from "./ConversationCallableFunctions.js";
+import { ConversationRequestOptionsModel } from "../schemas/conversationRequestOptions.schema.js";
+import { ModerationException } from "../exceptions/ModerationException.js";
+import createDryChatCompletion from "../utils/createDryChatCompletion.js";
+import createChatCompletion from "../utils/createChatCompletion.js";
+import {
+ HandleChatCompletionOptions,
+ PromptOptions,
+} from "../utils/types/index.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+
+/**
+ * Encapsulates the logic for sending requests to the OpenAI API for the Create Chat Completion endpoint.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ChatCompletionService {
+ constructor(
+ private readonly pluginService: ConversationPluginService,
+ private readonly config: ConversationConfig,
+ private readonly requestOptions: ConversationRequestOptions,
+ private readonly history: ConversationHistory,
+ private readonly callableFunctions: ConversationCallableFunctions
+ ) {}
+
+ /**
+ * @internal
+ * Should not be used directly by library consumers. Use `getChatCompletionResponse` from the `Conversation` class instead.
+ */
+ public async getChatCompletionResponse(
+ options: PromptOptions = {},
+ requestOptions: ConversationRequestOptionsModel = {}
+ ): Promise {
+ const stream = options.stream ?? this.config.stream;
+ const message = stream
+ ? this.handleStreamedResponse(options, requestOptions)
+ : await this.handleNonStreamedResponse(options, requestOptions);
+ await this.pluginService.onChatCompletion(message);
+ return message;
+ }
+
+ /**
+ * Gets the assistant's response given the current messages stored in the conversation's history, moderates it if moderation is enabled, and adds it to the conversation's history.
+ *
+ * @param options Additional options to pass to the Create Chat Completion API endpoint. This overrides the config passed to the constructor.
+ * @param requestOptions Additional options to pass for the HTTP request. This overrides the config passed to the constructor.
+ * @returns The assistant's response as a [`Message`](./Message.js) instance.
+ */
+ public async getAssistantResponse(
+ options?: PromptOptions,
+ requestOptions?: ConversationRequestOptionsModel
+ ) {
+ const response = await this.getChatCompletionResponse(
+ options,
+ requestOptions
+ );
+ await this.moderateMessage(response);
+ return this.history.addMessage(response);
+ }
+
+ /**
+ * @internal
+ * Should not be used directly by library consumers. Use `moderate` from the `Message` class instead.
+ */
+ public async moderateMessage(message: Message) {
+ if (!this.config.isModerationEnabled) return;
+
+ await message.moderate(
+ this.config.apiKey,
+ this.requestOptions.getRequestOptions()
+ );
+ await this.pluginService.onModeration(message);
+
+ const flags = message.flags ?? [];
+
+ if (this.config.isModerationStrict && flags.length > 0) {
+ throw new ModerationException(flags);
+ }
+ }
+
+ public handleStreamedResponse(
+ options: HandleChatCompletionOptions = {},
+ requestOptions: ConversationRequestOptionsModel = {}
+ ) {
+ const message = new Message("assistant", "", this.config.model);
+ const messages = this.history.getCreateChatCompletionMessages();
+
+ if (this.config.dry) {
+ const historyMessages = this.history.getMessages();
+ const response = createDryChatCompletion(
+ historyMessages[historyMessages.length - 1]?.content ?? "",
+ {
+ model: this.config.model,
+ }
+ );
+ message.readContentFromStream(response);
+ } else {
+ createChatCompletion(
+ {
+ ...this.config.getChatCompletionConfig(),
+ ...options,
+ stream: true,
+ messages,
+ functions:
+ this.callableFunctions.getCreateChatCompletionFunctions(),
+ },
+ {
+ ...this.requestOptions.getRequestOptions(),
+ ...requestOptions,
+ }
+ ).then((response) => {
+ // Using .then() to get the message out as soon as possible, since the content is known to be empty at first.
+ // This gives time for client code to subscribe to the streaming events.
+ message.readContentFromStream(response);
+ });
+ }
+
+ return message;
+ }
+
+ public async handleNonStreamedResponse(
+ options: HandleChatCompletionOptions = {},
+ requestOptions: ConversationRequestOptionsModel = {}
+ ) {
+ const message = new Message("assistant", "", this.config.model);
+ const messages = this.history.getCreateChatCompletionMessages();
+
+ if (this.config.dry) {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ message.content = messages[messages.length - 1]?.content ?? null;
+ } else {
+ const response = await createChatCompletion(
+ {
+ ...this.config.getChatCompletionConfig(),
+ ...options,
+ stream: false,
+ messages,
+ functions:
+ this.callableFunctions.getCreateChatCompletionFunctions(),
+ },
+ {
+ ...this.requestOptions.getRequestOptions(),
+ ...requestOptions,
+ }
+ );
+ const responseMessage = response.choices[0].message;
+ message.content = responseMessage.content;
+ if (responseMessage.function_call) {
+ try {
+ message.functionCall = {
+ name: responseMessage.function_call.name,
+ arguments: JSON.parse(
+ responseMessage.function_call.arguments
+ ),
+ };
+ } catch {
+ throw new Error(
+ "Assistant did not generate valid JSON arguments."
+ );
+ }
+ }
+ }
+
+ return message;
+ }
+}
diff --git a/packages/lib/src/classes/Conversation.ts b/packages/lib/src/classes/Conversation.ts
index 1c02495..24d38a6 100644
--- a/packages/lib/src/classes/Conversation.ts
+++ b/packages/lib/src/classes/Conversation.ts
@@ -1,170 +1,149 @@
import { ConversationConfig } from "./ConversationConfig.js";
-import {
- createChatCompletion,
- createDryChatCompletion,
-} from "../utils/index.js";
-import { ModerationException } from "../exceptions/ModerationException.js";
import { Message } from "./Message.js";
-import { MessageRoleException } from "../exceptions/index.js";
import { v4 as uuid } from "uuid";
-import {
- AddMessageListener,
- ConversationConfigParameters,
- HandleChatCompletionOptions,
- PromptOptions,
- RemoveMessageListener,
- RequestOptions,
-} from "../utils/types.js";
-import { DEFAULT_DISABLEMODERATION } from "../index.js";
import {
ConversationModel,
conversationSchema,
} from "../schemas/conversation.schema.js";
-import { MessageModel } from "../schemas/message.schema.js";
-import { CallableFunction } from "./CallableFunction.js";
-import { CallableFunctionModel } from "../schemas/callableFunction.schema.js";
+import { ConversationRequestOptions } from "./ConversationRequestOptions.js";
+import { ConversationHistory } from "./ConversationHistory.js";
+import { ConversationCallableFunctions } from "./ConversationCallableFunctions.js";
+import { ConversationRequestOptionsModel } from "schemas/conversationRequestOptions.schema.js";
+import { ChatCompletionService } from "./ChatCompletionService.js";
+import {
+ ConversationGlobalPlugins,
+ ConversationOptions,
+ PluginsFromConversationOptionsWithGlobalPlugins,
+ PromptOptions,
+} from "../utils/types/index.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+import { ConversationPlugins } from "./ConversationPlugins.js";
/**
* A Conversation manages the messages sent to and from the OpenAI API and handles the logic for providing the message history to the API for each prompt.
*/
-export class Conversation {
+export class Conversation<
+ TOptions extends ConversationOptions = ConversationOptions,
+> {
/**
- * A UUID generated by the library for this conversation. Not the same as the conversation ID returned by the OpenAI API.
+ * Plugins that will be used for all conversations.
+ *
+ * @remarks
+ * Only applies to conversations created after this property is set. Previous conversations will not be affected.
+ *
+ * @example
+ * For TypeScript users, you can achieve type-safe global plugins by overriding the `ConversationGlobalPluginsOverride` interface.
+ * ```ts
+ * const globalPlugins = [somePlugin, someOtherPlugin];
+ *
+ * declare module "gpt-turbo" {
+ * interface ConversationGlobalPluginsOverride {
+ * globalPlugins: typeof globalPlugins;
+ * }
+ * }
+ *
+ * Conversation.globalPlugins = globalPlugins;
+ * ```
*/
- public id = uuid();
-
- private config!: ConversationConfig;
- private requestOptions: RequestOptions;
- private messages: Message[] = [];
- private functions: CallableFunction[] = [];
- private addMessageListeners: AddMessageListener[] = [];
- private removeMessageListeners: RemoveMessageListener[] = [];
- private cumulativeSize = 0;
- private cumulativeCost = 0;
+ public static globalPlugins: ConversationGlobalPlugins = [];
/**
- * Creates a new Conversation instance.
- *
- * @param config The configuration for this conversation. See {@link ConversationConfigParameters}
- * @param requestOptions The HTTP request options. See {@link RequestOptions}
+ * A UUID generated by the library for this conversation. Not the same as the conversation ID returned by the OpenAI API.
*/
- constructor(
- config: ConversationConfigParameters = {},
- requestOptions: RequestOptions = {}
- ) {
- this.setConfig(config);
- this.requestOptions = requestOptions;
- this.clearMessages();
- }
+ public id: string;
+
+ public readonly config: ConversationConfig;
+ public readonly requestOptions: ConversationRequestOptions;
+ public readonly history: ConversationHistory;
+ public readonly callableFunctions: ConversationCallableFunctions;
+ public readonly plugins: ConversationPlugins<
+ PluginsFromConversationOptionsWithGlobalPlugins
+ >;
+
+ private readonly chatCompletionService: ChatCompletionService;
+ private readonly pluginService: ConversationPluginService<
+ PluginsFromConversationOptionsWithGlobalPlugins
+ >;
/**
- * Creates a new Conversation instance from a list of messages.
+ * Creates a new Conversation instance.
*
- * @param messages The messages (as JSON) to add to the conversation. See {@link MessageModel}
- * @param config The configuration for this conversation. See {@link ConversationConfigParameters}
- * @param requestOptions The HTTP request options. See {@link RequestOptions}
- * @param disableInitialModeration Whether to disable moderation for the initial messages. Defaults to `true` to prevent multiple API calls in a short period of time.
- * @returns The new Conversation instance.
- */
- public static async fromMessages(
- messages: MessageModel[],
- config: ConversationConfigParameters = {},
- requestOptions: RequestOptions = {},
- disableInitialModeration: ConversationConfigParameters["disableModeration"] = true
- ) {
- const conversation = new Conversation(
- {
- ...config,
- disableModeration: disableInitialModeration,
- },
+ * @param options The options for the Conversation instance's configuration, request options, history, and callable functions.
+ */
+ constructor(options?: TOptions) {
+ const {
+ id = uuid(),
+ config,
+ requestOptions,
+ history,
+ callableFunctions,
+ plugins = [],
+ pluginsData,
+ } = options ?? {};
+
+ this.id = id;
+
+ this.pluginService = new ConversationPluginService([
+ ...Conversation.globalPlugins,
+ ...plugins,
+ ]);
+
+ this.config = new ConversationConfig(this.pluginService, config);
+ this.requestOptions = new ConversationRequestOptions(
+ this.pluginService,
requestOptions
);
+ this.history = new ConversationHistory(
+ this.pluginService,
+ this.config,
+ history
+ );
+ this.callableFunctions = new ConversationCallableFunctions(
+ this.pluginService,
+ callableFunctions
+ );
+ this.plugins = new ConversationPlugins(this.pluginService);
+
+ this.chatCompletionService = new ChatCompletionService(
+ this.pluginService,
+ this.config,
+ this.requestOptions,
+ this.history,
+ this.callableFunctions
+ );
- for (const message of messages) {
- switch (message.role) {
- case "user":
- if (message.content === null)
- throw new Error("User message content cannot be null.");
- await conversation.addUserMessage(message.content);
- break;
- case "assistant":
- if (message.content === null) {
- if (!message.function_call)
- throw new Error("Function call must be provided.");
- await conversation.addFunctionCallMessage(
- message.function_call
- );
- } else {
- await conversation.addAssistantMessage(message.content);
- }
- break;
- case "system":
- if (message.content === null)
- throw new Error("Context cannot be null.");
- conversation.setContext(message.content);
- break;
- case "function":
- if (!message.name)
- throw new Error("Function name must be specified.");
- if (message.content === null)
- throw new Error(
- "Function message content cannot be null."
- );
- await conversation.addFunctionMessage(
- message.content,
- message.name
- );
- break;
- }
- }
-
- conversation.setConfig(
+ this.pluginService.onInit(
{
- disableModeration:
- config.disableModeration || DEFAULT_DISABLEMODERATION,
+ conversation: this,
+ config: this.config,
+ requestOptions: this.requestOptions,
+ history: this.history,
+ callableFunctions: this.callableFunctions,
+ plugins: this.plugins,
+ chatCompletionService: this.chatCompletionService,
+ pluginService: this.pluginService,
},
- true
+ pluginsData
);
-
- return conversation;
}
/**
* Creates a new Conversation instance from a JSON object.
*
* @param json The JSON object of the Conversation instance.
- * @param config Overrides for the loaded configuration of this conversation. See {@link ConversationConfigParameters}
- * @param requestOptions Overrides for the loaded HTTP request options. See {@link RequestOptions}
- * @param disableInitialModeration Whether to disable moderation for the initial messages. Defaults to `true` to prevent multiple API calls in a short period of time.
+ * @param plugins The plugins to use for the Conversation instance.
* @returns The new Conversation instance.
*/
- public static async fromJSON(
+ public static fromJSON(
json: ConversationModel,
- config: ConversationConfigParameters = {},
- requestOptions: RequestOptions = {},
- disableInitialModeration: ConversationConfigParameters["disableModeration"] = true
+ plugins?: ConversationOptions["plugins"]
) {
const conversationJson = conversationSchema.parse(json);
- const conversation = await Conversation.fromMessages(
- conversationJson.messages,
- {
- ...(conversationJson.config ?? {}),
- ...config,
- },
- {
- ...(conversationJson.requestOptions ?? {}),
- ...requestOptions,
- },
- disableInitialModeration
- );
-
- if (conversationJson.id) conversation.id = conversationJson.id;
-
- conversationJson.functions.forEach((fn) =>
- conversation.addFunction(fn)
- );
-
- return conversation;
+ const conversationOptions: ConversationOptions = {
+ ...conversationJson,
+ plugins,
+ };
+ return new Conversation(conversationOptions);
}
/**
@@ -176,223 +155,15 @@ export class Conversation {
const json: ConversationModel = {
id: this.id,
config: this.config.toJSON(),
- messages: this.messages.map((message) => message.toJSON()),
- functions: this.functions.map((fn) => fn.toJSON()),
- requestOptions: this.requestOptions,
+ requestOptions: this.requestOptions.toJSON(),
+ callableFunctions: this.callableFunctions.toJSON(),
+ history: this.history.toJSON(),
+ pluginsData: this.pluginService.getPluginsData(),
};
- return conversationSchema.parse(json);
- }
- /**
- * Adds a message with the role of `"assistant"` to the conversation.
- *
- * @param message The content of the message.
- * @returns The [Message](./utils/types.ts) object that was added to the conversation
- */
- public addAssistantMessage(message: string) {
- const assistantMessage = new Message(
- "assistant",
- message,
- this.config.model
+ return conversationSchema.parse(
+ this.pluginService.transformConversationJson(json)
);
- if (!assistantMessage.isCompletion()) {
- throw new Error("Not a completion message.");
- }
- return this.addMessage(assistantMessage);
- }
-
- /**
- * Adds a message with the role of "user" to the conversation.
- *
- * @param message The content of the message.
- * @returns The [Message](./utils/types.ts) object that was added to the conversation
- */
- public addUserMessage(message: string) {
- const userMessage = new Message("user", message, this.config.model);
- if (!userMessage.isCompletion()) {
- throw new Error("Not a completion message.");
- }
- return this.addMessage(userMessage);
- }
-
- /**
- * Adds a message with the role of "assistant" to the conversation with the function call generated by the assistant.
- *
- * @param functionCall The name and arguments of the function to call, generated by the assistant.
- * @returns The [Message](./utils/types.ts) object that was added to the conversation
- */
- public addFunctionCallMessage(functionCall: {
- name: string;
- arguments: Record;
- }) {
- const functionCallMessage = new Message(
- "assistant",
- null,
- this.config.model
- );
- functionCallMessage.functionCall = functionCall;
- if (!functionCallMessage.isFunctionCall()) {
- throw new Error("Not a function call message.");
- }
- return this.addMessage(functionCallMessage);
- }
-
- /**
- * Adds a message with the role of "function" to the conversation with the content being the return value of the function call, called by your own code.
- *
- * @param message The return value of the function call, stringified if needed.
- * @param name The name of the function that was called.
- * @returns The [Message](./utils/types.ts) object that was added to the conversation
- */
- public addFunctionMessage(message: string, name: string) {
- const functionMessage = new Message(
- "function",
- message,
- this.config.model
- );
- functionMessage.name = name;
- if (!functionMessage.isFunction()) {
- throw new Error("Not a function message.");
- }
- return this.addMessage(functionMessage);
- }
-
- /**
- * Get the messages in the conversation.
- *
- * @param includeContext Whether to include the context message in the returned array.
- * @returns A **shallow copy** of the messages array.
- */
- public getMessages(includeContext = false) {
- if (!this.config.context) return this.messages.slice(0);
- return includeContext ? this.messages.slice(0) : this.messages.slice(1);
- }
-
- /**
- * Removes a callable function from the conversation.
- *
- * @param idOrFn Either the ID of the function, the function itself, or the function model.
- */
- public removeFunction(
- idOrFn: string | CallableFunction | CallableFunctionModel
- ) {
- const id = typeof idOrFn === "string" ? idOrFn : idOrFn.id;
- const removedFn = this.functions.find((fn) => fn.id === id);
- if (!removedFn) return;
- this.functions = this.functions.filter((fn) => fn.id !== id);
- }
-
- /**
- * Removes all functions from the conversation.
- */
- public clearFunctions() {
- this.functions = [];
- }
-
- /**
- * Adds a function to the conversation. This function can be "called" by the assistant, generating a function call message.
- *
- * @param fn The function to add to the conversation.
- */
- public addFunction(fn: CallableFunction | CallableFunctionModel) {
- this.functions = this.functions
- .filter((f) => f.name !== fn.name && f.id !== fn.id)
- .concat(
- fn instanceof CallableFunction
- ? fn
- : CallableFunction.fromJSON(fn)
- );
- }
-
- /**
- * Get the functions in the conversation.
- *
- * @returns A **shallow copy** of the functions array.
- */
- public getFunctions() {
- return this.functions.slice(0);
- }
-
- /**
- * Removes a listener function from the list of listeners that was previously added with `onMessageAdded`.
- *
- * @param listener The function to remove from the list of listeners.
- */
- public offMessageAdded(listener: AddMessageListener) {
- this.addMessageListeners = this.addMessageListeners.filter(
- (l) => l !== listener
- );
- }
-
- /**
- * Adds a listener function that is called whenever a message is added to the conversation.
- *
- * @param listener The function to call when a message is added to the conversation.
- * @returns A function that removes the listener from the list of listeners.
- */
- public onMessageAdded(listener: AddMessageListener) {
- this.addMessageListeners.push(listener);
- return () => this.offMessageAdded(listener);
- }
-
- /**
- * Removes a listener function from the list of listeners that was previously added with `onMessageRemoved`.
- *
- * @param listener The function to remove from the list of listeners.
- */
- public offMessageRemoved(listener: RemoveMessageListener) {
- this.removeMessageListeners = this.removeMessageListeners.filter(
- (l) => l !== listener
- );
- }
-
- /**
- * Adds a listener function that is called whenever a message is removed to the conversation.
- *
- * @param listener The function to call when a message is removed to the conversation.
- * @returns A function that removes the listener from the list of listeners.
- */
- public onMessageRemoved(listener: RemoveMessageListener) {
- this.removeMessageListeners.push(listener);
- return () => this.offMessageRemoved(listener);
- }
-
- /**
- * Clears all messages in the conversation except the context message, if it is set.
- */
- public clearMessages() {
- this.messages = [];
- this.addSystemMessage(this.config.context);
- }
-
- /**
- * Removes a message from the conversation's history.
- *
- * @param message Either the ID of the message to remove, or the message object itself (where the ID will be extracted from).
- */
- public removeMessage(message: string | Message) {
- const id = typeof message === "string" ? message : message.id;
- const removedMessage = this.messages.find((m) => m.id === id);
- if (!removedMessage) return;
- this.messages = this.messages.filter((m) => m.id !== id);
- this.notifyMessageRemoved(removedMessage);
- }
-
- /**
- * Sends a Create Chat Completion request to the OpenAI API using the current messages stored in the conversation's history.
- *
- * @param options Additional options to pass to the Create Chat Completion API endpoint. This overrides the config passed to the constructor.
- * @param requestOptions Additional options to pass for the HTTP request. This overrides the config passed to the constructor.
- * @returns A new [`Message`](./Message.js) instance with the role of "assistant" and the content set to the response from the OpenAI API. If the `stream` config option was set to `true`, the content will be progressively updated as the response is streamed from the API. Listen to the returned message's `onMessageUpdate` event to get the updated content.
- */
- public async getChatCompletionResponse(
- options: PromptOptions = {},
- requestOptions: RequestOptions = {}
- ): Promise {
- const stream = options.stream ?? this.config.stream;
- return stream
- ? this.handleStreamedResponse(options, requestOptions)
- : this.handleNonStreamedResponse(options, requestOptions);
}
/**
@@ -406,18 +177,22 @@ export class Conversation {
public async prompt(
prompt: string,
options?: PromptOptions,
- requestOptions?: RequestOptions
+ requestOptions?: ConversationRequestOptionsModel
) {
- const userMessage = await this.addUserMessage(prompt);
+ const userMessage = this.history.addUserMessage(prompt);
try {
- const assistantMessage = await this.getAssistantResponse(
- options,
- requestOptions
- );
+ await this.pluginService.onUserPrompt(userMessage);
+ await this.chatCompletionService.moderateMessage(userMessage);
+ const assistantMessage =
+ await this.chatCompletionService.getAssistantResponse(
+ options,
+ requestOptions
+ );
return assistantMessage;
} catch (e) {
- this.removeMessage(userMessage);
+ await this.pluginService.onUserPromptError(e);
+ this.history.removeMessage(userMessage);
throw e;
}
}
@@ -428,7 +203,7 @@ export class Conversation {
*
* This is useful if you want to edit a previous user message (by specifying `newPrompt`) or if you want to regenerate the response to a previous user message (by not specifying `newPrompt`).
*
- * @param fromMessage The message to re-prompt from. This can be either a message ID or a [`Message`](./Message.js) instance.
+ * @param fromMessageOrId The message to re-prompt from. This can be either a message ID or a [`Message`](./Message.js) instance.
* @param newPrompt The new prompt to use for the previous user message. If not provided, the previous user's message content will be reused.
* @param options Additional options to pass to the Create Chat Completion API endpoint. This overrides the config passed to the constructor.
* @param requestOptions Additional options to pass for the HTTP request. This overrides the config passed to the constructor.
@@ -447,26 +222,29 @@ export class Conversation {
* ```
*/
public async reprompt(
- fromMessage: string | Message,
+ fromMessageOrId: string | Message,
newPrompt?: string,
options?: PromptOptions,
- requestOptions?: RequestOptions
+ requestOptions?: ConversationRequestOptionsModel
) {
// Find the message to reprompt from
const id =
- typeof fromMessage === "string" ? fromMessage : fromMessage.id;
- const fromIndex = this.messages.findIndex((m) => m.id === id);
+ typeof fromMessageOrId === "string"
+ ? fromMessageOrId
+ : fromMessageOrId.id;
+ const messages = this.history.getMessages();
+ const fromIndex = messages.findIndex((m) => m.id === id);
if (fromIndex === -1) {
throw new Error(`Message with ID "${id}" not found.`);
}
// Find the previous user message
let previousUserMessageIndex = fromIndex;
- let previousUserMessage = this.messages[previousUserMessageIndex];
+ let previousUserMessage = messages[previousUserMessageIndex];
while (previousUserMessage.role !== "user") {
previousUserMessageIndex--;
if (previousUserMessageIndex < 0) break;
- previousUserMessage = this.messages[previousUserMessageIndex];
+ previousUserMessage = messages[previousUserMessageIndex];
}
if (previousUserMessage?.role !== "user") {
throw new Error(
@@ -474,27 +252,18 @@ export class Conversation {
);
}
+ // Remove all messages from the previous user message (including it)
+ messages
+ .slice(previousUserMessageIndex)
+ .forEach((m) => this.history.removeMessage(m));
+
// Edit the previous user message if needed
if (newPrompt) {
previousUserMessage.content = newPrompt;
}
- // Remove all messages after the previous user message
- this.messages
- .slice(previousUserMessageIndex + 1)
- .forEach((m) => this.removeMessage(m));
-
- try {
- // Get the new assistant response
- const assistantMessage = await this.getAssistantResponse(
- options,
- requestOptions
- );
- return assistantMessage;
- } catch (e) {
- this.removeMessage(previousUserMessage);
- throw e;
- }
+ const prompt = newPrompt ?? previousUserMessage.content ?? "";
+ return this.prompt(prompt, options, requestOptions);
}
/**
@@ -502,7 +271,7 @@ export class Conversation {
* This method should usually be called after receiving a function_call message from the assistant (using `getChatCompletionResponse()` or `prompt()`) and evaluating your own function with the provided arguments from that message.
*
* @param name The name of the function used to generate the result. This function must be defined in the `functions` config option.
- * @param result The result of the function call. If the result is anything other than a string, it will be JSON stringified. Since `result` can be anything, the `T` template is provided for your typing convenience, but is not used internally
+ * @param result The result of the function call. If the result is anything other than a string, it will be JSON stringified. Since `result` can be anything, the `T` generic is provided for your typing convenience, but is not used internally
* @param options Additional options to pass to the Create Chat Completion API endpoint. This overrides the config passed to the constructor.
* @param requestOptions Additional options to pass for the HTTP request. This overrides the config passed to the constructor.
* @returns The assistant's response as a [`Message`](./Message.js) instance.
@@ -511,283 +280,47 @@ export class Conversation {
name: string,
result: T,
options?: PromptOptions,
- requestOptions?: RequestOptions
+ requestOptions?: ConversationRequestOptionsModel
) {
- const functionMessage = await this.addFunctionMessage(
- typeof result === "string" ? result : JSON.stringify(result),
+ const transformedResult =
+ await this.pluginService.transformFunctionResult(result);
+ const functionMessage = this.history.addFunctionMessage(
+ typeof transformedResult === "string"
+ ? transformedResult
+ : JSON.stringify(transformedResult),
name
);
try {
- const assistantMessage = await this.getAssistantResponse(
- options,
- requestOptions
- );
+ await this.pluginService.onFunctionPrompt(functionMessage);
+ await this.chatCompletionService.moderateMessage(functionMessage);
+ const assistantMessage =
+ await this.chatCompletionService.getAssistantResponse(
+ options,
+ requestOptions
+ );
return assistantMessage;
} catch (e) {
- this.removeMessage(functionMessage);
+ await this.pluginService.onFunctionPromptError(e);
+ this.history.removeMessage(functionMessage);
throw e;
}
}
/**
- * Returns the sum of the token count of each message in the conversation's current messages. The next time `getChatCompletionResponse()` is called, this is the minimum amount of tokens that will be sent to the OpenAI API (estimated).
- */
- public getSize() {
- return this.messages.reduce((size, message) => size + message.size, 0);
- }
-
- /**
- * Everytime a prompt is sent (using `prompt` or `getChatCompletionResponse`), the size of the conversation (in tokens) is added to an internal cumulative size. This method returns this cumulative size.
- */
- public getCumulativeSize() {
- return this.cumulativeSize;
- }
-
- /**
- * Returns the sum of the estimated cost of each message in the conversation's current messages. The next time `getChatCompletionResponse()` is called, this is the minimum amount of tokens that will be sent to the OpenAI API (estimated).
- */
- public getCost() {
- return this.messages.reduce((cost, message) => cost + message.cost, 0);
- }
-
- /**
- * Returns the total estimated cost that has been incurred by the OpenAI API since the conversation was created.
- */
- public getCumulativeCost() {
- return this.cumulativeCost;
- }
-
- /**
- * Sets the first message sent to the OpenAI API with the role of "system". This gives context the Chat Completion and can be used to customize its behavior.
- *
- * @param context The content of the system message.
- */
- public setContext(context: string) {
- this.addSystemMessage(context);
- }
-
- /**
- * Gets the current config properties as a JavaScript object. (not an instance of the `ConversationConfig` class)
- */
- public getConfig() {
- return {
- ...this.config.chatCompletionConfig,
- ...this.config.config,
- } satisfies ConversationConfigParameters;
- }
-
- /**
- * Assigns a new config to the conversation.
+ * Sends a Create Chat Completion request to the OpenAI API using the current messages stored in the conversation's history.
*
- * @param config The new config to use.
- * @param merge Set to `true` to shallow merge the new config with the existing config instead of replacing it.
- */
- public setConfig(config: ConversationConfigParameters, merge = false) {
- const newConfig = merge ? { ...this.getConfig(), ...config } : config;
- this.config = new ConversationConfig(newConfig);
- if (config.context) {
- this.setContext(config.context);
- }
- if (config.functions) {
- if (!merge) this.functions = [];
- config.functions.forEach((fn) => this.addFunction(fn));
- }
- }
-
- /**
- * Gets the current request options of the conversation.
- */
- public getRequestOptions() {
- return this.requestOptions;
- }
-
- /**
- * Sets new request options to be used as defaults for all HTTP requests made by this conversation.
+ * @remarks
+ * This method is solely provided for client code that wants to trigger a Create Chat Completion request manually.
+ * It is not used internally by the library and does not moderate messages before sending them to the API.
*
- * @param requestOptions The new request options to use.
- * @param merge Set to `true` to shallow merge the new request options with the existing request options instead of replacing them.
+ * @param options Additional options to pass to the Create Chat Completion API endpoint. This overrides the config passed to the constructor.
+ * @param requestOptions Additional options to pass for the HTTP request. This overrides the config passed to the constructor.
+ * @returns A new [`Message`](./Message.js) instance with the role of "assistant" and the content set to the response from the OpenAI API. If the `stream` config option was set to `true`, the content will be progressively updated as the response is streamed from the API. Listen to the returned message's `onUpdate` event to get the updated content.
*/
- public setRequestOptions(requestOptions: RequestOptions, merge = false) {
- this.requestOptions = merge
- ? { ...this.requestOptions, ...requestOptions }
- : requestOptions;
- }
-
- private notifyMessageAdded(message: Message) {
- this.addMessageListeners.forEach((listener) => listener(message));
- }
-
- private notifyMessageRemoved(message: Message) {
- this.removeMessageListeners.forEach((listener) => listener(message));
- }
-
- private async addMessage(message: Message) {
- if (message.isCompletion() || message.isFunction()) {
- message.content = message.content.trim();
- }
-
- if (!message.content && message.role === "user") {
- throw new Error("User message content cannot be empty.");
- }
-
- if (this.config.isModerationEnabled) {
- const flags = await message.moderate(
- this.config.apiKey,
- this.requestOptions
- );
- if (this.config.isModerationStrict && flags.length > 0) {
- throw new ModerationException(flags);
- }
- }
-
- if (message.role === "system") {
- this.config.context = message.content ?? "";
- if (message.content) {
- // Update the system message or add it if it doesn't exist.
- if (this.messages[0]?.role === "system") {
- this.messages[0] = message;
- } else {
- this.messages.unshift(message);
- }
- } else if (this.messages[0]?.role === "system") {
- // Remove the system message if it exists and the new content is empty.
- this.messages.shift();
- }
- } else {
- if (
- this.messages[this.messages.length - 1]?.role === message.role
- ) {
- throw new MessageRoleException();
- }
- this.messages.push(message);
- }
-
- this.notifyMessageAdded(message);
- return message;
- }
-
- private addSystemMessage(message: string) {
- const systemMessage = new Message("system", message, this.config.model);
- return this.addMessage(systemMessage);
- }
-
- private async handleStreamedResponse(
- options: HandleChatCompletionOptions = {},
- requestOptions: RequestOptions = {}
- ) {
- const message = new Message("assistant", "", this.config.model);
- const messages = this.getCreateChatCompletionMessages();
-
- const unsubscribeStreaming = message.onMessageStreamingStop((m) => {
- // FIXME: Find out how the size is calculated for messages with function calls, fix in Message class and remove this condition
- if (!message.isFunctionCall()) {
- this.cumulativeSize += this.getSize() + m.size;
- this.cumulativeCost += this.getCost() + m.cost;
- }
- unsubscribeStreaming();
- });
-
- if (this.config.dry) {
- const response = createDryChatCompletion(
- this.messages[messages.length - 1]?.content ?? "",
- {
- model: this.config.model,
- }
- );
- message.readContentFromStream(response);
- } else {
- createChatCompletion(
- {
- ...this.config.chatCompletionConfig,
- ...options,
- stream: true,
- messages,
- functions: this.getCreateChatCompletionFunctions(),
- },
- {
- ...this.requestOptions,
- ...requestOptions,
- }
- ).then((response) => {
- // Using .then() to get the message out as soon as possible, since the content is known to be empty at first.
- // This gives time for client code to subscribe to the streaming events.
- message.readContentFromStream(response);
- });
- }
-
- return message;
- }
-
- private async handleNonStreamedResponse(
- options: HandleChatCompletionOptions = {},
- requestOptions: RequestOptions = {}
- ) {
- const message = new Message("assistant", "", this.config.model);
- const messages = this.getCreateChatCompletionMessages();
-
- if (this.config.dry) {
- await new Promise((resolve) => setTimeout(resolve, 1000));
- message.content = messages[messages.length - 1]?.content ?? null;
- } else {
- const response = await createChatCompletion(
- {
- ...this.config.chatCompletionConfig,
- ...options,
- stream: false,
- messages,
- functions: this.getCreateChatCompletionFunctions(),
- },
- {
- ...this.requestOptions,
- ...requestOptions,
- }
- );
- const responseMessage = response.choices[0].message;
- message.content = responseMessage.content;
- if (responseMessage.function_call) {
- try {
- message.functionCall = {
- name: responseMessage.function_call.name,
- arguments: JSON.parse(
- responseMessage.function_call.arguments
- ),
- };
- } catch {
- throw new Error(
- "Assistant did not generate valid JSON arguments."
- );
- }
- }
- }
-
- // FIXME: Find out how the size is calculated for messages with function calls, fix in Message class and remove this condition
- if (!message.isFunctionCall()) {
- this.cumulativeSize += this.getSize() + message.size;
- this.cumulativeCost += this.getCost() + message.cost;
- }
-
- return message;
- }
-
- private async getAssistantResponse(
- options?: PromptOptions,
- requestOptions?: RequestOptions
+ public async getChatCompletionResponse(
+ ...args: Parameters
) {
- const completion = await this.getChatCompletionResponse(
- options,
- requestOptions
- );
- const assistantMessage = await this.addMessage(completion);
- return assistantMessage;
- }
-
- private getCreateChatCompletionMessages() {
- return this.messages.map((message) => message.chatCompletionMessage);
- }
-
- private getCreateChatCompletionFunctions() {
- if (this.functions.length === 0) return undefined;
- return this.functions.map((fn) => fn.chatCompletionFunction);
+ return this.chatCompletionService.getChatCompletionResponse(...args);
}
}
diff --git a/packages/lib/src/classes/ConversationCallableFunctions.ts b/packages/lib/src/classes/ConversationCallableFunctions.ts
new file mode 100644
index 0000000..6c470e2
--- /dev/null
+++ b/packages/lib/src/classes/ConversationCallableFunctions.ts
@@ -0,0 +1,173 @@
+import { CallableFunctionModel } from "../schemas/callableFunction.schema.js";
+import {
+ ConversationCallableFunctionsModel,
+ conversationCallableFunctionsSchema,
+} from "../schemas/conversationCallableFunctions.schema.js";
+import {
+ AddCallableFunctionListener,
+ RemoveCallableFunctionListener,
+} from "../utils/types/index.js";
+import { CallableFunction } from "./CallableFunction.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+import { EventManager } from "./EventManager.js";
+
+/**
+ * Holds the callable functions of a `Conversation`.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ConversationCallableFunctions {
+ private functions: CallableFunction[];
+ private addCallableFunctionEvents =
+ new EventManager();
+ private removeCallableFunctionEvents =
+ new EventManager();
+
+ public constructor(
+ private readonly pluginService: ConversationPluginService,
+ options: ConversationCallableFunctionsModel = {}
+ ) {
+ const { functions = [] } = options;
+ this.functions = functions.map((fn) => CallableFunction.fromJSON(fn));
+ }
+
+ /**
+ * Serializes the `ConversationCallableFunctions` to JSON.
+ *
+ * @returns A JSON representation of the `ConversationCallableFunctions` instance.
+ *
+ * @internal
+ * This method is used internally by the library and is not meant to be used by consumers of the library.
+ */
+ public toJSON(): ConversationCallableFunctionsModel {
+ const json: ConversationCallableFunctionsModel = {
+ functions:
+ this.functions.length === 0
+ ? undefined
+ : this.functions.map((fn) => fn.toJSON()),
+ };
+ return conversationCallableFunctionsSchema.parse(
+ this.pluginService.transformConversationCallableFunctionsJson(json)
+ );
+ }
+
+ /**
+ * Removes a callable function from the conversation.
+ *
+ * @param idOrFn Either the ID of the function, the function itself, or the function model.
+ */
+ public removeFunction(
+ idOrFn: string | CallableFunction | CallableFunctionModel
+ ) {
+ const id = typeof idOrFn === "string" ? idOrFn : idOrFn.id;
+ const removedFn = this.functions.find((fn) => fn.id === id);
+ if (!removedFn) return;
+ this.functions = this.functions.filter((fn) => fn.id !== id);
+ this.removeCallableFunctionEvents.emit(removedFn);
+ }
+
+ /**
+ * Removes all functions from the conversation.
+ */
+ public clearFunctions() {
+ this.functions.forEach((fn) => this.removeFunction(fn));
+ }
+
+ /**
+ * Adds a function to the conversation. This function can be "called" by the assistant, generating a function call message.
+ *
+ * @param fn The function to add to the conversation.
+ */
+ public addFunction(fn: CallableFunction | CallableFunctionModel) {
+ const callableFunction =
+ fn instanceof CallableFunction ? fn : CallableFunction.fromJSON(fn);
+ this.functions = this.functions
+ .filter(
+ (f) =>
+ f.name !== callableFunction.name &&
+ f.id !== callableFunction.id
+ )
+ .concat(callableFunction);
+ this.addCallableFunctionEvents.emit(callableFunction);
+ }
+
+ /**
+ * Get the functions in the conversation.
+ *
+ * @returns A **shallow copy** of the functions array.
+ */
+ public getFunctions() {
+ return this.functions.slice(0);
+ }
+
+ /**
+ * Gets the functions in a format the OpenAI Chat Completion API can understand.
+ */
+ public getCreateChatCompletionFunctions() {
+ if (this.functions.length === 0) return undefined;
+ return this.functions.map((fn) => fn.chatCompletionFunction);
+ }
+
+ /**
+ * Adds a listener function that is called whenever a callableFunction is added to the conversation.
+ *
+ * @param listener The function to call when a callableFunction is added to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onCallableFunctionAdded(listener: AddCallableFunctionListener) {
+ return this.addCallableFunctionEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called only once whenever a callableFunction is added to the conversation.
+ *
+ * @param listener The function to call when a callableFunction is added to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onceCallableFunctionAdded(listener: AddCallableFunctionListener) {
+ return this.addCallableFunctionEvents.once(listener);
+ }
+
+ /**
+ * Removes a listener function from the list of listeners that was previously added with `onCallableFunctionAdded`.
+ *
+ * @param listener The function to remove from the list of listeners.
+ */
+ public offCallableFunctionAdded(listener: AddCallableFunctionListener) {
+ return this.addCallableFunctionEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called whenever a callableFunction is removed to the conversation.
+ *
+ * @param listener The function to call when a callableFunction is removed to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onCallableFunctionRemoved(listener: RemoveCallableFunctionListener) {
+ return this.removeCallableFunctionEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called only once whenever a callableFunction is removed to the conversation.
+ *
+ * @param listener The function to call when a callableFunction is removed to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onceCallableFunctionRemoved(
+ listener: RemoveCallableFunctionListener
+ ) {
+ return this.removeCallableFunctionEvents.once(listener);
+ }
+
+ /**
+ * Removes a listener function from the list of listeners that was previously added with `onCallableFunctionRemoved`.
+ *
+ * @param listener The function to remove from the list of listeners.
+ */
+ public offCallableFunctionRemoved(
+ listener: RemoveCallableFunctionListener
+ ) {
+ return this.removeCallableFunctionEvents.removeListener(listener);
+ }
+}
diff --git a/packages/lib/src/classes/ConversationConfig.ts b/packages/lib/src/classes/ConversationConfig.ts
index e61ed40..0d53bcf 100644
--- a/packages/lib/src/classes/ConversationConfig.ts
+++ b/packages/lib/src/classes/ConversationConfig.ts
@@ -10,121 +10,64 @@ import {
conversationConfigSchema,
} from "../schemas/conversationConfig.schema.js";
import {
- ConversationConfigChatCompletionOptions,
- ConversationConfigOptions,
- ConversationConfigParameters,
-} from "../utils/types.js";
-
-type ConversationConfigChatCompletionRequiredOption<
- K extends keyof ConversationConfigChatCompletionOptions
-> = Exclude;
-
-type ConversationConfigRequiredOption<
- K extends keyof ConversationConfigOptions
-> = Exclude;
+ ChatCompletionConfigProperties,
+ ConfigProperties,
+ ConversationConfigProperties,
+} from "../utils/types/index.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
/**
* The configuration for a conversation. Contains library specific options and OpenAI's Chat Completion default options.
*
- * @remarks
- * Since this class is only instanciated by the Conversation class internally and is not required by any of its public methods, you should not need to use this class directly.
- *
* @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
*/
export class ConversationConfig {
- public model: ConversationConfigChatCompletionRequiredOption<"model">;
- public stream: ConversationConfigChatCompletionRequiredOption<"stream">;
- public frequencyPenalty: ConversationConfigChatCompletionOptions["frequency_penalty"];
- public presencePenalty: ConversationConfigChatCompletionOptions["presence_penalty"];
- public maxTokens: ConversationConfigChatCompletionOptions["max_tokens"];
- public logitBias: ConversationConfigChatCompletionOptions["logit_bias"];
- public temperature: ConversationConfigChatCompletionOptions["temperature"];
- public topP: ConversationConfigChatCompletionOptions["top_p"];
- public user: ConversationConfigChatCompletionOptions["user"];
- public functionCall: ConversationConfigChatCompletionOptions["function_call"];
- private _stop: ConversationConfigChatCompletionOptions["stop"];
- private _apiKey!: ConversationConfigChatCompletionRequiredOption<"apiKey">;
-
- public disableModeration: ConversationConfigRequiredOption<"disableModeration">;
- private _context!: ConversationConfigRequiredOption<"context">;
- private _dry!: ConversationConfigRequiredOption<"dry">;
-
- constructor({
- context = DEFAULT_CONTEXT,
- dry = DEFAULT_DRY,
- disableModeration = DEFAULT_DISABLEMODERATION,
- ...chatCompletionConfig
- }: ConversationConfigParameters) {
- const {
- apiKey = "",
- model = DEFAULT_MODEL,
- stream = DEFAULT_STREAM,
- frequency_penalty,
- presence_penalty,
- max_tokens,
- logit_bias,
- stop,
- temperature,
- top_p,
- user,
- function_call,
- } = chatCompletionConfig;
-
- this.apiKey = apiKey;
- this.dry = dry;
- this.model = model;
- this.context = context.trim();
- this.disableModeration = disableModeration;
- this.stream = stream;
-
- this.frequencyPenalty = frequency_penalty;
- this.presencePenalty = presence_penalty;
- this.maxTokens = max_tokens;
- this.logitBias = logit_bias;
- this.stop = stop;
- this.temperature = temperature;
- this.topP = top_p;
- this.user = user;
- this.functionCall = function_call;
- }
-
- /**
- * Creates a new `ConversationConfig` instance from a serialized config.
- *
- * @param json The JSON object of a ConversationConfig instance.
- * @returns A new `ConversationConfig` instance
- */
- public static fromJSON(json: ConversationConfigModel) {
- return new ConversationConfig(conversationConfigSchema.parse(json));
+ // OpenAI-specific options
+ public model!: ChatCompletionConfigProperties["model"];
+ public stream!: ChatCompletionConfigProperties["stream"];
+ public frequencyPenalty: ChatCompletionConfigProperties["frequency_penalty"];
+ public presencePenalty: ChatCompletionConfigProperties["presence_penalty"];
+ public maxTokens: ChatCompletionConfigProperties["max_tokens"];
+ public logitBias: ChatCompletionConfigProperties["logit_bias"];
+ public temperature: ChatCompletionConfigProperties["temperature"];
+ public topP: ChatCompletionConfigProperties["top_p"];
+ public user: ChatCompletionConfigProperties["user"];
+ public functionCall: ChatCompletionConfigProperties["function_call"];
+ private _stop: ChatCompletionConfigProperties["stop"];
+ private _apiKey!: ChatCompletionConfigProperties["apiKey"];
+
+ // Library-specific options
+ public disableModeration!: ConversationConfigProperties["disableModeration"];
+ private _context!: ConversationConfigProperties["context"];
+ private _dry!: ConversationConfigProperties["dry"];
+
+ constructor(
+ private readonly pluginService: ConversationPluginService,
+ options: ConversationConfigModel = {}
+ ) {
+ this.setConfig(options);
}
/**
* Serializes the ConversationConfig to JSON.
*
* @returns The `ConversationConfig` as a JSON object.
+ *
+ * @internal
+ * This method is used internally by the library and is not meant to be used by consumers of the library.
*/
public toJSON(): ConversationConfigModel {
- const json: ConversationConfigModel = {
- dry: this.dry,
- apiKey: this.apiKey,
- model: this.model,
- stream: this.stream,
- disableModeration: this.disableModeration,
- context: this.context,
- frequency_penalty: this.frequencyPenalty,
- presence_penalty: this.presencePenalty,
- max_tokens: this.maxTokens,
- logit_bias: this.logitBias,
- temperature: this.temperature,
- top_p: this.topP,
- user: this.user,
- stop: this.stop,
- function_call: this.functionCall,
- };
- return conversationConfigSchema.parse(json);
+ const json: ConversationConfigModel = this.getConfig();
+ return conversationConfigSchema.parse(
+ this.pluginService.transformConversationConfigJson(json)
+ );
}
- public get config(): Required {
+ /**
+ * Returns the **library-specific** options of the config.
+ */
+ public getConversationConfig(): ConversationConfigProperties {
return {
context: this.context,
dry: this.dry,
@@ -132,13 +75,10 @@ export class ConversationConfig {
};
}
- public get chatCompletionConfig(): Required<
- Pick<
- ConversationConfigChatCompletionOptions,
- "apiKey" | "model" | "stream"
- >
- > &
- ConversationConfigChatCompletionOptions {
+ /**
+ * Returns the **OpenAI-specific** options of the config.
+ */
+ public getChatCompletionConfig(): ChatCompletionConfigProperties {
return {
apiKey: this.apiKey,
model: this.model,
@@ -155,6 +95,63 @@ export class ConversationConfig {
};
}
+ /**
+ * Returns both the **library-specific** and **OpenAI-specific** options of the config.
+ */
+ public getConfig(): ConfigProperties {
+ return {
+ ...this.getChatCompletionConfig(),
+ ...this.getConversationConfig(),
+ };
+ }
+
+ /**
+ * Assigns a new config to the conversation.
+ *
+ * @param config The new config to use.
+ * @param merge Set to `true` to shallow merge the new config with the existing config instead of replacing it.
+ */
+ public setConfig(config: Partial, merge = false) {
+ const {
+ context = DEFAULT_CONTEXT,
+ dry = DEFAULT_DRY,
+ disableModeration = DEFAULT_DISABLEMODERATION,
+ ...chatCompletionConfig
+ } = merge ? { ...this.getConfig(), ...config } : config;
+
+ const {
+ apiKey = "",
+ model = DEFAULT_MODEL,
+ stream = DEFAULT_STREAM,
+ frequency_penalty,
+ presence_penalty,
+ max_tokens,
+ logit_bias,
+ stop,
+ temperature,
+ top_p,
+ user,
+ function_call,
+ } = chatCompletionConfig;
+
+ this.apiKey = apiKey;
+ this.dry = dry;
+ this.model = model;
+ this.context = context.trim();
+ this.disableModeration = disableModeration;
+ this.stream = stream;
+
+ this.frequencyPenalty = frequency_penalty;
+ this.presencePenalty = presence_penalty;
+ this.maxTokens = max_tokens;
+ this.logitBias = logit_bias;
+ this.stop = stop;
+ this.temperature = temperature;
+ this.topP = top_p;
+ this.user = user;
+ this.functionCall = function_call;
+ }
+
public get apiKey() {
return this._apiKey;
}
diff --git a/packages/lib/src/classes/ConversationHistory.ts b/packages/lib/src/classes/ConversationHistory.ts
new file mode 100644
index 0000000..9aece6b
--- /dev/null
+++ b/packages/lib/src/classes/ConversationHistory.ts
@@ -0,0 +1,334 @@
+import { Message } from "./Message.js";
+import { ConversationConfig } from "./ConversationConfig.js";
+import {
+ ConversationHistoryModel,
+ conversationHistorySchema,
+} from "../schemas/conversationHistory.schema.js";
+import {
+ AddMessageListener,
+ RemoveMessageListener,
+} from "../utils/types/index.js";
+import { MessageRoleException } from "../exceptions/MessageRoleException.js";
+import { EventManager } from "./EventManager.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+
+/**
+ * The request options for a conversation.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ConversationHistory {
+ private messages: Message[] = [];
+ private addMessageEvents = new EventManager();
+ private removeMessageEvents = new EventManager();
+
+ public constructor(
+ private readonly pluginService: ConversationPluginService,
+ private readonly config: ConversationConfig,
+ options: ConversationHistoryModel = {}
+ ) {
+ const { messages = [] } = options;
+
+ if (config.context) {
+ this.setContext(config.context);
+ }
+
+ for (const message of messages) {
+ switch (message.role) {
+ case "user":
+ if (message.content === null)
+ throw new Error("User message content cannot be null.");
+ this.addUserMessage(message.content);
+ break;
+ case "assistant":
+ if (message.content === null) {
+ if (!message.function_call)
+ throw new Error("Function call must be provided.");
+ this.addFunctionCallMessage(message.function_call);
+ } else {
+ this.addAssistantMessage(message.content);
+ }
+ break;
+ case "system":
+ if (message.content === null) continue;
+ this.setContext(message.content);
+ break;
+ case "function":
+ if (!message.name)
+ throw new Error("Function name must be specified.");
+ if (message.content === null)
+ throw new Error(
+ "Function message content cannot be null."
+ );
+ this.addFunctionMessage(message.content, message.name);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Serializes the `ConversationHistory` to JSON.
+ *
+ * @returns A JSON representation of the `ConversationHistory` instance.
+ *
+ * @internal
+ * This method is used internally by the library and is not meant to be used by consumers of the library.
+ */
+ public toJSON(): ConversationHistoryModel {
+ // Config already has the context, so we don't need to add it here.
+ const messages = this.getMessages();
+
+ const json: ConversationHistoryModel = {
+ messages:
+ messages.length === 0
+ ? undefined
+ : messages.map((message) => message.toJSON()),
+ };
+ return conversationHistorySchema.parse(
+ this.pluginService.transformConversationHistoryJson(json)
+ );
+ }
+
+ /**
+ * Adds a message to the conversation.
+ *
+ * @remarks
+ * This method is mostly meant to be internal, but exposed for convenience.
+ * You should use the `addAssistantMessage`, `addUserMessage`, `addFunctionCallMessage`, or `addFunctionMessage` methods when possible.
+ *
+ * @param message The message instance to add to the conversation.
+ * @returns The added message.
+ */
+ public addMessage(message: Message) {
+ if (message.isCompletion() || message.isFunction()) {
+ message.content = message.content.trim();
+ }
+
+ if (!message.content && message.role === "user") {
+ throw new Error("User message content cannot be empty.");
+ }
+
+ if (message.role === "system") {
+ if (message.content) {
+ // Update the system message or add it if it doesn't exist.
+ if (this.messages[0]?.role === "system") {
+ this.messages[0] = message;
+ } else {
+ this.messages.unshift(message);
+ }
+ } else if (this.messages[0]?.role === "system") {
+ // Remove the system message if it exists and the new content is empty.
+ this.messages.shift();
+ }
+ } else {
+ if (
+ this.messages[this.messages.length - 1]?.role === message.role
+ ) {
+ throw new MessageRoleException();
+ }
+ this.messages.push(message);
+ }
+
+ this.addMessageEvents.emit(message);
+ return message;
+ }
+
+ /**
+ * Adds a message with the role of `"assistant"` to the conversation.
+ *
+ * @param message The content of the message.
+ * @returns The [Message](./utils/types.ts) object that was added to the conversation
+ */
+ public addAssistantMessage(message: string) {
+ const assistantMessage = new Message(
+ "assistant",
+ message,
+ this.config.model
+ );
+ if (!assistantMessage.isCompletion()) {
+ throw new Error("Not a completion message.");
+ }
+ return this.addMessage(assistantMessage);
+ }
+
+ /**
+ * Adds a message with the role of "user" to the conversation.
+ *
+ * @param message The content of the message.
+ * @returns The [Message](./utils/types.ts) object that was added to the conversation
+ */
+ public addUserMessage(message: string) {
+ const userMessage = new Message("user", message, this.config.model);
+ if (!userMessage.isCompletion()) {
+ throw new Error("Not a completion message.");
+ }
+ return this.addMessage(userMessage);
+ }
+
+ /**
+ * Adds a message with the role of "assistant" to the conversation with the function call generated by the assistant.
+ *
+ * @param functionCall The name and arguments of the function to call, generated by the assistant.
+ * @returns The [Message](./utils/types.ts) object that was added to the conversation
+ */
+ public addFunctionCallMessage(functionCall: {
+ name: string;
+ arguments: Record;
+ }) {
+ const functionCallMessage = new Message(
+ "assistant",
+ null,
+ this.config.model
+ );
+ functionCallMessage.functionCall = functionCall;
+ if (!functionCallMessage.isFunctionCall()) {
+ throw new Error("Not a function call message.");
+ }
+ return this.addMessage(functionCallMessage);
+ }
+
+ /**
+ * Adds a message with the role of "function" to the conversation with the content being the return value of the function call, called by your own code.
+ *
+ * @param message The return value of the function call, stringified if needed.
+ * @param name The name of the function that was called.
+ * @returns The [Message](./utils/types.ts) object that was added to the conversation
+ */
+ public addFunctionMessage(message: string, name: string) {
+ const functionMessage = new Message(
+ "function",
+ message,
+ this.config.model
+ );
+ functionMessage.name = name;
+ if (!functionMessage.isFunction()) {
+ throw new Error("Not a function message.");
+ }
+ return this.addMessage(functionMessage);
+ }
+
+ /**
+ * Sets the first message sent to the OpenAI API with the role of "system". This gives context the Chat Completion and can be used to customize its behavior.
+ *
+ * @param context The content of the system message.
+ */
+ public setContext(context: string) {
+ this.addSystemMessage(context);
+ }
+
+ /**
+ * Returns the context message content, if it is set.
+ */
+ public getContext() {
+ const firstMessage = this.messages[0];
+ if (!firstMessage) return null;
+ if (firstMessage.role !== "system") return null;
+ return firstMessage.content;
+ }
+
+ /**
+ * Get the messages in the conversation.
+ *
+ * @param includeContext Whether to include the context message in the returned array.
+ * @returns A **shallow copy** of the messages array.
+ */
+ public getMessages(includeContext = false) {
+ if (!this.getContext()) return this.messages.slice(0);
+ return includeContext ? this.messages.slice(0) : this.messages.slice(1);
+ }
+
+ /**
+ * Clears all messages in the conversation except the context message, if it is set.
+ */
+ public clearMessages() {
+ const context = this.getContext();
+ this.messages.forEach((message) => this.removeMessage(message));
+ if (context) {
+ this.addSystemMessage(context);
+ }
+ }
+
+ /**
+ * Removes a message from the conversation's history.
+ *
+ * @param message Either the ID of the message to remove, or the message object itself (where the ID will be extracted from).
+ */
+ public removeMessage(message: string | Message) {
+ const id = typeof message === "string" ? message : message.id;
+ const removedMessage = this.messages.find((m) => m.id === id);
+ if (!removedMessage) return;
+ this.messages = this.messages.filter((m) => m.id !== id);
+ this.removeMessageEvents.emit(removedMessage);
+ }
+
+ /**
+ * Returns the messages in a format OpenAI's Chat Completion API can understand.
+ */
+ public getCreateChatCompletionMessages() {
+ return this.messages.map((message) => message.chatCompletionMessage);
+ }
+
+ private addSystemMessage(message: string) {
+ const systemMessage = new Message("system", message, this.config.model);
+ return this.addMessage(systemMessage);
+ }
+
+ /**
+ * Adds a listener function that is called whenever a message is added to the conversation.
+ *
+ * @param listener The function to call when a message is added to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onMessageAdded(listener: AddMessageListener) {
+ return this.addMessageEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called only once whenever a message is added to the conversation.
+ *
+ * @param listener The function to call when a message is added to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onceMessageAdded(listener: AddMessageListener) {
+ return this.addMessageEvents.once(listener);
+ }
+
+ /**
+ * Removes a listener function from the list of listeners that was previously added with `onMessageAdded`.
+ *
+ * @param listener The function to remove from the list of listeners.
+ */
+ public offMessageAdded(listener: AddMessageListener) {
+ return this.addMessageEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called whenever a message is removed to the conversation.
+ *
+ * @param listener The function to call when a message is removed to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onMessageRemoved(listener: RemoveMessageListener) {
+ return this.removeMessageEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener function that is called only once whenever a message is removed to the conversation.
+ *
+ * @param listener The function to call when a message is removed to the conversation.
+ * @returns A function that removes the listener from the list of listeners.
+ */
+ public onceMessageRemoved(listener: RemoveMessageListener) {
+ return this.removeMessageEvents.once(listener);
+ }
+
+ /**
+ * Removes a listener function from the list of listeners that was previously added with `onMessageRemoved`.
+ *
+ * @param listener The function to remove from the list of listeners.
+ */
+ public offMessageRemoved(listener: RemoveMessageListener) {
+ return this.removeMessageEvents.removeListener(listener);
+ }
+}
diff --git a/packages/lib/src/classes/ConversationPluginService.ts b/packages/lib/src/classes/ConversationPluginService.ts
new file mode 100644
index 0000000..fa100c2
--- /dev/null
+++ b/packages/lib/src/classes/ConversationPluginService.ts
@@ -0,0 +1,171 @@
+import { ConversationModel } from "../schemas/conversation.schema.js";
+import { ConversationCallableFunctionsModel } from "../schemas/conversationCallableFunctions.schema.js";
+import { ConversationConfigModel } from "../schemas/conversationConfig.schema.js";
+import { ConversationHistoryModel } from "../schemas/conversationHistory.schema.js";
+import { ConversationRequestOptionsModel } from "../schemas/conversationRequestOptions.schema.js";
+import {
+ ConversationPluginDefinition,
+ ConversationPlugin,
+ ConversationPluginProperties,
+} from "../utils/types/index.js";
+import { Message } from "./Message.js";
+
+/**
+ * Manages the plugins of a conversation and acts as a bridge between the plugins and the conversation.
+ *
+ * @remarks
+ * Most of these methods are not documented (with TypeDocs).
+ * In most cases, this is because they just call the equivalent method on each plugin.
+ * Refer to the documentation of the `ConversationPluginCreatorDefinitionBase` interface for their documentation.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ConversationPluginService<
+ TPluginCreators extends ConversationPlugin[] = ConversationPlugin[],
+> {
+ private plugins: ConversationPluginDefinition[] = [];
+
+ constructor(private readonly pluginCreators: TPluginCreators) {}
+
+ public getPlugins() {
+ return this.plugins.slice();
+ }
+
+ public onInit(
+ properties: ConversationPluginProperties,
+ pluginsData?: ConversationModel["pluginsData"]
+ ) {
+ for (const pluginCreator of this.pluginCreators) {
+ if (this.plugins.some((p) => p.name === pluginCreator.name)) {
+ console.warn(
+ `[gpt-turbo] Skipping duplicate plugin "${pluginCreator.name}"`
+ );
+ continue;
+ }
+ this.plugins.push({
+ ...pluginCreator.creator(
+ properties,
+ pluginsData?.[pluginCreator.name]
+ ),
+ name: pluginCreator.name,
+ });
+ }
+ this.onPostInit();
+ }
+
+ public onPostInit() {
+ this.plugins.forEach((p) => p.onPostInit?.());
+ }
+
+ public transformConversationJson(json: ConversationModel) {
+ return this.plugins.reduce(
+ (json, p) => p.transformConversationJson?.(json) ?? json,
+ json
+ );
+ }
+
+ public transformConversationCallableFunctionsJson(
+ json: ConversationCallableFunctionsModel
+ ) {
+ return this.plugins.reduce(
+ (json, p) =>
+ p.transformConversationCallableFunctionsJson?.(json) ?? json,
+ json
+ );
+ }
+
+ public transformConversationConfigJson(json: ConversationConfigModel) {
+ return this.plugins.reduce(
+ (json, p) => p.transformConversationConfigJson?.(json) ?? json,
+ json
+ );
+ }
+
+ public transformConversationHistoryJson(json: ConversationHistoryModel) {
+ return this.plugins.reduce(
+ (json, p) => p.transformConversationHistoryJson?.(json) ?? json,
+ json
+ );
+ }
+
+ public transformConversationRequestOptionsJson(
+ json: ConversationRequestOptionsModel
+ ) {
+ return this.plugins.reduce(
+ (json, p) =>
+ p.transformConversationRequestOptionsJson?.(json) ?? json,
+ json
+ );
+ }
+
+ public async onUserPrompt(message: Message) {
+ for (const plugin of this.plugins) {
+ await plugin.onUserPrompt?.(message);
+ }
+ }
+
+ public async onUserPromptError(error: unknown) {
+ for (const plugin of this.plugins) {
+ try {
+ await plugin.onUserPromptError?.(error);
+ } catch (e) {
+ console.error(
+ `[gpt-turbo] Plugin "${plugin.name}" errored during "onUserPromptError":`
+ );
+ console.error(e);
+ }
+ }
+ }
+
+ public async onChatCompletion(message: Message) {
+ for (const plugin of this.plugins) {
+ await plugin.onChatCompletion?.(message);
+ }
+ }
+
+ public async transformFunctionResult(result: any) {
+ return this.plugins.reduce(
+ async (result, p) =>
+ p.transformFunctionResult?.(await result) ?? result,
+ Promise.resolve(result)
+ );
+ }
+
+ public async onFunctionPrompt(message: Message) {
+ for (const plugin of this.plugins) {
+ await plugin.onFunctionPrompt?.(message);
+ }
+ }
+
+ public async onFunctionPromptError(error: unknown) {
+ for (const plugin of this.plugins) {
+ try {
+ await plugin.onFunctionPromptError?.(error);
+ } catch (e) {
+ console.error(
+ `[gpt-turbo] Plugin "${plugin.name}" errored during "onFunctionPromptError":`
+ );
+ console.error(e);
+ }
+ }
+ }
+
+ public getPluginsData() {
+ return this.plugins.reduce(
+ (data, p) => {
+ if (p.getPluginData !== undefined) {
+ data[p.name] = p.getPluginData();
+ }
+ return data;
+ },
+ {} as Record
+ );
+ }
+
+ public async onModeration(message: Message) {
+ for (const plugin of this.plugins) {
+ await plugin.onModeration?.(message);
+ }
+ }
+}
diff --git a/packages/lib/src/classes/ConversationPlugins.ts b/packages/lib/src/classes/ConversationPlugins.ts
new file mode 100644
index 0000000..da1edf0
--- /dev/null
+++ b/packages/lib/src/classes/ConversationPlugins.ts
@@ -0,0 +1,102 @@
+import {
+ ConversationPluginDefinition,
+ ConversationPlugin,
+ PluginDataFromName,
+ PluginOutputFromName,
+ ConversationPluginDefinitionFromPlugin,
+} from "../utils/types/index.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+
+/**
+ * Provides methods to interact with the plugins of a conversation.
+ * It is a bridge between client code and the `ConversationPluginService`, which is internal to the library.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ConversationPlugins<
+ TPluginCreators extends ConversationPlugin[] = ConversationPlugin[],
+ TPlugin extends
+ ConversationPluginDefinition = ConversationPluginDefinitionFromPlugin<
+ TPluginCreators[number]
+ >,
+ TPluginName extends string =
+ | TPluginCreators[number]["name"]
+ | (string & {}),
+> {
+ constructor(
+ private readonly pluginService: ConversationPluginService
+ ) {}
+
+ /**
+ * Gets a `PluginDefinition` by its name.
+ *
+ * @param name The name of the plugin to get. (case-sensitive)
+ * @returns The plugin with the specified name.
+ * @throws If no plugin with the specified name is found.
+ */
+ public getPlugin(name: N) {
+ const plugin = this.plugins.find((p) => p.name === name);
+ if (!plugin) {
+ throw new Error(`Plugin "${name}" not found.`);
+ }
+ return plugin as ConversationPluginDefinition<
+ N,
+ PluginOutputFromName,
+ PluginDataFromName
+ >;
+ }
+
+ /**
+ * Like `getPlugin`, but returns `undefined` instead of throwing an error if no plugin with the specified name is found.
+ *
+ * @param name The name of the plugin to get. (case-sensitive)
+ * @returns The plugin with the specified name, or `undefined` if no plugin with the specified name is found.
+ */
+ public safeGetPlugin(name: N) {
+ try {
+ return this.getPlugin(name);
+ } catch {
+ return undefined;
+ }
+ }
+
+ /**
+ * Gets a `PluginDefinition`'s output by its name.
+ *
+ * @param name The name of the plugin to get. (case-sensitive)
+ * @returns The output of the plugin with the specified name.
+ * @throws If no plugin with the specified name is found.
+ */
+ public getPluginOutput(name: N) {
+ const plugin = this.getPlugin(name);
+ return plugin.out as PluginOutputFromName;
+ }
+
+ /**
+ * Like `getPluginOutput`, but returns `undefined` instead of throwing an error if no plugin with the specified name is found.
+ *
+ * @param name The name of the plugin to get. (case-sensitive)
+ * @returns The output of the plugin with the specified name, or `undefined` if no plugin with the specified name is found.
+ */
+ public safeGetPluginOutput(name: N) {
+ return this.safeGetPlugin(name)?.out as
+ | PluginOutputFromName
+ | undefined;
+ }
+
+ /**
+ * Gets all the **initialized** plugin definitions of the conversation.
+ * These are different from the plugins passed to the `Conversation` constructor.
+ *
+ * @remarks
+ * This is a shallow copy of the plugins array, so modifying the array will not modify the plugins of the conversation.
+ */
+ public getPlugins() {
+ return this.plugins;
+ }
+
+ private get plugins() {
+ return this.pluginService.getPlugins();
+ }
+}
diff --git a/packages/lib/src/classes/ConversationRequestOptions.ts b/packages/lib/src/classes/ConversationRequestOptions.ts
new file mode 100644
index 0000000..f0f4f29
--- /dev/null
+++ b/packages/lib/src/classes/ConversationRequestOptions.ts
@@ -0,0 +1,62 @@
+import {
+ ConversationRequestOptionsModel,
+ conversationRequestOptionsSchema,
+} from "../schemas/conversationRequestOptions.schema.js";
+import { ConversationPluginService } from "./ConversationPluginService.js";
+
+/**
+ * Holds the callable functions of a `Conversation`.
+ *
+ * @internal
+ * This class is used internally by the library and is not meant to be **instantiated** by consumers of the library.
+ */
+export class ConversationRequestOptions {
+ headers: ConversationRequestOptionsModel["headers"];
+ proxy: ConversationRequestOptionsModel["proxy"];
+
+ constructor(
+ private readonly pluginService: ConversationPluginService,
+ options: ConversationRequestOptionsModel = {}
+ ) {
+ this.setRequestOptions(options);
+ }
+
+ /**
+ * Serializes the `ConversationRequestOptions` to JSON.
+ *
+ * @returns A JSON representation of the `ConversationRequestOptions` instance.
+ */
+ public toJSON(): ConversationRequestOptionsModel {
+ const json: ConversationRequestOptionsModel = this.getRequestOptions();
+ return conversationRequestOptionsSchema.parse(
+ this.pluginService.transformConversationRequestOptionsJson(json)
+ );
+ }
+
+ /**
+ * Returns the current request options.
+ */
+ public getRequestOptions() {
+ return {
+ headers: this.headers,
+ proxy: this.proxy,
+ };
+ }
+
+ /**
+ * Sets new request options to be used as defaults for all HTTP requests made by this conversation.
+ *
+ * @param requestOptions The new request options to use.
+ * @param merge Set to `true` to shallow merge the new request options with the existing request options instead of replacing them.
+ */
+ public setRequestOptions(
+ requestOptions: ConversationRequestOptionsModel,
+ merge = false
+ ) {
+ const options = merge
+ ? { ...this.headers, ...this.proxy, ...requestOptions }
+ : requestOptions;
+ this.headers = options.headers;
+ this.proxy = options.proxy;
+ }
+}
diff --git a/packages/lib/src/classes/EventManager.ts b/packages/lib/src/classes/EventManager.ts
new file mode 100644
index 0000000..3bc244d
--- /dev/null
+++ b/packages/lib/src/classes/EventManager.ts
@@ -0,0 +1,31 @@
+export class EventManager<
+ T extends (...args: any[]) => void = (...args: any) => void,
+> {
+ private listeners: Array = [];
+
+ public addListener(listener: T) {
+ this.listeners.push(listener);
+ return () => this.removeListener(listener);
+ }
+
+ public once(listener: T) {
+ const onceListener = ((...args: Parameters) => {
+ listener(...args);
+ this.removeListener(onceListener);
+ }) as T;
+ this.addListener(onceListener);
+ return () => this.removeListener(onceListener);
+ }
+
+ public removeListener(listener: T) {
+ this.listeners = this.listeners.filter((l) => l !== listener);
+ }
+
+ public emit(...args: Parameters) {
+ this.listeners.forEach((l) => l(...args));
+ }
+
+ public clear() {
+ this.listeners = [];
+ }
+}
diff --git a/packages/lib/src/classes/Message.ts b/packages/lib/src/classes/Message.ts
index 4120e0f..b5dde7b 100644
--- a/packages/lib/src/classes/Message.ts
+++ b/packages/lib/src/classes/Message.ts
@@ -1,8 +1,6 @@
import { v4 as uuid } from "uuid";
-import { getMessageCost, getMessageSize } from "../index.js";
import {
ChatCompletionRequestMessageRoleEnum,
- RequestOptions,
CreateChatCompletionStreamResponse,
MessageStreamingListener,
MessageStreamingStartListener,
@@ -14,9 +12,12 @@ import {
CreateChatCompletionMessage,
CreateChatCompletionFunctionCallMessage,
CreateChatCompletionFunctionMessage,
-} from "../utils/types.js";
-import createModeration from "../utils/createModeration.js";
+ MessageContentStreamListener,
+} from "../utils/types/index.js";
import { MessageModel, messageSchema } from "../schemas/message.schema.js";
+import { ConversationRequestOptionsModel } from "../schemas/conversationRequestOptions.schema.js";
+import createModeration from "../utils/createModeration.js";
+import { EventManager } from "./EventManager.js";
/**
* A message in a Conversation.
@@ -38,19 +39,24 @@ export class Message {
}
| undefined;
private _flags: string[] | null = null;
- private _size: number | null = null;
- private _cost: number | null = null;
private _isStreaming = false;
- private messageUpdateListeners: MessageUpdateListener[] = [];
- private messageStreamingListeners: MessageStreamingListener[] = [];
+ private messageUpdateEvents = new EventManager();
+ private messageStreamingEvents =
+ new EventManager();
+ private messageStreamingStartEvents =
+ new EventManager();
+ private messageStreamingStopEvents =
+ new EventManager();
+ private messageContentStreamEvents =
+ new EventManager();
/**
* Creates a new Message instance.
*
* @param role The role of who this message is from. Either "user", "assistant" or "system".
* @param content The content of the message.
- * @param model The model used for processing this message. This is only used to calculate the cost of the message. If you don't specify a model, the `cost` will always be `0`.
+ * @param model The model used for processing this message.
*/
constructor(
role: ChatCompletionRequestMessageRoleEnum = "user",
@@ -98,84 +104,6 @@ export class Message {
return messageSchema.parse(json);
}
- /**
- * Removes a message update listener, previously set with `onMessageUpdate`.
- *
- * @param listener The previously added listener
- */
- public offMessageUpdate(listener: MessageUpdateListener) {
- this.messageUpdateListeners = this.messageUpdateListeners.filter(
- (l) => l !== listener
- );
- }
-
- /**
- * Add a listener for message content changes.
- *
- * @param listener The listener to trigger when `content` changes
- * @returns An unsubscribe function for this `listener`
- */
- public onMessageUpdate(listener: MessageUpdateListener) {
- this.messageUpdateListeners.push(listener);
- return () => this.offMessageUpdate(listener);
- }
-
- /**
- * Removes a message streaming listener, previously set with `onMessageStreamingUpdate`.
- *
- * @param listener The previously added listener
- */
- public offMessageStreaming(listener: MessageStreamingListener) {
- this.messageStreamingListeners = this.messageStreamingListeners.filter(
- (l) => l !== listener
- );
- }
-
- /**
- * Adds a listener for message streaming state changes.
- *
- * @param listener The listener to trigger when `isStreaming` changes
- * @returns An unsubscribe function for this `listener`
- */
- public onMessageStreamingUpdate(listener: MessageStreamingListener) {
- this.messageStreamingListeners.push(listener);
- return () => this.offMessageStreaming(listener);
- }
-
- /**
- * Adds a listener for message streaming start.
- *
- * **Note:** Internally, this creates a new function wrapping your passed `listener` and passes it to `onMessageStreamingUpdate`.
- * For this reason, you cannot remove a listener using `offMessageStreaming(listener)`.
- * Instead, use the returned function to unsubscribe the listener properly.
- *
- * @param listener The listener to trigger when `isStreaming` is set to `true`
- * @returns An unsubscribe function for this `listener`
- */
- public onMessageStreamingStart(listener: MessageStreamingStartListener) {
- const startListener: MessageStreamingListener = (
- isStreaming,
- message
- ) => isStreaming && listener(message);
- return this.onMessageStreamingUpdate(startListener);
- }
-
- /**
- * Adds a listener for message streaming stop.
- *
- * **Note: ** Internally, this creates a new function wrapping your passed `listener` and passes it to `onMessageStreamingUpdate`.
- * For this reason, you cannot remove a listener using `offMessageStreaming(listener)`.
- * Instead, use the returned function to unsubscribe the listener properly.
- *
- * @param listener The listener to trigger when `isStreaming` is set to `false`
- * @returns An unsubscribe function for this `listener`
- */
- public onMessageStreamingStop(listener: MessageStreamingStopListener) {
- const stopListener: MessageStreamingListener = (isStreaming, message) =>
- !isStreaming && listener(message);
- return this.onMessageStreamingUpdate(stopListener);
- }
-
/**
* Call the OpenAI moderation API to check if the message is flagged. Only called once for the same content.
*
@@ -185,13 +113,16 @@ export class Message {
*/
public async moderate(
apiKey: string,
- requestOptions: RequestOptions = {}
+ requestOptions: ConversationRequestOptionsModel = {}
): Promise {
const flags = this.flags;
if (flags) {
return flags;
}
- if (!this.content) return [];
+ if (!this.content) {
+ this.flags = [];
+ return this.flags;
+ }
const response = await createModeration(
{
@@ -302,19 +233,6 @@ export class Message {
);
}
- private notifyMessageUpdate() {
- const content = this.content;
- this.messageUpdateListeners.forEach((listener) =>
- listener(content, this)
- );
- }
-
- private notifyMessageStreaming() {
- this.messageStreamingListeners.forEach((listener) => {
- listener(this.isStreaming, this);
- });
- }
-
/** The role of who this message is from. */
get role() {
return this._role;
@@ -341,10 +259,15 @@ export class Message {
set content(content) {
this._content = content;
this.flags = null;
- this.size = null;
- this.cost = null;
- this.notifyMessageUpdate();
+ this.messageUpdateEvents.emit(content, this);
+ if (this.isStreaming) {
+ this.messageContentStreamEvents.emit(
+ content,
+ this.isStreaming,
+ this
+ );
+ }
}
get name() {
@@ -362,8 +285,6 @@ export class Message {
set functionCall(functionCall) {
this._functionCall = functionCall;
this.flags = null;
- this.size = null;
- this.cost = null;
this.content = null; // also call notifyMessageUpdate() to notify listeners
}
@@ -381,51 +302,24 @@ export class Message {
return (this.flags?.length ?? 0) > 0;
}
- /** The size of the message's content, in tokens. */
- get size(): number {
- if (this._size) {
- return this._size;
- }
- // FIXME: Find out how the size is calculated for messages with function calls
- if (this._content === null) {
- return 0;
- }
- const s = getMessageSize(this._content);
- this.size = s;
- return this._size as typeof s;
- }
-
- private set size(size: number | null) {
- this._size = size;
- }
-
- /** The estimated cost of the message's content. */
- get cost(): number {
- if (this._cost) {
- return this._cost;
- }
- const c = getMessageCost(
- this.size,
- this.model,
- this.role === "assistant" ? "completion" : "prompt"
- );
- this.cost = c;
- return this._cost as typeof c;
- }
-
- private set cost(cost: number | null) {
- this._cost = cost;
- }
-
/** Whether the message is currently being streamed. */
get isStreaming() {
return this._isStreaming;
}
private set isStreaming(isStreaming) {
+ if (this._isStreaming === isStreaming) return;
this._isStreaming = isStreaming;
- this.notifyMessageStreaming();
+ this.messageStreamingEvents.emit(isStreaming, this);
+ isStreaming
+ ? this.messageStreamingStartEvents.emit(this)
+ : this.messageStreamingStopEvents.emit(this);
+
+ this.messageContentStreamEvents.emit(this.content, isStreaming, this);
+ if (!isStreaming) {
+ this.messageContentStreamEvents.clear();
+ }
}
/**
@@ -463,4 +357,161 @@ export class Message {
throw new Error("Message type not recognized.");
}
+
+ /**
+ * Add a listener for message content changes.
+ *
+ * @param listener The listener to trigger when `content` changes
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onUpdate(listener: MessageUpdateListener) {
+ return this.messageUpdateEvents.addListener(listener);
+ }
+
+ /**
+ * Add a listener that is called only once when the message content changes.
+ *
+ * @param listener The listener to trigger when `content` changes
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onceUpdate(listener: MessageUpdateListener) {
+ return this.messageUpdateEvents.once(listener);
+ }
+
+ /**
+ * Removes a message update listener, previously set with `onUpdate`.
+ *
+ * @param listener The previously added listener
+ */
+ public offUpdate(listener: MessageUpdateListener) {
+ return this.messageUpdateEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener for message streaming state changes.
+ *
+ * @param listener The listener to trigger when `isStreaming` changes
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onStreamingUpdate(listener: MessageStreamingListener) {
+ return this.messageStreamingEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener that is called only once when the message streaming state changes.
+ *
+ * @param listener The listener to trigger when `isStreaming` changes
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onceStreamingUpdate(listener: MessageStreamingListener) {
+ return this.messageStreamingEvents.once(listener);
+ }
+
+ /**
+ * Removes a message streaming listener, previously set with `onStreamingUpdate`.
+ *
+ * @param listener The previously added listener
+ */
+ public offStreaming(listener: MessageStreamingListener) {
+ return this.messageStreamingEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener for message streaming start.
+ *
+ * @param listener The listener to trigger when `isStreaming` is set to `true`
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onStreamingStart(listener: MessageStreamingStartListener) {
+ return this.messageStreamingStartEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener that is called only once when the message streaming starts.
+ *
+ * @param listener The listener to trigger when `isStreaming` is set to `true`
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onceStreamingStart(listener: MessageStreamingStartListener) {
+ return this.messageStreamingStartEvents.once(listener);
+ }
+
+ /**
+ * Removes a message streaming start listener, previously set with `onStreamingStart`.
+ *
+ * @param listener The previously added listener
+ */
+ public offStreamingStart(listener: MessageStreamingStartListener) {
+ return this.messageStreamingStartEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener for message streaming stop.
+ *
+ * @param listener The listener to trigger when `isStreaming` is set to `false`
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onStreamingStop(listener: MessageStreamingStopListener) {
+ return this.messageStreamingStopEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener that is called only once when the message streaming stops.
+ *
+ * @param listener The listener to trigger when `isStreaming` is set to `false`
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onceStreamingStop(listener: MessageStreamingStopListener) {
+ return this.messageStreamingStopEvents.once(listener);
+ }
+
+ /**
+ * Removes a message streaming stop listener, previously set with `onStreamingStop`.
+ *
+ * @param listener The previously added listener
+ */
+ public offStreamingStop(listener: MessageStreamingStopListener) {
+ return this.messageStreamingStopEvents.removeListener(listener);
+ }
+
+ /**
+ * Adds a listener that is fired whenever the message content is updated during streaming. Also fires when streaming starts/ends.
+ *
+ * @remarks
+ * Unlike the other listeners which behave like normal event listeners, this special listener is unsubscribed automatically when streaming ends.
+ * If this is not desired, use `onUpdate` and `onStreamingStart`/`onStreamingStop` instead.
+ *
+ * @param listener The listener to trigger when `content` changes during streaming
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onContentStream(listener: MessageContentStreamListener) {
+ return this.messageContentStreamEvents.addListener(listener);
+ }
+
+ /**
+ * Adds a listener that is fired only once whenever the message content is updated during streaming. Also fires when streaming starts/ends.
+ *
+ * @remarks
+ * Unlike the other listeners which behave like normal event listeners, this special listener is unsubscribed automatically when streaming ends, regardless of whether it was called once or not.
+ * If this is not desired, use `onceUpdate` and `onceStreamingStart`/`onceStreamingStop` instead.
+ *
+ * @param listener The listener to trigger when `content` changes during streaming
+ * @returns An unsubscribe function for this `listener`
+ */
+ public onceContentStream(listener: MessageContentStreamListener) {
+ return this.messageContentStreamEvents.once(listener);
+ }
+
+ /**
+ * Removes a message streaming content listener, previously set with `onContentStream`.
+ *
+ * @remarks
+ * You do not need to call this method manually, as this listener automatically unsubscribes all listeners when streaming ends.
+ * You should use this if you want to unsubscribe a listener before streaming ends.
+ *
+ * @param listener The previously added listener
+ */
+ public offContentStream(listener: MessageContentStreamListener) {
+ return this.messageContentStreamEvents.removeListener(listener);
+ }
}
diff --git a/packages/lib/src/classes/index.ts b/packages/lib/src/classes/index.ts
index 06827a4..9ae4052 100644
--- a/packages/lib/src/classes/index.ts
+++ b/packages/lib/src/classes/index.ts
@@ -1,5 +1,12 @@
+export * from "./CallableFunctionParameters/index.js";
+
+export * from "./CallableFunction.js";
+export * from "./ChatCompletionService.js";
export * from "./Conversation.js";
+export * from "./ConversationCallableFunctions.js";
export * from "./ConversationConfig.js";
+export * from "./ConversationHistory.js";
+export * from "./ConversationPluginService.js";
+export * from "./ConversationRequestOptions.js";
+export * from "./EventManager.js";
export * from "./Message.js";
-export * from "./CallableFunction.js";
-export * from "./CallableFunctionParameters/index.js";
diff --git a/packages/lib/src/config/constants.ts b/packages/lib/src/config/constants.ts
index 45f5c43..e384ea9 100644
--- a/packages/lib/src/config/constants.ts
+++ b/packages/lib/src/config/constants.ts
@@ -13,26 +13,3 @@ export const ENDPOINT_CHATCOMPLETION =
"https://api.openai.com/v1/chat/completions";
export const ENDPOINT_MODERATION = "https://api.openai.com/v1/moderations";
-
-export const PRICING_TABLE = {
- unknown: {
- prompt: 0,
- completion: 0,
- },
- "3.5": {
- prompt: 0.0000015,
- completion: 0.000002,
- },
- "3.5-16k": {
- prompt: 0.000003,
- completion: 0.000004,
- },
- "4": {
- prompt: 0.00003,
- completion: 0.00006,
- },
- "4-32k": {
- prompt: 0.00006,
- completion: 0.00012,
- },
-};
diff --git a/packages/lib/src/exceptions/PluginNotInitializedException.ts b/packages/lib/src/exceptions/PluginNotInitializedException.ts
new file mode 100644
index 0000000..570de0d
--- /dev/null
+++ b/packages/lib/src/exceptions/PluginNotInitializedException.ts
@@ -0,0 +1,26 @@
+/**
+ * Thrown when a plugin attempts to use a provided service before said service has been injected into the plugin (during `init`).
+ * This usually happens when a plugin attempts to use a service in its constructor, since plugins are initialized after instantiation.
+ *
+ * @example
+ * ```typescript
+ * class TestPlugin extends ConversationPlugin {
+ * constructor() {
+ * super();
+ * this.conversation.history.setContext(...); // This throws a PluginNotInitializedException
+ * }
+ *
+ * init(...) {
+ * this.conversation.history.setContext(...); // This works
+ * }
+ * }
+ * ```
+ */
+export class PluginNotInitializedException extends Error {
+ constructor() {
+ super(
+ "Cannot use a service before it has been injected into the plugin."
+ );
+ this.name = PluginNotInitializedException.name;
+ }
+}
diff --git a/packages/lib/src/exceptions/index.ts b/packages/lib/src/exceptions/index.ts
index 1a2f902..d70fb06 100644
--- a/packages/lib/src/exceptions/index.ts
+++ b/packages/lib/src/exceptions/index.ts
@@ -1,2 +1,3 @@
export * from "./ModerationException.js";
export * from "./MessageRoleException.js";
+export * from "./PluginNotInitializedException.js";
diff --git a/packages/lib/src/schemas/conversation.schema.ts b/packages/lib/src/schemas/conversation.schema.ts
index bc444d6..84e622f 100644
--- a/packages/lib/src/schemas/conversation.schema.ts
+++ b/packages/lib/src/schemas/conversation.schema.ts
@@ -1,34 +1,19 @@
import { z } from "zod";
-import { messageSchema } from "./message.schema.js";
import { conversationConfigSchema } from "./conversationConfig.schema.js";
-import { callableFunctionSchema } from "./callableFunction.schema.js";
+import { conversationRequestOptionsSchema } from "./conversationRequestOptions.schema.js";
+import { conversationHistorySchema } from "./conversationHistory.schema.js";
+import { conversationCallableFunctionsSchema } from "./conversationCallableFunctions.schema.js";
/**
* A JSON representation of a Conversation instance.
*/
export const conversationSchema = z.object({
id: z.string().uuid().optional(),
- messages: z.array(messageSchema),
+ history: conversationHistorySchema.optional(),
config: conversationConfigSchema.optional(),
- functions: z.array(callableFunctionSchema),
- requestOptions: z
- .object({
- headers: z.record(z.string(), z.string()).optional(),
- proxy: z
- .object({
- host: z.string(),
- port: z.number().optional(),
- protocol: z.enum(["http", "https"]).optional(),
- auth: z
- .object({
- username: z.string(),
- password: z.string(),
- })
- .optional(),
- })
- .optional(),
- })
- .optional(),
+ callableFunctions: conversationCallableFunctionsSchema.optional(),
+ requestOptions: conversationRequestOptionsSchema.optional(),
+ pluginsData: z.record(z.any()).optional(),
});
/**
diff --git a/packages/lib/src/schemas/conversationCallableFunctions.schema.ts b/packages/lib/src/schemas/conversationCallableFunctions.schema.ts
new file mode 100644
index 0000000..241f590
--- /dev/null
+++ b/packages/lib/src/schemas/conversationCallableFunctions.schema.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+import { callableFunctionSchema } from "./callableFunction.schema.js";
+
+export const conversationCallableFunctionsSchema = z.object({
+ functions: z.array(callableFunctionSchema).optional(),
+});
+
+export type ConversationCallableFunctionsModel = z.infer<
+ typeof conversationCallableFunctionsSchema
+>;
diff --git a/packages/lib/src/schemas/conversationHistory.schema.ts b/packages/lib/src/schemas/conversationHistory.schema.ts
new file mode 100644
index 0000000..737106d
--- /dev/null
+++ b/packages/lib/src/schemas/conversationHistory.schema.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+import { messageSchema } from "./message.schema.js";
+
+export const conversationHistorySchema = z.object({
+ messages: z.array(messageSchema).optional(),
+});
+
+export type ConversationHistoryModel = z.infer<
+ typeof conversationHistorySchema
+>;
diff --git a/packages/lib/src/schemas/conversationRequestOptions.schema.ts b/packages/lib/src/schemas/conversationRequestOptions.schema.ts
new file mode 100644
index 0000000..81bd371
--- /dev/null
+++ b/packages/lib/src/schemas/conversationRequestOptions.schema.ts
@@ -0,0 +1,28 @@
+import { z } from "zod";
+
+/**
+ * A JSON representation of a ConversationRequestOptions instance.
+ */
+export const conversationRequestOptionsSchema = z.object({
+ headers: z.record(z.string(), z.string()).optional(),
+ proxy: z
+ .object({
+ host: z.string(),
+ port: z.number().optional(),
+ protocol: z.enum(["http", "https"]).optional(),
+ auth: z
+ .object({
+ username: z.string(),
+ password: z.string(),
+ })
+ .optional(),
+ })
+ .optional(),
+});
+
+/**
+ * A JSON representation of a ConversationRequestOptions instance.
+ */
+export type ConversationRequestOptionsModel = z.infer<
+ typeof conversationRequestOptionsSchema
+>;
diff --git a/packages/lib/src/schemas/index.ts b/packages/lib/src/schemas/index.ts
index 96b9a4b..28f8b8f 100644
--- a/packages/lib/src/schemas/index.ts
+++ b/packages/lib/src/schemas/index.ts
@@ -1,5 +1,9 @@
+export * from "./callableFunction.schema.js";
export * from "./conversation.schema.js";
+export * from "./conversationCallableFunctions.schema.js";
export * from "./conversationConfig.schema.js";
-export * from "./message.schema.js";
+export * from "./conversationHistory.schema.js";
+export * from "./conversationRequestOptions.schema.js";
+export * from "./javascriptIdentifier.schema.js";
export * from "./jsonSchema.schema.js";
-export * from "./callableFunction.schema.js";
+export * from "./message.schema.js";
diff --git a/packages/lib/src/utils/createChatCompletion.ts b/packages/lib/src/utils/createChatCompletion.ts
index f5f8968..c8eb84a 100644
--- a/packages/lib/src/utils/createChatCompletion.ts
+++ b/packages/lib/src/utils/createChatCompletion.ts
@@ -1,11 +1,11 @@
+import { ConversationRequestOptionsModel } from "schemas/conversationRequestOptions.schema.js";
import { ENDPOINT_CHATCOMPLETION } from "../index.js";
import getRequestHeaders from "./getRequestHeaders.js";
import getRequestUrl from "./getRequestUrl.js";
import {
CreateChatCompletionRequest,
- RequestOptions,
CreateChatCompletionResponse,
-} from "./types.js";
+} from "./types/index.js";
/**
* Sends a Create Chat Completion request to the OpenAI API.
@@ -16,7 +16,7 @@ import {
*/
export default async (
{ apiKey, ...chatCompletionRequest }: T,
- { headers: optHeaders = {}, proxy }: RequestOptions = {}
+ { headers: optHeaders = {}, proxy }: ConversationRequestOptionsModel = {}
): Promise<
T["stream"] extends true ? ReadableStream : CreateChatCompletionResponse
> => {
diff --git a/packages/lib/src/utils/createConversationPlugin.ts b/packages/lib/src/utils/createConversationPlugin.ts
new file mode 100644
index 0000000..78bc77a
--- /dev/null
+++ b/packages/lib/src/utils/createConversationPlugin.ts
@@ -0,0 +1,40 @@
+import {
+ ConversationPlugin,
+ ConversationPluginCreator,
+ ConversationPluginData,
+} from "./types/index.js";
+
+/**
+ * Convenience utility to create a conversation plugin with strict typing.
+ *
+ * This is the recommended way to create a type safe plugin, since it takes care of specifying generic exact type parameters of the `ConversationPlugin` interface.
+ * For example, this will make your plugin appear in the intellisense of the `Conversation.getPlugin` method.
+ *
+ * Your can also type the generic parameters yourself, or even use the `ConversationPlugin` interface directly instead of this utility.
+ *
+ * @example
+ * ```ts
+ * // Since the "pluginData" parameter cannot be inferred by the returned definition, you must specify it yourself.
+ * export default createConversationPlugin("example-plugin", ({}, pluginData?: number) => {
+ return {
+ onPostInit: () => {},
+ getPluginData: () => 1,
+ out: {} as StatsPluginOutput,
+ };
+});
+ * ```
+ *
+ * @param name A unique name for your plugin
+ * @param creator A function that creates your plugin definition
+ * @returns A conversation plugin that can be used in the `Conversation` constructor
+ */
+export default <
+ TName extends string,
+ TOut = undefined,
+ TData extends ConversationPluginData = undefined,
+>(
+ name: TName,
+ creator: ConversationPluginCreator
+) => {
+ return { name, creator } as ConversationPlugin;
+};
diff --git a/packages/lib/src/utils/createDryChatCompletion.ts b/packages/lib/src/utils/createDryChatCompletion.ts
index 89173b1..8b0b22a 100644
--- a/packages/lib/src/utils/createDryChatCompletion.ts
+++ b/packages/lib/src/utils/createDryChatCompletion.ts
@@ -1,5 +1,4 @@
-import getMessageTokens from "./getMessageTokens.js";
-import { CreateDryChatCompletionConfig } from "./types.js";
+import { CreateDryChatCompletionConfig } from "./types/index.js";
/**
* Creates a dry chat completion and returns a `ReadableStream`. Can be used to simulate a streamed chat completion.
@@ -17,7 +16,8 @@ export default (
initialDelay = 500,
chunkDelay = 50,
} = config;
- const tokens = getMessageTokens(message);
+
+ const tokens = message.match(/.{1,5}/g) ?? [];
const id = `chatcmpl-${Math.random().toString(36).substring(2)}`;
const created = Date.now();
diff --git a/packages/lib/src/utils/createModeration.ts b/packages/lib/src/utils/createModeration.ts
index 9955bde..05aae84 100644
--- a/packages/lib/src/utils/createModeration.ts
+++ b/packages/lib/src/utils/createModeration.ts
@@ -1,11 +1,11 @@
+import { ConversationRequestOptionsModel } from "schemas/conversationRequestOptions.schema.js";
import { ENDPOINT_MODERATION } from "../index.js";
import getRequestHeaders from "./getRequestHeaders.js";
import getRequestUrl from "./getRequestUrl.js";
import {
CreateModerationRequest,
CreateModerationResponse,
- RequestOptions,
-} from "./types.js";
+} from "./types/index.js";
/**
* Sends a Create Moderation request to the OpenAI API.
@@ -16,7 +16,7 @@ import {
*/
export default async (
{ apiKey, input }: CreateModerationRequest,
- { headers: optHeaders = {}, proxy }: RequestOptions
+ { headers: optHeaders = {}, proxy }: ConversationRequestOptionsModel
): Promise => {
const headers = getRequestHeaders(apiKey, optHeaders, proxy);
const url = getRequestUrl(ENDPOINT_MODERATION, proxy);
diff --git a/packages/lib/src/utils/getMessageTokens.ts b/packages/lib/src/utils/getMessageTokens.ts
deleted file mode 100644
index bfdaab3..0000000
--- a/packages/lib/src/utils/getMessageTokens.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { encode } from "gpt-token-utils";
-
-/**
- * Returns an array of tokens from a given `message`.
- *
- * @param message The message to get the tokens from.
- */
-export default (message: string): string[] => {
- return encode(message).matchedTextSegments;
-};
diff --git a/packages/lib/src/utils/getRequestHeaders.ts b/packages/lib/src/utils/getRequestHeaders.ts
index cae4f59..b5f3cfb 100644
--- a/packages/lib/src/utils/getRequestHeaders.ts
+++ b/packages/lib/src/utils/getRequestHeaders.ts
@@ -1,13 +1,13 @@
+import { ConversationRequestOptionsModel } from "schemas/conversationRequestOptions.schema.js";
import base64Encode from "./base64Encode.js";
-import { RequestOptionsProxy } from "./types.js";
/**
* Returns the headers object for a request.
*/
export default (
apiKey: string,
- optHeaders: Record = {},
- proxy?: RequestOptionsProxy
+ optHeaders: ConversationRequestOptionsModel["headers"] = {},
+ proxy: ConversationRequestOptionsModel["proxy"]
) => {
const headers = new Headers({
...optHeaders,
diff --git a/packages/lib/src/utils/getRequestUrl.ts b/packages/lib/src/utils/getRequestUrl.ts
index 0f34e9e..a12f471 100644
--- a/packages/lib/src/utils/getRequestUrl.ts
+++ b/packages/lib/src/utils/getRequestUrl.ts
@@ -1,9 +1,12 @@
-import { RequestOptionsProxy } from "./types.js";
+import { ConversationRequestOptionsModel } from "schemas/conversationRequestOptions.schema.js";
/**
* Returns the URL object for a request, taking into account a proxy.
*/
-export default (targetUrl: string, proxy?: RequestOptionsProxy) => {
+export default (
+ targetUrl: string,
+ proxy?: ConversationRequestOptionsModel["proxy"]
+) => {
if (!proxy) return new URL(targetUrl);
const protocol = proxy.protocol || "http";
diff --git a/packages/lib/src/utils/index.ts b/packages/lib/src/utils/index.ts
index 2311568..bafbe77 100644
--- a/packages/lib/src/utils/index.ts
+++ b/packages/lib/src/utils/index.ts
@@ -1,9 +1,9 @@
-export { default as getMessageSize } from "./getMessageSize.js";
-export { default as getMessageCost } from "./getMessageCost.js";
-export { default as getMessageTokens } from "./getMessageTokens.js";
+export * from "./types/index.js";
+
+export { default as base64Encode } from "./base64Encode.js";
export { default as createChatCompletion } from "./createChatCompletion.js";
export { default as createDryChatCompletion } from "./createDryChatCompletion.js";
-export { default as getPricing } from "./getPricing.js";
+export { default as createModeration } from "./createModeration.js";
+export { default as createConversationPlugin } from "./createConversationPlugin.js";
export { default as getRequestHeaders } from "./getRequestHeaders.js";
export { default as getRequestUrl } from "./getRequestUrl.js";
-export * from "./types.js";
diff --git a/packages/lib/src/utils/types/callableFunction.types.ts b/packages/lib/src/utils/types/callableFunction.types.ts
new file mode 100644
index 0000000..228b2a0
--- /dev/null
+++ b/packages/lib/src/utils/types/callableFunction.types.ts
@@ -0,0 +1,10 @@
+import { JsonSchemaObject } from "../../schemas/jsonSchema.schema.js";
+
+/**
+ * A function that can be called by the model.
+ */
+export interface CreateChatCompletionFunction {
+ name: string;
+ description?: string;
+ parameters?: JsonSchemaObject;
+}
diff --git a/packages/lib/src/utils/types.ts b/packages/lib/src/utils/types/chatCompletionService.types.ts
similarity index 53%
rename from packages/lib/src/utils/types.ts
rename to packages/lib/src/utils/types/chatCompletionService.types.ts
index ae5e825..d9d73ea 100644
--- a/packages/lib/src/utils/types.ts
+++ b/packages/lib/src/utils/types/chatCompletionService.types.ts
@@ -1,17 +1,9 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import { Conversation } from "../classes/Conversation.js";
-import { ConversationConfig } from "../classes/ConversationConfig.js";
-import { Message } from "../classes/Message.js";
-import { JsonSchemaObject } from "../schemas/jsonSchema.schema.js";
-
-/**
- * Supported values for the `role` property of a message.
- */
-export type ChatCompletionRequestMessageRoleEnum =
- | "user"
- | "system"
- | "assistant"
- | "function";
+import { CreateChatCompletionFunction } from "./callableFunction.types.js";
+import {
+ CreateChatCompletionFunctionCallMessage,
+ CreateChatCompletionFunctionMessage,
+ CreateChatCompletionMessage,
+} from "./message.types.js";
/**
* Overridable {@link CreateChatCompletionRequest} properties of a {@link Conversation}'s config for a single prompt.
@@ -26,173 +18,6 @@ export type PromptOptions = Omit<
*/
export type HandleChatCompletionOptions = Omit;
-/**
- * Listener for when a {@link Message} is added to a conversation.
- */
-export type AddMessageListener = (
- /**
- * The {@link Message message} that was added
- */
- message: Message
-) => void;
-
-/**
- * Listener for when a {@link Message} is removed from a conversation.
- */
-export type RemoveMessageListener = (
- /**
- * The {@link Message message} that was removed
- */
- message: Message
-) => void;
-
-/**
- * Proxy configuration to use for requests to the OpenAI API.
- */
-export interface RequestOptionsProxy {
- /**
- * The hostname or IP address of the proxy server.
- *
- * @example "proxy.example.com"
- * @example "127.0.0.1"
- */
- host: string;
-
- /**
- * The port number of the proxy server.
- *
- * @default 80 for HTTP, 443 for HTTPS
- */
- port?: number;
-
- /**
- * The HTTP protocol used by the proxy server.
- */
- protocol?: "http" | "https";
-
- /**
- * **HTTP Basic** credentials for the proxy server.
- *
- * @remarks
- * The `Proxy-Authorization` header will be added with the Base64 encoded value of `username:password`: `Proxy-Authorization: Basic ${base64(username:password)}`
- */
- auth?: {
- /**
- * The username to use for authentication.
- */
- username: string;
-
- /**
- * The password to use for authentication.
- */
- password: string;
- };
-}
-
-/**
- * Headers and proxy configuration to use for requests to the OpenAI API.
- */
-export interface RequestOptions {
- /**
- * Additional headers to send with the request.
- *
- * @remarks
- * Note that the `Content-Type`, `Authorization` and `Proxy-Authorization` headers are ignored, as they are set by the library itself.
- */
- headers?: Record;
-
- /**
- * Proxy configuration to use for the request.
- *
- * @remarks
- * Example resulting request url with a proxy:\
- * `https://my.proxy.com:1337/https://api.openai.com/v1/chat/completions`
- */
- proxy?: RequestOptionsProxy;
-}
-
-/**
- * A message in OpenAI's chat format.
- *
- * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
- */
-export interface CreateChatCompletionMessage {
- /**
- * The message role.
- *
- * @see {@link ChatCompletionRequestMessageRoleEnum}
- */
- role: Exclude;
-
- /**
- * The message content.
- */
- content: string;
-
- function_call?: undefined;
- name?: undefined;
-}
-
-export type CompletionMessage = Message & {
- role: Exclude;
- content: string;
- functionCall: undefined;
- name: undefined;
-};
-
-/**
- * A function_call-related message in OpenAI's chat format.
- *
- * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
- */
-export interface CreateChatCompletionFunctionCallMessage {
- role: Extract;
- content: null;
- function_call: {
- name: string;
- arguments: string;
- };
-
- name?: undefined;
-}
-
-export type FunctionCallMessage = Message & {
- role: "assistant";
- content: null;
- functionCall: {
- name: string;
- arguments: Record;
- };
-};
-
-/**
- * A function-related message in OpenAI's chat format.
- *
- * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
- */
-export interface CreateChatCompletionFunctionMessage {
- role: Extract;
- content: string;
- name: string;
-
- function_call?: undefined;
-}
-
-export type FunctionMessage = Message & {
- role: "function";
- name: string;
- content: string;
-};
-
-/**
- * A function that can be called by the model.
- */
-export interface CreateChatCompletionFunction {
- name: string;
- description?: string;
- parameters?: JsonSchemaObject;
-}
-
/**
* Request body for a Create Chat Completion request, which creates a completion for the conversation's messages.
*
@@ -397,7 +222,7 @@ export interface CreateChatCompletionResponse {
* The index of the choice in the request. Always 0.
*/
index: number;
- }
+ },
];
}
@@ -446,85 +271,7 @@ export interface CreateChatCompletionStreamResponse {
* The index of the choice in the request. Always 0.
*/
index: number;
- }
- ];
-}
-
-/**
- * Request body for a Create Moderation request, which classifies if text violates OpenAI's Content Policy.
- *
- * Endpoint: `POST https://api.openai.com/v1/moderations`
- *
- * @see {@link https://platform.openai.com/docs/api-reference/moderations/create Create Moderation Request Body}
- */
-export interface CreateModerationRequest {
- /**
- * Your OpenAI API key.
- */
- apiKey: string;
-
- /**
- * The text to classify.
- */
- input: string;
-}
-
-/**
- * Response body for a {@link CreateModerationRequest Create Moderation Request}.
- */
-export interface CreateModerationResponse {
- /**
- * The ID of the request.
- */
- id: string;
-
- /**
- * The moderation model used to classify the text.
- */
- model: string;
-
- /**
- * The classification results.
- */
- results: [
- {
- /**
- * Contains a dictionary of per-category binary usage policies violation flags.
- * For each category, the value is `true` if the model flags the corresponding category as violated, `false` otherwise.
- *
- * @see {@link https://platform.openai.com/docs/guides/moderation/overview Moderation Categories}
- */
- categories: {
- hate: boolean;
- "hate/threatening": boolean;
- "self-harm": boolean;
- sexual: boolean;
- "sexual/minors": boolean;
- violence: boolean;
- "violence/graphic": boolean;
- };
-
- /**
- * Contains a dictionary of per-category raw scores output by the model, denoting the model's confidence that the input violates the OpenAI's policy for the category.
- * The value is between 0 and 1, where higher values denote higher confidence. The scores should not be interpreted as probabilities.
- *
- * @see {@link https://platform.openai.com/docs/guides/moderation/overview Moderation Categories}
- */
- category_scores: {
- hate: number;
- "hate/threatening": number;
- "self-harm": number;
- sexual: number;
- "sexual/minors": number;
- violence: number;
- "violence/graphic": number;
- };
-
- /**
- * Set to `true` if the model classifies the content as violating OpenAI's usage policies, `false` otherwise.
- */
- flagged: boolean;
- }
+ },
];
}
@@ -553,97 +300,3 @@ export interface CreateDryChatCompletionConfig {
*/
chunkDelay?: number;
}
-
-/**
- * Default values for OpenAI's Chat Completion API supported by the {@link ConversationConfig} class.
- */
-export type ConversationConfigChatCompletionOptions = Omit<
- Partial,
- "messages"
->;
-
-/**
- * Library specific configuration options for the {@link Conversation} class, used by the {@link ConversationConfig} class.
- */
-export interface ConversationConfigOptions {
- /**
- * The first system message to set the context for the GPT model.
- *
- * @default "You are a large language model trained by OpenAI. Answer as concisely as possible."
- */
- context?: string;
-
- /**
- * Dry run. Don't send any requests to OpenAI. Responses will mirror the last message in the conversation.
- *
- * @default false
- */
- dry?: boolean;
-
- /**
- * By default, messages are checked for violations of the OpenAI Community Guidelines and throw an error if any are found.\
- * Set this to `true` to disable this check.\
- * Set this to `"soft"` to still check for violations, but not throw an error if any are found. The violations will be added to the `flags` property of the message.
- *
- * **Note:** This is not recommended, as it could result in account suspension. Additionally, {@link https://platform.openai.com/docs/guides/moderation OpenAI's Moderation API} is free to use.
- *
- * @default false
- */
- disableModeration?: boolean | "soft";
-}
-
-/**
- * Combines the {@link ConversationConfigOptions} and {@link ConversationConfigChatCompletionOptions} types.
- */
-export type ConversationConfigParameters = ConversationConfigOptions &
- ConversationConfigChatCompletionOptions;
-
-/**
- * Listener for when a {@link Message message}'s content is updated.
- */
-export type MessageUpdateListener = (
- /**
- * The new content of the message.
- */
- content: string | null,
-
- /**
- * The {@link Message message} instance that was updated.
- */
- message: Message
-) => void;
-
-/**
- * Listener for when a {@link Message message}'s streaming state is updated.
- */
-export type MessageStreamingListener = (
- /**
- * The new streaming state of the message.
- */
- isStreaming: boolean,
-
- /**
- * The {@link Message message} instance that was updated.
- */
- message: Message
-) => void;
-
-/**
- * Listener for when a {@link Message message} starts streaming.
- */
-export type MessageStreamingStartListener = (
- /**
- * The {@link Message message} instance that started streaming.
- */
- message: Message
-) => void;
-
-/**
- * Listener for when a {@link Message message} stops streaming.
- */
-export type MessageStreamingStopListener = (
- /**
- * The {@link Message message} instance that stopped streaming.
- */
- message: Message
-) => void;
diff --git a/packages/lib/src/utils/types/conversation.types.ts b/packages/lib/src/utils/types/conversation.types.ts
new file mode 100644
index 0000000..893c91b
--- /dev/null
+++ b/packages/lib/src/utils/types/conversation.types.ts
@@ -0,0 +1,106 @@
+import { ConversationModel } from "../../schemas/conversation.schema.js";
+import { ConversationPlugin } from "./conversationPlugin.types.js";
+
+export interface ConversationOptions extends ConversationModel {
+ /**
+ * Plugins to be used in the conversation.
+ *
+ * Note that the order of the plugins is important. Each plugin are called in the order they are defined.
+ */
+ plugins?: ConversationPlugin[];
+}
+
+/**
+ * Override this type in an interface to have type-safe global plugins. By default, `Conversation.globalPlugins` is of the general type `ConversationPlugin[]`.
+ */
+export interface ConversationGlobalPluginsOverride {}
+
+/**
+ * Specifies the type of the global plugins of a conversation.
+ *
+ * @internal
+ * This type is used internally to determine the type of the global plugins of a conversation.
+ * You're probably looking for `ConversationGlobalPluginsOverride` instead.
+ */
+export type ConversationGlobalPluginsOverwritten =
+ ConversationGlobalPluginsOverride extends {
+ globalPlugins: ConversationPlugin[];
+ }
+ ? ConversationGlobalPluginsOverride
+ : { globalPlugins: ConversationPlugin[] };
+
+/**
+ * Specifies the type of the global plugins of a conversation.
+ *
+ * @internal
+ * This type is used internally to determine the type of the global plugins of a conversation.
+ * You're probably looking for `ConversationGlobalPlugins` instead.
+ */
+export type ConversationGlobalPlugins =
+ ConversationGlobalPluginsOverwritten["globalPlugins"];
+
+/**
+ * Utility type to infer the type of the plugin names available in a list of plugins.
+ *
+ * @internal
+ * This type is used internally to determine the type of the plugin names available in a conversation. It should have no use outside of the library.
+ */
+export type PluginNameFromPlugins<
+ TPlugins extends ConversationPlugin[],
+ TPlugin = TPlugins extends ConversationPlugin[] ? TPlugins[number] : string,
+ TPluginDefinition = TPlugin extends ConversationPlugin
+ ? ReturnType
+ : string,
+> = TPluginDefinition extends { name: infer U } ? U | (string & {}) : string; // (string & {}) is a neat trick to provide intellisense for U, while still allowing any string
+
+/**
+ * Utility type to infer the possible plugin names from the global plugins of a conversation.
+ *
+ * @internal
+ * This type is used internally to determine the type of the plugin names available in a conversation. It should have no use outside of the library.
+ */
+export type GlobalPluginName = PluginNameFromPlugins;
+
+/**
+ * Utility type to infer the possible plugin names available in a conversation, based on the options passed to the conversation.
+ *
+ * @internal
+ * This type is used internally to determine the type of the plugin names available in a conversation. It should have no use outside of the library.
+ */
+export type PluginNameFromConversationOptions<
+ TOptions extends ConversationOptions,
+> = TOptions["plugins"] extends ConversationPlugin[]
+ ? PluginNameFromPlugins
+ : string;
+
+/**
+ * Utility type to infer the possible plugin names available in a conversation, based on the options passed to the conversation, as well as the global plugins.
+ *
+ * @internal
+ * This type is used internally to determine the type of the plugin names available in a conversation. It should have no use outside of the library.
+ */
+export type PluginNameFromConversationOptionsWithGlobalPlugins<
+ TOptions extends ConversationOptions,
+> = ConversationGlobalPluginsOverride extends {
+ globalPlugins: ConversationPlugin[];
+}
+ ? TOptions["plugins"] extends ConversationPlugin[]
+ ? PluginNameFromConversationOptions | GlobalPluginName
+ : GlobalPluginName
+ : PluginNameFromConversationOptions | (string & {});
+
+export type PluginsFromConversationOptions<
+ TOptions extends ConversationOptions,
+> = TOptions["plugins"] extends ConversationPlugin[]
+ ? TOptions["plugins"]
+ : ConversationPlugin[];
+
+export type PluginsFromConversationOptionsWithGlobalPlugins<
+ TOptions extends ConversationOptions,
+> = ConversationGlobalPluginsOverride extends {
+ globalPlugins: ConversationPlugin[];
+}
+ ? TOptions["plugins"] extends ConversationPlugin[]
+ ? PluginsFromConversationOptions | ConversationGlobalPlugins
+ : ConversationGlobalPlugins
+ : PluginsFromConversationOptions;
diff --git a/packages/lib/src/utils/types/conversationCallableFunctions.types.ts b/packages/lib/src/utils/types/conversationCallableFunctions.types.ts
new file mode 100644
index 0000000..0714067
--- /dev/null
+++ b/packages/lib/src/utils/types/conversationCallableFunctions.types.ts
@@ -0,0 +1,15 @@
+import { CallableFunction } from "../../classes/CallableFunction.js";
+
+export type AddCallableFunctionListener = (
+ /**
+ * The {@link CallableFunction callable function} that was added
+ */
+ callableFunction: CallableFunction
+) => void;
+
+export type RemoveCallableFunctionListener = (
+ /**
+ * The {@link CallableFunction callable function} that was removed
+ */
+ callableFunction: CallableFunction
+) => void;
diff --git a/packages/lib/src/utils/types/conversationConfig.types.ts b/packages/lib/src/utils/types/conversationConfig.types.ts
new file mode 100644
index 0000000..cad17dc
--- /dev/null
+++ b/packages/lib/src/utils/types/conversationConfig.types.ts
@@ -0,0 +1,66 @@
+import { CreateChatCompletionRequest } from "./chatCompletionService.types.js";
+
+/**
+ * **OpenAI-specific** configuration options for the {@link Conversation} class, used by the {@link ConversationConfig} class.
+ *
+ * @remarks
+ * The `messages` property is omitted since messages is handled entirely by the library.
+ */
+export type ChatCompletionConfigOptions = Omit<
+ Partial,
+ "messages"
+>;
+
+/**
+ * **Library specific** configuration options for the {@link Conversation} class, used by the {@link ConversationConfig} class.
+ */
+export interface ConversationConfigOptions {
+ /**
+ * The first system message to set the context for the GPT model.
+ *
+ * @default "You are a large language model trained by OpenAI. Answer as concisely as possible."
+ */
+ context?: string;
+
+ /**
+ * Dry run. Don't send any requests to OpenAI. Responses will mirror the last message in the conversation.
+ *
+ * @default false
+ */
+ dry?: boolean;
+
+ /**
+ * By default, messages are checked for violations of the OpenAI Community Guidelines and throw an error if any are found.\
+ * Set this to `true` to disable this check.\
+ * Set this to `"soft"` to still check for violations, but not throw an error if any are found. The violations will be added to the `flags` property of the message.
+ *
+ * **Note:** This is not recommended, as it could result in account suspension. Additionally, {@link https://platform.openai.com/docs/guides/moderation OpenAI's Moderation API} is free to use.
+ *
+ * @default false
+ */
+ disableModeration?: boolean | "soft";
+}
+
+/**
+ * Although all properties of `ChatCompletionConfigOptions` are optional, some of them are certain to have a value when used in a `ConversationConfig` instance.
+ *
+ * @internal Only used for convenience within the `ConversationConfig` class. Not meant to be used by consumers of the library.
+ */
+export type ChatCompletionConfigProperties = ChatCompletionConfigOptions &
+ Required>;
+
+/**
+ * Although all properties of `ConversationConfigOptions` are optional, some of them are certain to have a value when used in a `ConversationConfig` instance.
+ *
+ * @internal Only used for convenience within the `ConversationConfig` class. Not meant to be used by consumers of the library.
+ */
+export type ConversationConfigProperties = ConversationConfigOptions &
+ Required<
+ Pick
+ >;
+
+/**
+ * @internal Only used for convenience within the `ConversationConfig` class. Not meant to be used by consumers of the library.
+ */
+export type ConfigProperties = ChatCompletionConfigProperties &
+ ConversationConfigProperties;
diff --git a/packages/lib/src/utils/types/conversationHistory.types.ts b/packages/lib/src/utils/types/conversationHistory.types.ts
new file mode 100644
index 0000000..6984bcc
--- /dev/null
+++ b/packages/lib/src/utils/types/conversationHistory.types.ts
@@ -0,0 +1,21 @@
+import { Message } from "../../classes/Message.js";
+
+/**
+ * Listener for when a {@link Message} is added to a conversation.
+ */
+export type AddMessageListener = (
+ /**
+ * The {@link Message message} that was added
+ */
+ message: Message
+) => void;
+
+/**
+ * Listener for when a {@link Message} is removed from a conversation.
+ */
+export type RemoveMessageListener = (
+ /**
+ * The {@link Message message} that was removed
+ */
+ message: Message
+) => void;
diff --git a/packages/lib/src/utils/types/conversationPlugin.types.ts b/packages/lib/src/utils/types/conversationPlugin.types.ts
new file mode 100644
index 0000000..cb54dd7
--- /dev/null
+++ b/packages/lib/src/utils/types/conversationPlugin.types.ts
@@ -0,0 +1,354 @@
+import { ChatCompletionService } from "../../classes/ChatCompletionService.js";
+import { Conversation } from "../../classes/Conversation.js";
+import { ConversationCallableFunctions } from "../../classes/ConversationCallableFunctions.js";
+import { ConversationConfig } from "../../classes/ConversationConfig.js";
+import { ConversationHistory } from "../../classes/ConversationHistory.js";
+import { ConversationPluginService } from "../../classes/ConversationPluginService.js";
+import { ConversationPlugins } from "../../classes/ConversationPlugins.js";
+import { ConversationRequestOptions } from "../../classes/ConversationRequestOptions.js";
+import { Message } from "../../classes/Message.js";
+import { ConversationModel } from "../../schemas/conversation.schema.js";
+import { ConversationCallableFunctionsModel } from "../../schemas/conversationCallableFunctions.schema.js";
+import { ConversationConfigModel } from "../../schemas/conversationConfig.schema.js";
+import { ConversationHistoryModel } from "../../schemas/conversationHistory.schema.js";
+import { ConversationRequestOptionsModel } from "../../schemas/conversationRequestOptions.schema.js";
+
+/**
+ * Conversation plugins are used to extend the functionality of a conversation.
+ */
+export interface ConversationPlugin<
+ TName extends string = string,
+ TOut = any,
+ TData extends ConversationPluginData = any,
+> {
+ /**
+ * A **unique** name for this plugin. You should set this to your plugin's package name to avoid name collisions.
+ */
+ name: TName;
+
+ /**
+ * Creator function to create a `ConversationPluginCreatorDefinition` object.
+ * This object is the one that will be used by the library to interact with the conversation.
+ *
+ * @param properties Conversation properties exposed to the plugin
+ * @param pluginData The plugin data previously stored by this plugin with the `getPluginData` method.
+ * @returns A `ConversationPluginCreatorDefinition` object with methods to interact with the conversation.
+ */
+ creator: ConversationPluginCreator;
+}
+
+export interface ConversationPluginCreator<
+ TOut = any,
+ TData extends ConversationPluginData = any,
+> {
+ (
+ /**
+ * The properties of the conversation this plugin has access to.
+ *
+ * Internally, these are read-only, so you are guaranteed they will not change (reference) while your plugin is running.
+ */
+ properties: ConversationPluginProperties,
+
+ /**
+ * The plugin data previously stored by this plugin with the `getPluginData` method.
+ */
+ pluginData?: TData
+ ): ConversationPluginCreatorDefinition;
+}
+
+/**
+ * The properties of the conversation a plugin has access to.
+ */
+export interface ConversationPluginProperties {
+ /**
+ * The conversation the plugin is attached to.
+ */
+ conversation: Conversation;
+
+ /**
+ * The configuration of the conversation.
+ */
+ config: ConversationConfig;
+
+ /**
+ * The request options of the conversation.
+ */
+ requestOptions: ConversationRequestOptions;
+
+ /**
+ * The history of the conversation.
+ */
+ history: ConversationHistory;
+
+ /**
+ * The callable functions of the conversation.
+ */
+ callableFunctions: ConversationCallableFunctions;
+
+ /**
+ * The plugins of the conversation.
+ */
+ plugins: ConversationPlugins;
+
+ /**
+ * The chat completion service of the conversation.
+ * Contains **internal** methods to interact with the chat completion API from OpenAI.
+ */
+ chatCompletionService: ChatCompletionService;
+
+ /**
+ * The plugin service of the conversation.
+ * Contains **internal** methods to interact with the plugins.
+ */
+ pluginService: ConversationPluginService;
+}
+
+/**
+ * The base definition of a conversation plugin without output.
+ */
+export interface ConversationPluginDefinitionBase {
+ /**
+ * Called after all plugins have been initialized.
+ *
+ * @remarks
+ * This could potentially be used to interop with other plugins.
+ */
+ onPostInit?: () => void;
+
+ /**
+ * Transform the `ConversationModel` returned by the `Conversation.toJSON` method.
+ *
+ * Must return a valid `ConversationModel`.
+ * Model is validated using the `conversationSchema` **at the end** of all transformations.
+ *
+ * @remarks
+ * You should **not** modify the `pluginsData` property of the `ConversationModel` in this method.
+ * Use the `getPluginData` method instead to prevent conflicts with other plugins.
+ *
+ * @param json The current `ConversationModel` that was generated by the library (or previous plugins)
+ * @returns The new state of the `ConversationModel` to be returned by the `Conversation.toJSON` method.
+ */
+ transformConversationJson?: (json: ConversationModel) => ConversationModel;
+
+ /**
+ * Transform the `ConversationCallableFunctionsModel` returned by the `ConversationCallableFunctions.toJSON` method.
+ * Note that this is the model of the `ConversationCallableFunctions` class, not the `CallableFunction` class.
+ *
+ * Must return a valid `ConversationCallableFunctionsModel`.
+ * Model is validated using the `conversationCallableFunctionsSchema` **at the end** of all transformations.
+ *
+ * @param json The current `ConversationCallableFunctionsModel` that was generated by the library (or previous plugins)
+ * @returns The new state of the `ConversationCallableFunctionsModel` to be returned by the `ConversationCallableFunctions.toJSON` method.
+ */
+ transformConversationCallableFunctionsJson?: (
+ json: ConversationCallableFunctionsModel
+ ) => ConversationCallableFunctionsModel;
+
+ /**
+ * Transform the `ConversationConfigModel` returned by the `ConversationConfig.toJSON` method.
+ *
+ * Must return a valid `ConversationConfigModel`.
+ * Model is validated using the `conversationConfigSchema` **at the end** of all transformations.
+ *
+ * @param json The current `ConversationConfigModel` that was generated by the library (or previous plugins)
+ * @returns The new state of the `ConversationConfigModel` to be returned by the `ConversationConfig.toJSON` method.
+ */
+ transformConversationConfigJson?: (
+ json: ConversationConfigModel
+ ) => ConversationConfigModel;
+
+ /**
+ * Transform the `ConversationHistoryModel` returned by the `ConversationHistory.toJSON` method.
+ *
+ * Must return a valid `ConversationHistoryModel`.
+ * Model is validated using the `conversationHistorySchema` **at the end** of all transformations.
+ *
+ * @param json The current `ConversationHistoryModel` that was generated by the library (or previous plugins)
+ * @returns The new state of the `ConversationHistoryModel` to be returned by the `ConversationHistory.toJSON` method.
+ */
+ transformConversationHistoryJson?: (
+ json: ConversationHistoryModel
+ ) => ConversationHistoryModel;
+
+ /**
+ * Transform the `ConversationRequestOptionsModel` returned by the `ConversationRequestOptions.toJSON` method.
+ *
+ * Must return a valid `ConversationRequestOptionsModel`.
+ * Model is validated using the `conversationRequestOptionsSchema` **at the end** of all transformations.
+ *
+ * @param json The current `ConversationRequestOptionsModel` that was generated by the library (or previous plugins)
+ * @returns The new state of the `ConversationRequestOptionsModel` to be returned by the `ConversationRequestOptions.toJSON` method.
+ */
+ transformConversationRequestOptionsJson?: (
+ json: ConversationRequestOptionsModel
+ ) => ConversationRequestOptionsModel;
+
+ /**
+ * Tap into a user message instance created during prompt.
+ *
+ * @param message The user message instance
+ */
+ onUserPrompt?: (message: Message) => void | Promise;
+
+ /**
+ * Called when an error is thrown during a user prompt.
+ * This can be useful to clean up your plugin's state.
+ *
+ * @remarks
+ * Errors thrown during this method will have no effect.
+ * This is to ensure all plugins implementing this method are called.
+ *
+ * @param error The error that was thrown during a user prompt
+ */
+ onUserPromptError?: (error: unknown) => void | Promise;
+
+ /**
+ * Tap into an assistant message instance during a chat completion **before** it is added to the history.
+ *
+ * @remarks
+ * Keep in mind that the message passed to this method is not necessarily a standard chat completion. It could be a function call too.
+ * Additionnaly, the message could be streamed or not, so message content may not yet be available when this method is called.
+ *
+ * @param message The assistant message instance
+ */
+ onChatCompletion?: (message: Message) => void | Promise;
+
+ /**
+ * Transform the function prompt result before it is stringified.
+ *
+ * @param result The result of the function generated by the client code
+ * @returns The new result. Keep in mind this result is later stringified (using `JSON.stringify`). Your result must be serializable.
+ */
+ transformFunctionResult?: (result: any) => any | Promise;
+
+ /**
+ * Tap into a function message instance.
+ *
+ * @param prompt The stringified result of what was passed to the `Conversation.functionPrompt` method, after it was transformed by `transformFunctionResult`.
+ */
+ onFunctionPrompt?: (prompt: Message) => void | Promise;
+
+ /**
+ * Called when an error is thrown during a function prompt.
+ * This can be useful to clean up your plugin's state.
+ *
+ * @remarks
+ * Errors thrown during this method will have no effect.
+ * This is to ensure all plugins implementing this method are called.
+ *
+ * @param error The error that was thrown during a function prompt
+ */
+ onFunctionPromptError?: (error: unknown) => void | Promise;
+
+ /**
+ * Tap into a message that was moderated.
+ *
+ * @param message The message that was moderated, with the `message.flags` property populated.
+ */
+ onModeration?: (message: Message) => void | Promise;
+}
+
+export type ConversationPluginDefinitionOutput = TOut extends undefined
+ ? {
+ out?: undefined;
+ }
+ : {
+ /**
+ * The output of the plugin
+ * This is useful for plugins that want to expose some functionality to client code.
+ * This output can be virtually anything, such as a class instance, a function, or a primitive value.
+ *
+ * @example
+ * Typically, client code would access the output of a plugin like this:
+ *
+ * ```ts
+ * const myPlugin = conversation.getPlugin("myPlugin");
+ * const myPluginOutput = myPlugin.out;
+ * ```
+ */
+ out: TOut;
+ };
+
+export type ConversationPluginDefinitionGetPluginData<
+ TData extends ConversationPluginData,
+> = TData extends undefined
+ ? {
+ getPluginData?: () => undefined;
+ }
+ : {
+ /**
+ * Allows your plugin to store data in the `pluginsData` property of the `ConversationModel` returned by the `Conversation.toJSON` method.
+ * This data will be stored under the name of your plugin and will be accessible when your plugin is initialized. (second argument of the plugin function)
+ *
+ * @remarks
+ * This data should be serializable.
+ *
+ * @returns The data to be stored in the `pluginsData` property of the `ConversationModel` returned by the `Conversation.toJSON` method.
+ */
+ getPluginData: () => TData | Promise;
+ };
+
+/**
+ * The definition of a conversation plugin, which includes methods to interact with the conversation.
+ */
+export type ConversationPluginCreatorDefinition<
+ TOut = undefined,
+ TData extends ConversationPluginData = undefined,
+> = ConversationPluginDefinitionBase &
+ ConversationPluginDefinitionOutput &
+ ConversationPluginDefinitionGetPluginData;
+
+/**
+ * Same as `ConversationPluginCreatorDefinition`, but with additionnal properties not defined directly by the plugin creator.
+ * These properties are calculated by the `ConversationPluginService` at runtime.
+ */
+export type ConversationPluginDefinition<
+ TName extends string = string,
+ TOut = undefined,
+ TData extends ConversationPluginData = undefined,
+> = ConversationPluginCreatorDefinition & {
+ /**
+ * The unique name of the plugin.
+ */
+ name: TName;
+};
+
+/**
+ * The data stored by a plugin in the `pluginsData` property of the `ConversationModel` returned by the `Conversation.toJSON` method.
+ *
+ * @remarks
+ * Must be a serializable type.
+ */
+export type ConversationPluginData =
+ | string
+ | number
+ | boolean
+ | null
+ | undefined
+ | Date
+ | Array
+ | { [key: string]: ConversationPluginData };
+
+/**
+ * Utility type to extract a `ConversationPluginDefinition` from a `ConversationPlugin`.
+ *
+ * Useful for creating type guards from plugins.
+ *
+ * @example
+ * ```ts
+ * const myPlugin = createConversationPlugin("my-plugin", () => {
+ * return { ... };
+ * });
+ *
+ * type MyPluginDefinition = ConversationPluginDefinitionFromPlugin;
+ *
+ * const isMyPlugin = (plugin?: ConversationPluginDefinition): plugin is MyPluginDefinition
+ * => plugin?.name === myPluginName;
+ *
+ * ```
+ */
+export type ConversationPluginDefinitionFromPlugin<
+ T extends ConversationPlugin,
+> = T extends ConversationPlugin
+ ? ConversationPluginDefinition
+ : never;
diff --git a/packages/lib/src/utils/types/conversationPlugins.types.ts b/packages/lib/src/utils/types/conversationPlugins.types.ts
new file mode 100644
index 0000000..e82ee6b
--- /dev/null
+++ b/packages/lib/src/utils/types/conversationPlugins.types.ts
@@ -0,0 +1,29 @@
+import { ConversationPluginDefinition } from "./conversationPlugin.types.js";
+
+type PluginOutputFromNameNonRecursive<
+ TPlugin extends ConversationPluginDefinition,
+ N extends string,
+> = TPlugin extends ConversationPluginDefinition
+ ? O
+ : never;
+
+export type PluginOutputFromName<
+ TPlugin extends ConversationPluginDefinition,
+ N extends string,
+> = PluginOutputFromNameNonRecursive extends never
+ ? any
+ : PluginOutputFromNameNonRecursive;
+
+type PluginDataFromNameNonRecursive<
+ TPlugin extends ConversationPluginDefinition,
+ N extends string,
+> = TPlugin extends ConversationPluginDefinition
+ ? D
+ : never;
+
+export type PluginDataFromName<
+ TPlugin extends ConversationPluginDefinition,
+ N extends string,
+> = PluginDataFromNameNonRecursive extends never
+ ? any
+ : PluginDataFromNameNonRecursive;
diff --git a/packages/lib/src/utils/types/index.ts b/packages/lib/src/utils/types/index.ts
new file mode 100644
index 0000000..4831f67
--- /dev/null
+++ b/packages/lib/src/utils/types/index.ts
@@ -0,0 +1,9 @@
+export * from "./callableFunction.types.js";
+export * from "./chatCompletionService.types.js";
+export * from "./conversation.types.js";
+export * from "./conversationCallableFunctions.types.js";
+export * from "./conversationConfig.types.js";
+export * from "./conversationHistory.types.js";
+export * from "./conversationPlugin.types.js";
+export * from "./conversationPlugins.types.js";
+export * from "./message.types.js";
diff --git a/packages/lib/src/utils/types/message.types.ts b/packages/lib/src/utils/types/message.types.ts
new file mode 100644
index 0000000..bc3190e
--- /dev/null
+++ b/packages/lib/src/utils/types/message.types.ts
@@ -0,0 +1,236 @@
+import { Message } from "../../classes/Message.js";
+
+/**
+ * Supported values for the `role` property of a message.
+ */
+export type ChatCompletionRequestMessageRoleEnum =
+ | "user"
+ | "system"
+ | "assistant"
+ | "function";
+
+/**
+ * A message in OpenAI's chat format.
+ *
+ * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
+ */
+export interface CreateChatCompletionMessage {
+ /**
+ * The message role.
+ *
+ * @see {@link ChatCompletionRequestMessageRoleEnum}
+ */
+ role: Exclude;
+
+ /**
+ * The message content.
+ */
+ content: string;
+
+ function_call?: undefined;
+ name?: undefined;
+}
+
+export type CompletionMessage = Message & {
+ role: Exclude;
+ content: string;
+ functionCall: undefined;
+ name: undefined;
+};
+
+/**
+ * A function_call-related message in OpenAI's chat format.
+ *
+ * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
+ */
+export interface CreateChatCompletionFunctionCallMessage {
+ role: Extract;
+ content: null;
+ function_call: {
+ name: string;
+ arguments: string;
+ };
+
+ name?: undefined;
+}
+
+export type FunctionCallMessage = Message & {
+ role: "assistant";
+ content: null;
+ functionCall: {
+ name: string;
+ arguments: Record;
+ };
+};
+
+/**
+ * A function-related message in OpenAI's chat format.
+ *
+ * @see {@link https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages Create Chat Completion Request Body - messages}
+ */
+export interface CreateChatCompletionFunctionMessage {
+ role: Extract;
+ content: string;
+ name: string;
+
+ function_call?: undefined;
+}
+
+export type FunctionMessage = Message & {
+ role: "function";
+ name: string;
+ content: string;
+};
+
+/**
+ * Request body for a Create Moderation request, which classifies if text violates OpenAI's Content Policy.
+ *
+ * Endpoint: `POST https://api.openai.com/v1/moderations`
+ *
+ * @see {@link https://platform.openai.com/docs/api-reference/moderations/create Create Moderation Request Body}
+ */
+export interface CreateModerationRequest {
+ /**
+ * Your OpenAI API key.
+ */
+ apiKey: string;
+
+ /**
+ * The text to classify.
+ */
+ input: string;
+}
+
+/**
+ * Response body for a {@link CreateModerationRequest Create Moderation Request}.
+ */
+export interface CreateModerationResponse {
+ /**
+ * The ID of the request.
+ */
+ id: string;
+
+ /**
+ * The moderation model used to classify the text.
+ */
+ model: string;
+
+ /**
+ * The classification results.
+ */
+ results: [
+ {
+ /**
+ * Contains a dictionary of per-category binary usage policies violation flags.
+ * For each category, the value is `true` if the model flags the corresponding category as violated, `false` otherwise.
+ *
+ * @see {@link https://platform.openai.com/docs/guides/moderation/overview Moderation Categories}
+ */
+ categories: {
+ hate: boolean;
+ "hate/threatening": boolean;
+ harassment: boolean;
+ "harassment/threatening": boolean;
+ "self-harm": boolean;
+ "self-harm/intent": boolean;
+ "self-harm/instructions": boolean;
+ sexual: boolean;
+ "sexual/minors": boolean;
+ violence: boolean;
+ "violence/graphic": boolean;
+ };
+
+ /**
+ * Contains a dictionary of per-category raw scores output by the model, denoting the model's confidence that the input violates the OpenAI's policy for the category.
+ * The value is between 0 and 1, where higher values denote higher confidence. The scores should not be interpreted as probabilities.
+ *
+ * @see {@link https://platform.openai.com/docs/guides/moderation/overview Moderation Categories}
+ */
+ category_scores: {
+ hate: number;
+ "hate/threatening": number;
+ harassment: number;
+ "harassment/threatening": number;
+ "self-harm": number;
+ "self-harm/intent": number;
+ "self-harm/instructions": number;
+ sexual: number;
+ "sexual/minors": number;
+ violence: number;
+ "violence/graphic": number;
+ };
+
+ /**
+ * Set to `true` if the model classifies the content as violating OpenAI's usage policies, `false` otherwise.
+ */
+ flagged: boolean;
+ },
+ ];
+}
+
+/**
+ * Listener for when a {@link Message message}'s content is updated.
+ */
+export type MessageUpdateListener = (
+ /**
+ * The new content of the message.
+ */
+ content: string | null,
+
+ /**
+ * The {@link Message message} instance that was updated.
+ */
+ message: Message
+) => void;
+
+/**
+ * Listener for when a {@link Message message}'s streaming state is updated.
+ */
+export type MessageStreamingListener = (
+ /**
+ * The new streaming state of the message.
+ */
+ isStreaming: boolean,
+
+ /**
+ * The {@link Message message} instance that was updated.
+ */
+ message: Message
+) => void;
+
+/**
+ * Listener for when a {@link Message message} starts streaming.
+ */
+export type MessageStreamingStartListener = (
+ /**
+ * The {@link Message message} instance that started streaming.
+ */
+ message: Message
+) => void;
+
+/**
+ * Listener for when a {@link Message message} stops streaming.
+ */
+export type MessageStreamingStopListener = (
+ /**
+ * The {@link Message message} instance that stopped streaming.
+ */
+ message: Message
+) => void;
+
+export type MessageContentStreamListener = (
+ /**
+ * The new content of the message.
+ */
+ content: string | null,
+
+ /**
+ * The new streaming state of the message.
+ */
+ isStreaming: boolean,
+
+ /**
+ * The {@link Message message} instance that was updated during streaming.
+ */
+ message: Message
+) => void;
diff --git a/packages/nest/src/conversations/dtos/conversation.dto.ts b/packages/nest/src/conversations/dtos/conversation.dto.ts
deleted file mode 100644
index d0d1993..0000000
--- a/packages/nest/src/conversations/dtos/conversation.dto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { z } from "nestjs-zod/z";
-import { messageDtoSchema } from "./message.dto.js";
-import { createZodDto } from "nestjs-zod";
-import { conversationConfigDtoSchema } from "./conversationConfig.dto.js";
-
-export const conversationDtoSchema = z.object({
- id: z.string(),
- config: conversationConfigDtoSchema,
- messages: z.array(messageDtoSchema).min(0),
- cost: z.number(),
- size: z.number(),
- cumulativeCost: z.number(),
- cumulativeSize: z.number(),
-});
-
-export type ConversationDto = z.infer;
-
-export class ConversationDtoEntity extends createZodDto(
- conversationDtoSchema
-) {}
diff --git a/packages/nest/src/conversations/dtos/conversationConfig.dto.ts b/packages/nest/src/conversations/dtos/conversationConfig.dto.ts
deleted file mode 100644
index 2eda7b6..0000000
--- a/packages/nest/src/conversations/dtos/conversationConfig.dto.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
- DEFAULT_MODEL,
- DEFAULT_STREAM,
- DEFAULT_CONTEXT,
- DEFAULT_DRY,
- DEFAULT_DISABLEMODERATION,
-} from "gpt-turbo";
-import { createZodDto } from "nestjs-zod";
-import { z } from "nestjs-zod/z";
-
-export const conversationConfigDtoSchema = z.object({
- model: z.string().default(DEFAULT_MODEL),
- temperature: z.number().min(0).max(2).default(1),
- top_p: z.number().min(0).max(1).default(1),
- stream: z.boolean().default(DEFAULT_STREAM),
- stop: z.string().or(z.array(z.string()).max(4)).or(z.null()).default(null),
- max_tokens: z.number().min(1).max(2048).default(2048),
- presence_penalty: z.number().min(-2).max(2).default(0),
- frequency_penalty: z.number().min(-2).max(2).default(0),
- logit_bias: z.record(z.number(), z.number()).default({}),
- user: z.string().default(""),
-
- context: z.string().default(DEFAULT_CONTEXT),
- dry: z.boolean().default(DEFAULT_DRY),
- disableModeration: z
- .boolean()
- .or(z.literal("soft"))
- .default(DEFAULT_DISABLEMODERATION),
-});
-
-export type ConversationConfigDto = z.infer;
-
-export class ConversationConfigDtoEntity extends createZodDto(
- conversationConfigDtoSchema
-) {}
diff --git a/packages/nest/src/conversations/dtos/message.dto.ts b/packages/nest/src/conversations/dtos/message.dto.ts
deleted file mode 100644
index 6b1eebe..0000000
--- a/packages/nest/src/conversations/dtos/message.dto.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { z } from "nestjs-zod/z";
-import { uuidSchema } from "../../schemas/uuidSchema.js";
-import { createZodDto } from "nestjs-zod";
-
-export const messageDtoSchema = z.object({
- id: uuidSchema,
- content: z.string(),
- role: z.string(),
- cost: z.number(),
- size: z.number(),
- isFlagged: z.boolean(),
- isStreaming: z.boolean(),
- flags: z.array(z.string()).or(z.null()),
-});
-
-export type MessageDto = z.infer;
-
-export class MessageDtoEntity extends createZodDto(messageDtoSchema) {}
diff --git a/packages/nest/src/utils/messageToJson.ts b/packages/nest/src/utils/messageToJson.ts
deleted file mode 100644
index 41f10c9..0000000
--- a/packages/nest/src/utils/messageToJson.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Message } from "gpt-turbo";
-
-export default (message: Message) => {
- const {
- content,
- role,
- cost,
- flags,
- id,
- isFlagged,
- isStreaming,
- model,
- size,
- } = message;
-
- return {
- content,
- role,
- cost,
- flags,
- id,
- isFlagged,
- isStreaming,
- model,
- size,
- };
-};
diff --git a/packages/plugins/gpt-turbo-plugin-stats/.eslintrc.cjs b/packages/plugins/gpt-turbo-plugin-stats/.eslintrc.cjs
new file mode 100644
index 0000000..ad7d668
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/.eslintrc.cjs
@@ -0,0 +1,65 @@
+module.exports = {
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ project: "tsconfig.json",
+ tsconfigRootDir: __dirname,
+ sourceType: "module",
+ },
+ plugins: ["@typescript-eslint/eslint-plugin"],
+ extends: [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:prettier/recommended",
+ ],
+ root: true,
+ env: {
+ node: true,
+ },
+ ignorePatterns: [".eslintrc.cjs"],
+ rules: {
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ ignoreRestSiblings: true,
+ },
+ ],
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-empty-interface": "off",
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/await-thenable": "warn",
+ "max-classes-per-file": "off",
+ "no-useless-constructor": "off",
+ "class-methods-use-this": "off",
+ eqeqeq: ["warn", "always"],
+ "lines-between-class-members": "off",
+ "no-console": [
+ "warn",
+ {
+ allow: ["warn", "error", "info"],
+ },
+ ],
+ "no-continue": "off",
+ "no-plusplus": "off",
+ "no-param-reassign": [
+ "warn",
+ {
+ props: false,
+ },
+ ],
+ "no-restricted-exports": "off",
+ "no-unused-vars": "off",
+ "spaced-comment": [
+ "warn",
+ "always",
+ {
+ markers: ["/"],
+ },
+ ],
+ "arrow-body-style": "off",
+
+ "prettier/prettier": ["warn"],
+ },
+};
diff --git a/packages/plugins/gpt-turbo-plugin-stats/.gitignore b/packages/plugins/gpt-turbo-plugin-stats/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/packages/plugins/gpt-turbo-plugin-stats/.prettierrc b/packages/plugins/gpt-turbo-plugin-stats/.prettierrc
new file mode 100644
index 0000000..f73cab1
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "tabWidth": 4,
+ "useTabs": false,
+ "arrowParens": "always",
+ "semi": true,
+ "quoteProps": "as-needed",
+ "jsxSingleQuote": false,
+ "trailingComma": "es5",
+ "endOfLine": "lf",
+ "printWidth": 80,
+ "singleQuote": false
+}
diff --git a/packages/plugins/gpt-turbo-plugin-stats/LICENSE.md b/packages/plugins/gpt-turbo-plugin-stats/LICENSE.md
new file mode 100644
index 0000000..ca6b544
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Tristan Chin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/plugins/gpt-turbo-plugin-stats/README.md b/packages/plugins/gpt-turbo-plugin-stats/README.md
new file mode 100644
index 0000000..705d6d2
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/README.md
@@ -0,0 +1,93 @@
+# GPT Turbo - Plugin - Stats
+
+
+
+ [![npm i gpt-turbo-plugin-stats](https://img.shields.io/npm/v/gpt-turbo-plugin-stats?color=brightgreen&label=gpt-turbo-plugin-stats&logo=npm)](https://www.npmjs.com/package/gpt-turbo-plugin-stats)
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+
+
+A GPT Turbo plugin to augment conversations with usage statistics:
+
+- **Size**: The size (in tokens) of the conversation. This includes the system (context) message. It's an indicator of the **minimum** amount of tokens that will be sent on the next Chat Completion call.
+- **Cumulative Size**: The cumulative size (in tokens) of the conversation. It's the amount of tokens that have been send/received since the start of the conversation. This is the amount of tokens you've been charged for.
+- **Cost**: The cost (in $USD) of the conversation. It's an indicator of the **minimum** amount of money that will be charged on the next Chat Completion call.
+- **Cumulative cost**: The cumulative cost (in $USD) of the conversation. It's the amount of money that has been charged since the start of the conversation.
+
+## Disclaimer
+
+The statistics are based on the tokens given by a third party library: [`gpt-tokenizer`](https://www.npmjs.com/package/gpt-tokenizer). While its tokenization is usually accurate, it's not guaranteed to be 100% accurate. Stats may differ from OpenAI's bill, but usually not by much.
+
+## Installation
+
+[`gpt-turbo`](https://www.npmjs.com/package/gpt-turbo) is required to use this plugin and is marked as a peer dependency. You can install it with:
+
+```bash
+npm i gpt-turbo
+```
+
+Then install this plugin with:
+
+```bash
+npm i gpt-turbo-plugin-stats
+```
+
+*Note: since this plugin is developed alongside the library, both versions **must** be equal. Your installation will fail otherwise.*
+
+## Usage
+
+Here are some examples of how to inject the plugin (`statsPlugin`) in a `Conversation` and how to retrieve the output `ConversationStats` instance (`stats`).
+
+```ts
+import { Conversation } from "gpt-turbo";
+import statsPlugin, { statsPluginName } from "gpt-turbo-plugin-stats";
+
+// As a per-conversation plugin
+const conversation = new Conversation({
+ plugins: [statsPlugin]
+});
+const stats = conversation.plugins.getPluginOutput(statsPluginName); // ConversationStats
+
+// As a global plugin
+const globalPlugins = [statsPlugin];
+declare module "gpt-turbo" {
+ interface ConversationGlobalPluginsOverride {
+ globalPlugins: typeof globalPlugins;
+ }
+}
+Conversation.globalPlugins = globalPlugins;
+const conversation = new Conversation();
+const stats = conversation.plugins.getPluginOutput(statsPluginName); // ConversationStats
+```
+
+If you're getting the plugin dynamically (i.e. not from a string literal or the `statsPluginName` constant), you can use the `isStatsPlugin` type guard to properly type the plugin:
+
+```ts
+import { Conversation } from "gpt-turbo";
+import statsPlugin, { isStatsPlugin } from "gpt-turbo-plugin-stats";
+
+const conversation = new Conversation({
+ plugins: [statsPlugin]
+});
+
+const plugin = conversation.plugins.getPlugin("gpt-turbo-plugin" + "-stats");
+plugin.out; // any
+
+if (isStatsPlugin(plugin)) {
+ const stats = plugin.out; // ConversationStats
+}
+```
+
+With the `ConversationStats` instance, you can retrieve the stats:
+
+```ts
+console.log(stats.size, stats.cumulativeSize, stats.cost, stats.cumulativeCost);
+```
+
+Finally, you can subscribe to events to get notified when the stats change:
+
+```ts
+const unsubscribe = stats.onStatsUpdate(() => {
+ console.log(stats.size, stats.cumulativeSize, stats.cost, stats.cumulativeCost);
+});
+setTimeout(unsubscribe, 1000); // Unsubscribe after 1 second
+```
\ No newline at end of file
diff --git a/packages/plugins/gpt-turbo-plugin-stats/package.json b/packages/plugins/gpt-turbo-plugin-stats/package.json
new file mode 100644
index 0000000..cfb2d93
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "gpt-turbo-plugin-stats",
+ "version": "4.5.0",
+ "description": "GPT Turbo plugin that calculates your conversation size and cost",
+ "main": "dist/index.js",
+ "type": "module",
+ "scripts": {
+ "lint": "eslint --ext .ts src",
+ "lint:strict": "npm run lint -- --max-warnings 0",
+ "lint:fix": "npm run lint -- --fix",
+ "tscheck": "tsc --noEmit",
+ "build": "npm run lint:strict && rimraf dist && tsc -p tsconfig.build.json && copyfiles -u 1 -e \"src/**/*.ts\" \"src/**/*\" dist"
+ },
+ "keywords": [
+ "openai",
+ "chatgpt",
+ "chat",
+ "gpt",
+ "gpt3",
+ "gpt-3",
+ "gpt3.5",
+ "gpt-3.5",
+ "gpt4",
+ "gpt-4",
+ "completion",
+ "chatcompletion",
+ "conversation",
+ "conversation ai",
+ "ai",
+ "ml",
+ "bot",
+ "chatbot",
+ "gpt-turbo",
+ "gpt-turbo-plugin",
+ "tokenizer",
+ "bpe"
+ ],
+ "author": "Tristan Chin ",
+ "homepage": "https://github.com/maxijonson/gpt-turbo/tree/develop/packages/plugins/gpt-turbo-plugin-stats#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/maxijonson/gpt-turbo.git"
+ },
+ "bugs": {
+ "url": "https://github.com/maxijonson/gpt-turbo/issues"
+ },
+ "license": "MIT",
+ "directories": {
+ "lib": "dist"
+ },
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.54.0",
+ "@typescript-eslint/parser": "^5.54.0",
+ "copyfiles": "^2.4.1",
+ "eslint": "^8.35.0",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "prettier": "^2.8.4",
+ "rimraf": "^4.2.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.9.5"
+ },
+ "peerDependencies": {
+ "gpt-turbo": "4.5.0"
+ },
+ "dependencies": {
+ "gpt-tokenizer": "^2.1.1"
+ }
+}
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/classes/ConversationStats.ts b/packages/plugins/gpt-turbo-plugin-stats/src/classes/ConversationStats.ts
new file mode 100644
index 0000000..5390060
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/classes/ConversationStats.ts
@@ -0,0 +1,194 @@
+import { ConversationHistory, EventManager, Message } from "gpt-turbo";
+import { MessageStats } from "./MessageStats.js";
+import { StatsPluginData } from "../utils/types/index.js";
+import { ConversationStatsStatsUpdateListener } from "../utils/types/conversationStats.types.js";
+
+export class ConversationStats {
+ private _cumulativeSize = 0;
+ private _cumulativeCost = 0;
+
+ private readonly messageStats = new Map();
+ private readonly statsUpdateEvents =
+ new EventManager();
+
+ constructor(
+ private readonly history: ConversationHistory,
+ options?: StatsPluginData
+ ) {
+ this._cumulativeSize = options?.cumulativeSize || 0;
+ this._cumulativeCost = options?.cumulativeCost || 0;
+
+ this.history
+ .getMessages(true)
+ .forEach((message) => this.createMessageStats(message));
+ }
+
+ /**
+ * Gets the stats for a message in the conversation history.
+ *
+ * @param id The message id to get stats for.
+ * @returns The stats for the message with the given id.
+ * @throws If the message with the given id is not found in the conversation history.
+ */
+ public getMessageStats(id: string) {
+ const messageStats = this.messageStats.get(id);
+ if (messageStats) return messageStats;
+
+ const message = this.getMessageById(id);
+ if (!message) {
+ throw new Error(`Message ${id} not found`);
+ }
+ return this.createMessageStats(message);
+ }
+
+ /**
+ * Shortcut for `getMessageStats(id).size`.
+ */
+ public getMessageSize(id: string) {
+ return this.getMessageStats(id).size;
+ }
+
+ /**
+ * Shortcut for `getMessageStats(id).cost`.
+ */
+ public getMessageCost(id: string) {
+ return this.getMessageStats(id).cost;
+ }
+
+ /**
+ * Increments the cumulative size and cost of the conversation by the size and cost of the given message and the current conversation size and cost.
+ *
+ * @internal
+ * Only meant to be used by gpt-turbo.
+ */
+ public onChatCompletion(message: Message) {
+ // FIXME: Find out how size/cost is calculated for function calls/messages
+ if (!message.isCompletion()) return;
+ const messageStats = this.createMessageStats(message);
+
+ if (!message.isStreaming && message.content) {
+ this.cumulativeSize += this.size + messageStats.size;
+ this.cumulativeCost += this.cost + messageStats.cost;
+ return;
+ }
+
+ this.cumulativeSize += this.size;
+ this.cumulativeCost += this.cost;
+
+ const offMessageSizeUpdate = messageStats.onUpdateSize((_, delta) => {
+ this.cumulativeSize += delta;
+ });
+ const offMessageCostUpdate = messageStats.onUpdateCost((_, delta) => {
+ this.cumulativeCost += delta;
+ });
+
+ message.onceStreamingStop(() => {
+ offMessageSizeUpdate();
+ offMessageCostUpdate();
+ });
+ }
+
+ /**
+ * Gets the plugin data for the `Conversation` instance.
+ *
+ * @internal
+ * Only meant to be used by gpt-turbo.
+ */
+ public getPluginData(): StatsPluginData {
+ return {
+ cumulativeSize: this.cumulativeSize,
+ cumulativeCost: this.cumulativeCost,
+ };
+ }
+
+ /**
+ * The current amount of tokens that the conversation contains.
+ * This is the minimum amount of tokens that you will be charged for on your next prompt.
+ *
+ * @remarks
+ * This is an estimate using the `gpt-tokenizer` library. Stats may vary with your actual usage of the API.
+ */
+ public get size() {
+ return this.history
+ .getMessages(true)
+ .reduce((size, m) => size + this.getMessageSize(m.id), 0);
+ }
+
+ /**
+ * The cumulative amount of tokens that were sent/received to/from the OpenAI API.
+ *
+ * @remarks
+ * This is an estimate using the `gpt-tokenizer` library. Stats may vary with your actual usage of the API.
+ */
+ public get cumulativeSize() {
+ return this._cumulativeSize;
+ }
+
+ private set cumulativeSize(value) {
+ this._cumulativeSize = value;
+ this.statsUpdateEvents.emit();
+ }
+
+ /**
+ * The current cost of the conversation.
+ * This is the minimum amount of $USD that you will be charged for on your next prompt.
+ *
+ * @remarks
+ * This is an estimate using the `gpt-tokenizer` library. Stats may vary with your actual usage of the API.
+ */
+ public get cost() {
+ return this.history
+ .getMessages(true)
+ .reduce((cost, m) => cost + this.getMessageCost(m.id), 0);
+ }
+
+ /**
+ * The cumulative cost of the conversation.
+ * This is the total amount of $USD that you have been charged for the conversation so far.
+ *
+ * @remarks
+ * This is an estimate using the `gpt-tokenizer` library. Stats may vary with your actual usage of the API.
+ */
+ public get cumulativeCost() {
+ return this._cumulativeCost;
+ }
+
+ private set cumulativeCost(value) {
+ this._cumulativeCost = value;
+ this.statsUpdateEvents.emit();
+ }
+
+ private createMessageStats(message: Message) {
+ const existingMessageStats = this.messageStats.get(message.id);
+ if (existingMessageStats) return existingMessageStats;
+
+ const messageStats = new MessageStats(message);
+ this.messageStats.set(message.id, messageStats);
+
+ messageStats.onUpdateCost(() => {
+ this.statsUpdateEvents.emit();
+ });
+
+ messageStats.onUpdateSize(() => {
+ this.statsUpdateEvents.emit();
+ });
+
+ return messageStats;
+ }
+
+ private getMessageById(id: string) {
+ return this.history.getMessages(true).find((m) => m.id === id);
+ }
+
+ public onStatsUpdate(listener: ConversationStatsStatsUpdateListener) {
+ return this.statsUpdateEvents.addListener(listener);
+ }
+
+ public onceStatsUpdate(listener: ConversationStatsStatsUpdateListener) {
+ return this.statsUpdateEvents.once(listener);
+ }
+
+ public offStatsUpdate(listener: ConversationStatsStatsUpdateListener) {
+ return this.statsUpdateEvents.removeListener(listener);
+ }
+}
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/classes/MessageStats.ts b/packages/plugins/gpt-turbo-plugin-stats/src/classes/MessageStats.ts
new file mode 100644
index 0000000..516d882
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/classes/MessageStats.ts
@@ -0,0 +1,78 @@
+import { Message, EventManager } from "gpt-turbo";
+import getMessageSize from "../utils/getMessageSize.js";
+import getMessageCost from "../utils/getMessageCost.js";
+import {
+ MessageCostUpdateListener,
+ MessageSizeUpdateListener,
+} from "../utils/types/messageStats.types.js";
+
+export class MessageStats {
+ public readonly id: string;
+ private _size = 0;
+ private _cost = 0;
+ private readonly sizeUpdateEvents =
+ new EventManager();
+ private readonly costUpdateEvents =
+ new EventManager();
+
+ constructor(private readonly message: Message) {
+ this.id = message.id;
+ this.update();
+ message.onUpdate(() => this.update());
+ }
+
+ public get size() {
+ return this._size;
+ }
+
+ private set size(size: number) {
+ const delta = size - this._size;
+ this._size = size;
+ this.sizeUpdateEvents.emit(this._size, delta);
+ }
+
+ public get cost() {
+ return this._cost;
+ }
+
+ private set cost(cost: number) {
+ const delta = cost - this._cost;
+ this._cost = cost;
+ this.costUpdateEvents.emit(this._cost, delta);
+ }
+
+ private update() {
+ // FIXME: Find out how size/cost is calculated for function calls/messages
+ if (!this.message.isCompletion()) return;
+ this.size = getMessageSize(this.message.content);
+ this.cost = getMessageCost(
+ this.size,
+ this.message.model,
+ this.message.role === "assistant" ? "completion" : "prompt"
+ );
+ }
+
+ public onUpdateSize(listener: MessageSizeUpdateListener) {
+ return this.sizeUpdateEvents.addListener(listener);
+ }
+
+ public onceUpdateSize(listener: MessageSizeUpdateListener) {
+ return this.sizeUpdateEvents.once(listener);
+ }
+
+ public offUpdateSize(listener: MessageSizeUpdateListener) {
+ return this.sizeUpdateEvents.removeListener(listener);
+ }
+
+ public onUpdateCost(listener: MessageCostUpdateListener) {
+ return this.costUpdateEvents.addListener(listener);
+ }
+
+ public onceUpdateCost(listener: MessageCostUpdateListener) {
+ return this.costUpdateEvents.once(listener);
+ }
+
+ public offUpdateCost(listener: MessageCostUpdateListener) {
+ return this.costUpdateEvents.removeListener(listener);
+ }
+}
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/classes/index.ts b/packages/plugins/gpt-turbo-plugin-stats/src/classes/index.ts
new file mode 100644
index 0000000..d4c4fc7
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/classes/index.ts
@@ -0,0 +1,2 @@
+export * from "./ConversationStats.js";
+export * from "./MessageStats.js";
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/config/constants.ts b/packages/plugins/gpt-turbo-plugin-stats/src/config/constants.ts
new file mode 100644
index 0000000..b02e0ce
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/config/constants.ts
@@ -0,0 +1,22 @@
+export const PRICING_TABLE = {
+ unknown: {
+ prompt: 0,
+ completion: 0,
+ },
+ "3.5": {
+ prompt: 0.0000015,
+ completion: 0.000002,
+ },
+ "3.5-16k": {
+ prompt: 0.000003,
+ completion: 0.000004,
+ },
+ "4": {
+ prompt: 0.00003,
+ completion: 0.00006,
+ },
+ "4-32k": {
+ prompt: 0.00006,
+ completion: 0.00012,
+ },
+};
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/config/index.ts b/packages/plugins/gpt-turbo-plugin-stats/src/config/index.ts
new file mode 100644
index 0000000..548d69e
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/config/index.ts
@@ -0,0 +1 @@
+export * from "./constants.js";
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/index.ts b/packages/plugins/gpt-turbo-plugin-stats/src/index.ts
new file mode 100644
index 0000000..56b132c
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/index.ts
@@ -0,0 +1,74 @@
+import {
+ ConversationPluginDefinition,
+ ConversationPluginDefinitionFromPlugin,
+ createConversationPlugin,
+} from "gpt-turbo";
+import { ConversationStats } from "./classes/ConversationStats.js";
+import { StatsPluginData } from "./utils/types/index.js";
+
+/**
+ * The name of the `statsPlugin`.
+ */
+export const statsPluginName = "gpt-turbo-plugin-stats" as const;
+
+/**
+ * The type of the plugin definition created by the `statsPlugin`.
+ */
+export type StatsPluginDefinition = ConversationPluginDefinitionFromPlugin<
+ typeof statsPlugin
+>;
+
+/**
+ * Type guard for the `statsPlugin`. Useful when using the `Conversation.getPlugin` method with a dynamic plugin name, preserving type safety.
+ *
+ * @example
+ * ```ts
+ * ["some-plugin", "some-other-plugin", "gpt-turbo-plugin-stats"].forEach(
+ * (pluginName) => {
+ * const plugin = conversation.plugins.safeGetPlugin(pluginName);
+ * if (isStatsPlugin(plugin)) {
+ * // plugin is now typed as StatsPlugin
+ * plugin.out.getMessageStats(...);
+ * }
+ * }
+ * );
+ * ```
+ *
+ * @param plugin The plugin to check.
+ * @returns Whether the plugin is a `statsPlugin`.
+ */
+export const isStatsPlugin = (
+ plugin?: ConversationPluginDefinition
+): plugin is StatsPluginDefinition => plugin?.name === statsPluginName;
+
+/**
+ * GPT Turbo plugin that calculates and stores statistics about the conversation.
+ *
+ * @example
+ * ```ts
+ * import { Conversation } from "gpt-turbo";
+ * import stats from "gpt-turbo-plugin-stats";
+ *
+ * const conversation = new Conversation({
+ * plugins: [stats],
+ * });
+ * ```
+ */
+const statsPlugin = createConversationPlugin(
+ statsPluginName,
+ ({ history }, pluginData?: StatsPluginData) => {
+ const conversationStats = new ConversationStats(history, pluginData);
+
+ return {
+ getPluginData: () => conversationStats.getPluginData(),
+ onChatCompletion: (...args) =>
+ conversationStats.onChatCompletion(...args),
+ out: conversationStats,
+ };
+ }
+);
+
+export * from "./utils/index.js";
+export * from "./classes/index.js";
+export * from "./config/index.js";
+export default statsPlugin;
diff --git a/packages/lib/src/utils/getMessageCost.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageCost.ts
similarity index 100%
rename from packages/lib/src/utils/getMessageCost.ts
rename to packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageCost.ts
diff --git a/packages/lib/src/utils/getMessageSize.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageSize.ts
similarity index 81%
rename from packages/lib/src/utils/getMessageSize.ts
rename to packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageSize.ts
index 5e498a2..5b07798 100644
--- a/packages/lib/src/utils/getMessageSize.ts
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageSize.ts
@@ -1,4 +1,4 @@
-import { encode } from "gpt-token-utils";
+import { encode } from "gpt-tokenizer";
/**
* Returns the size of a message in tokens.
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageTokens.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageTokens.ts
new file mode 100644
index 0000000..a617560
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getMessageTokens.ts
@@ -0,0 +1,10 @@
+import { encode } from "gpt-tokenizer";
+
+/**
+ * Returns an array of tokens from a given `message`.
+ *
+ * @param message The message to get the tokens from.
+ */
+export default (message: string) => {
+ return encode(message);
+};
diff --git a/packages/lib/src/utils/getPricing.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/getPricing.ts
similarity index 100%
rename from packages/lib/src/utils/getPricing.ts
rename to packages/plugins/gpt-turbo-plugin-stats/src/utils/getPricing.ts
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/index.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/index.ts
new file mode 100644
index 0000000..285e824
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/index.ts
@@ -0,0 +1,5 @@
+export { default as getMessageCost } from "./getMessageCost.js";
+export { default as getMessageSize } from "./getMessageSize.js";
+export { default as getMessageTokens } from "./getMessageTokens.js";
+export { default as getPricing } from "./getPricing.js";
+export * from "./types/index.js";
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/conversationStats.types.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/conversationStats.types.ts
new file mode 100644
index 0000000..a2fdbd5
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/conversationStats.types.ts
@@ -0,0 +1 @@
+export type ConversationStatsStatsUpdateListener = () => void;
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/index.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/index.ts
new file mode 100644
index 0000000..7493740
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/index.ts
@@ -0,0 +1,2 @@
+export * from "./messageStats.types.js";
+export * from "./plugin.types.js";
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/messageStats.types.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/messageStats.types.ts
new file mode 100644
index 0000000..324b47c
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/messageStats.types.ts
@@ -0,0 +1,2 @@
+export type MessageSizeUpdateListener = (size: number, delta: number) => void;
+export type MessageCostUpdateListener = (cost: number, delta: number) => void;
diff --git a/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/plugin.types.ts b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/plugin.types.ts
new file mode 100644
index 0000000..a6ac472
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/src/utils/types/plugin.types.ts
@@ -0,0 +1,4 @@
+export type StatsPluginData = {
+ cumulativeSize: number;
+ cumulativeCost: number;
+};
diff --git a/packages/plugins/gpt-turbo-plugin-stats/tsconfig.build.json b/packages/plugins/gpt-turbo-plugin-stats/tsconfig.build.json
new file mode 100644
index 0000000..856f27d
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/plugins/gpt-turbo-plugin-stats/tsconfig.json b/packages/plugins/gpt-turbo-plugin-stats/tsconfig.json
new file mode 100644
index 0000000..8edb0d2
--- /dev/null
+++ b/packages/plugins/gpt-turbo-plugin-stats/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "module": "Node16",
+ "moduleResolution": "node16",
+ "declaration": true,
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "baseUrl": "./src",
+ "incremental": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "strictNullChecks": true,
+ "noImplicitAny": true,
+ "strictBindCallApply": false,
+ "forceConsistentCasingInFileNames": false,
+ "noFallthroughCasesInSwitch": false,
+ "resolveJsonModule": true
+ }
+}
diff --git a/packages/web/src/components/error-handling/AppCatcher.tsx b/packages/web/src/components/error-handling/AppCatcher.tsx
deleted file mode 100644
index e15ffb1..0000000
--- a/packages/web/src/components/error-handling/AppCatcher.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import {
- Anchor,
- Button,
- Container,
- Group,
- Kbd,
- List,
- Modal,
- ScrollArea,
- Stack,
- Text,
- Title,
-} from "@mantine/core";
-import React, { ErrorInfo } from "react";
-import { BiBug, BiRefresh, BiTrash } from "react-icons/bi";
-
-export interface AppCatcherProps {
- children?: React.ReactNode;
-}
-
-export interface AppCatcherState {
- hasError: boolean;
-}
-
-class AppCatcher extends React.Component {
- constructor(props: AppCatcherProps) {
- super(props);
- this.state = { hasError: false };
- }
-
- static getDerivedStateFromError(_error: unknown) {
- return { hasError: true };
- }
-
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
- console.error(error, errorInfo);
- }
-
- render() {
- const { hasError } = this.state;
- const { children } = this.props;
-
- if (!hasError) {
- return children;
- }
-
- return (
- {}}
- centered
- title={
-
- Fatal error
-
- }
- color="red"
- size="xl"
- scrollAreaComponent={ScrollArea.Autosize}
- >
-
-
-
- The application encountered a fatal error. Try to
- reload the page. If the issue persists, try opening
- the app in a private tab:
-
-
- If the app now works in a private tab, you
- might have invalid or outdated local storage
- data in your browser. Consider clearing your
- local storage data{" "}
-
- which will clear all your settings and
- conversations
-
-
-
- If the app still does not work in a private
- tab, you might have encountered a bug. You
- may report it on the project's{" "}
-
- GitHub issues page
-
- . In order to help us resolve this issue,
- please include a screenshot (or copy paste
- the output) of the error in the console (
- F12).
-
-
-
-
-
- }
- onClick={() =>
- window.open(
- "https://github.com/maxijonson/gpt-turbo/issues/new?assignees=&labels=needs-triage&projects=&template=bug_report.yml&title=%5BBug%5D%3A",
- "_blank"
- )
- }
- >
- Report issue on GitHub
-
- }
- onClick={() => {
- localStorage.clear();
- window.location.reload();
- }}
- >
- Clear local storage
-
- }
- onClick={() => window.location.reload()}
- >
- Reload
-
-
-
-
- );
- }
-}
-
-export default AppCatcher;
diff --git a/packages/web/src/store/actions/conversations/duplicateConversation.ts b/packages/web/src/store/actions/conversations/duplicateConversation.ts
deleted file mode 100644
index 214c141..0000000
--- a/packages/web/src/store/actions/conversations/duplicateConversation.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Conversation } from "gpt-turbo";
-import { createAction } from "../createAction";
-import { addConversation } from "./addConversation";
-import { setConversationName } from "./setConversationName";
-
-export const duplicateConversation = createAction(
- async ({ get }, id: string) => {
- const { conversations, persistedConversationIds, conversationNames } =
- get();
- const conversation = conversations.find((c) => c.id === id);
-
- if (!conversation) {
- throw new Error(`Conversation with id ${id} not found`);
- }
-
- const { id: _, ...json } = conversation.toJSON();
- const copy = await Conversation.fromJSON(json);
-
- const newConversation = addConversation(
- copy,
- undefined,
- undefined,
- persistedConversationIds.includes(id)
- );
-
- const name = conversationNames.get(id);
- if (name) {
- setConversationName(newConversation.id, name);
- }
- },
- "duplicateConversation"
-);
diff --git a/packages/web/src/store/persist/migrateOldData.ts b/packages/web/src/store/persist/migrateOldData.ts
deleted file mode 100644
index 754157a..0000000
--- a/packages/web/src/store/persist/migrateOldData.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { notifications } from "@mantine/notifications";
-import { CallableFunction, Conversation } from "gpt-turbo";
-
-const notify = (
- title: string,
- message: string,
- color = "blue",
- autoClose = true
-) => {
- setTimeout(() => {
- notifications.show({
- color,
- autoClose,
- title,
- message,
- });
- }, 100);
-};
-
-// TODO: Remove this after a while. This is to migrate users from the old persistence system to the new one.
-// Not EVERYTHING is migrated, but the most important things are.
-export const migrateOldData = async () => {
- // Wait a second to make sure the store is initialized
- await new Promise((resolve) => setTimeout(resolve, 1000));
- const { addConversation } = await import(
- "../actions/conversations/addConversation"
- );
- const { setConversationName } = await import(
- "../actions/conversations/setConversationName"
- );
- const { setConversationLastEdit } = await import(
- "../actions/conversations/setConversationLastEdit"
- );
- const { addCallableFunction } = await import(
- "../actions/callableFunctions/addCallableFunction"
- );
- const { saveContext } = await import(
- "../actions/savedContexts/saveContext"
- );
- const { savePrompt } = await import("../actions/savedPrompts/savePrompt");
- const { setDefaultSettings } = await import(
- "../actions/defaultConversationSettings/setDefaultSettings"
- );
- const { addPersistedConversationId } = await import(
- "../actions/persistence/addPersistedConversationId"
- );
-
- const oldPersistence = localStorage.getItem("gpt-turbo-persistence");
- const oldSettings = localStorage.getItem("gpt-turbo-settings");
-
- if (oldPersistence) {
- let deleteAfter = true;
- try {
- const { conversations, functions, contexts, prompts } =
- JSON.parse(oldPersistence);
- for (const { name, lastEdited, ...conversation } of conversations) {
- try {
- const c = await Conversation.fromJSON(conversation);
- addConversation(c);
- setConversationName(c.id, name);
- setConversationLastEdit(c.id, lastEdited);
- addPersistedConversationId(c.id);
- } catch (e) {
- deleteAfter = false;
- console.error(e);
- notify(
- "Conversation Migration Failed",
- "Failed to migrate one of your conversations to the new storage system.",
- "red",
- false
- );
- }
- }
-
- for (const { displayName, code, ...fn } of functions) {
- try {
- const f = CallableFunction.fromJSON(fn);
- addCallableFunction(f, displayName, code);
- } catch (e) {
- deleteAfter = false;
- console.error(e);
- notify(
- "Callable Function Migration Failed",
- "Failed to migrate one of your callable functions to the new storage system.",
- "red",
- false
- );
- }
- }
-
- for (const { name, value } of contexts) {
- saveContext(name, value);
- }
-
- for (const { name, value } of prompts) {
- savePrompt(name, value);
- }
-
- if (deleteAfter) {
- localStorage.removeItem("gpt-turbo-persistence");
- notify(
- "Persistence Migration Complete",
- "Your old saved data has been migrated to the new storage system!",
- "green"
- );
- } else {
- notify(
- "Persistence Migration Partially Complete",
- "Only some of your old saved data has been migrated to the new storage system. Please check the console for more details.",
- "yellow"
- );
- }
- } catch (e) {
- console.error(e);
- notify(
- "Persistence Migration Failed",
- "Failed to migrate your old saved data to the new storage system.",
- "red",
- false
- );
- }
- }
-
- if (oldSettings) {
- try {
- setDefaultSettings(JSON.parse(oldSettings));
- localStorage.removeItem("gpt-turbo-settings");
- notify(
- "Settings Migration Complete",
- "Your old settings have been migrated to the new storage system!",
- "green"
- );
- } catch (e) {
- console.error(e);
- notify(
- "Settings Migration Failed",
- "Failed to migrate your old settings to the new storage system.",
- "red",
- false
- );
- }
- }
-};