From e1cc2175708811263f789483fec2f9f51f677460 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sat, 11 Jan 2025 02:49:05 +0900 Subject: [PATCH] Multiple applications support by `@nestia/agent`. From now on, `@nestia/agent` supports multiple swagger files and typescript classes at the same time. --- packages/agent/.eslintrc.cjs | 28 - packages/agent/build/prompt.ts | 6 +- packages/agent/eslint.config.mjs | 21 + packages/agent/package.json | 13 +- packages/agent/src/NestiaAgent.ts | 229 ++++++++ packages/agent/src/NestiaChatAgent.ts | 236 --------- packages/agent/src/chatgpt/ChatGptAgent.ts | 220 ++------ .../src/chatgpt/ChatGptCancelFunctionAgent.ts | 286 +++++----- .../chatgpt/ChatGptDescribeFunctionAgent.ts | 78 ++- .../chatgpt/ChatGptExecuteFunctionAgent.ts | 497 ++++++++++-------- .../src/chatgpt/ChatGptHistoryDecoder.ts | 33 +- .../chatgpt/ChatGptInitializeFunctionAgent.ts | 116 ++-- .../src/chatgpt/ChatGptSelectFunctionAgent.ts | 315 ++++++----- .../functional/createHttpLlmApplication.ts | 35 ++ ...gentConstant.ts => NestiaAgentConstant.ts} | 2 +- ...egator.ts => NestiaAgentCostAggregator.ts} | 6 +- ...tPrompt.ts => NestiaAgentDefaultPrompt.ts} | 18 +- .../internal/NestiaAgentOperationComposer.ts | 82 +++ .../src/internal/NestiaAgentPromptFactory.ts | 30 ++ .../internal/NestiaAgentPromptTransformer.ts | 82 +++ .../src/internal/NestiaAgentSystemPrompt.ts | 14 + .../internal/NestiaChatAgentSystemPrompt.ts | 8 - packages/agent/src/internal/__map_take.ts | 15 + packages/agent/src/module.ts | 18 +- .../agent/src/structures/IChatGptService.ts | 21 - .../src/structures/INestiaAgentConfig.ts | 88 ++++ .../src/structures/INestiaAgentContext.ts | 28 + .../src/structures/INestiaAgentController.ts | 35 ++ .../agent/src/structures/INestiaAgentEvent.ts | 184 +++++++ .../src/structures/INestiaAgentOperation.ts | 43 ++ .../INestiaAgentOperationCollection.ts | 8 + .../INestiaAgentOperationSelection.ts | 61 +++ .../src/structures/INestiaAgentPrompt.ts | 155 ++++++ .../agent/src/structures/INestiaAgentProps.ts | 13 + .../src/structures/INestiaAgentProvider.ts | 31 ++ .../structures/INestiaAgentSystemPrompt.ts | 11 + ...okenUsage.ts => INestiaAgentTokenUsage.ts} | 8 +- .../agent/src/structures/INestiaChatAgent.ts | 21 - .../agent/src/structures/INestiaChatEvent.ts | 125 ----- .../INestiaChatFunctionSelection.ts | 21 - .../agent/src/structures/INestiaChatPrompt.ts | 125 ----- .../agent/src/typings/NestiaAgentSource.ts | 6 + packages/agent/test/cli.ts | 51 +- packages/agent/test/index.ts | 17 +- packages/chat/.gitignore | 25 + packages/chat/LICENSE | 21 + packages/chat/README.md | 87 +++ packages/chat/eslint.config.js | 28 + packages/chat/package.json | 78 +++ packages/chat/rollup.config.js | 29 + packages/chat/src/NestiaChatApplication.tsx | 11 + packages/chat/src/NestiaChatUploader.tsx | 10 + packages/chat/src/index.ts | 0 packages/chat/src/main.tsx | 0 packages/chat/src/movies/NestiaChatMovie.tsx | 41 ++ .../prompts/NestiaChatDescribePromptMovie.tsx | 8 + .../prompts/NestiaChatExecutePromptMovie.tsx | 7 + .../movies/prompts/NestiaChatPromptMovie.tsx | 8 + .../prompts/NestiaChatSelectPromptMovie.tsx | 8 + .../prompts/NestiaChatTextPromptMovie.tsx | 8 + .../sides/NestiaChatTokenUsageMovie.tsx | 65 +++ packages/chat/src/vite-env.d.ts | 1 + packages/chat/tsconfig.app.json | 25 + packages/chat/tsconfig.app.tsbuildinfo | 1 + packages/chat/tsconfig.json | 7 + packages/chat/tsconfig.lib.json | 105 ++++ packages/chat/tsconfig.node.json | 23 + packages/chat/tsconfig.node.tsbuildinfo | 1 + packages/chat/tsconfig.test.json | 10 + packages/chat/vite.config.ts | 16 + packages/editor/package.json | 4 +- 71 files changed, 2577 insertions(+), 1490 deletions(-) delete mode 100644 packages/agent/.eslintrc.cjs create mode 100644 packages/agent/eslint.config.mjs create mode 100644 packages/agent/src/NestiaAgent.ts delete mode 100644 packages/agent/src/NestiaChatAgent.ts create mode 100644 packages/agent/src/functional/createHttpLlmApplication.ts rename packages/agent/src/internal/{NestiaChatAgentConstant.ts => NestiaAgentConstant.ts} (58%) rename packages/agent/src/internal/{NestiaChatAgentCostAggregator.ts => NestiaAgentCostAggregator.ts} (86%) rename packages/agent/src/internal/{NestiaChatAgentDefaultPrompt.ts => NestiaAgentDefaultPrompt.ts} (66%) create mode 100644 packages/agent/src/internal/NestiaAgentOperationComposer.ts create mode 100644 packages/agent/src/internal/NestiaAgentPromptFactory.ts create mode 100644 packages/agent/src/internal/NestiaAgentPromptTransformer.ts create mode 100644 packages/agent/src/internal/NestiaAgentSystemPrompt.ts delete mode 100644 packages/agent/src/internal/NestiaChatAgentSystemPrompt.ts create mode 100644 packages/agent/src/internal/__map_take.ts delete mode 100644 packages/agent/src/structures/IChatGptService.ts create mode 100644 packages/agent/src/structures/INestiaAgentConfig.ts create mode 100644 packages/agent/src/structures/INestiaAgentContext.ts create mode 100644 packages/agent/src/structures/INestiaAgentController.ts create mode 100644 packages/agent/src/structures/INestiaAgentEvent.ts create mode 100644 packages/agent/src/structures/INestiaAgentOperation.ts create mode 100644 packages/agent/src/structures/INestiaAgentOperationCollection.ts create mode 100644 packages/agent/src/structures/INestiaAgentOperationSelection.ts create mode 100644 packages/agent/src/structures/INestiaAgentPrompt.ts create mode 100644 packages/agent/src/structures/INestiaAgentProps.ts create mode 100644 packages/agent/src/structures/INestiaAgentProvider.ts create mode 100644 packages/agent/src/structures/INestiaAgentSystemPrompt.ts rename packages/agent/src/structures/{INestiaChatTokenUsage.ts => INestiaAgentTokenUsage.ts} (61%) delete mode 100644 packages/agent/src/structures/INestiaChatAgent.ts delete mode 100644 packages/agent/src/structures/INestiaChatEvent.ts delete mode 100644 packages/agent/src/structures/INestiaChatFunctionSelection.ts delete mode 100644 packages/agent/src/structures/INestiaChatPrompt.ts create mode 100644 packages/agent/src/typings/NestiaAgentSource.ts create mode 100644 packages/chat/.gitignore create mode 100644 packages/chat/LICENSE create mode 100644 packages/chat/README.md create mode 100644 packages/chat/eslint.config.js create mode 100644 packages/chat/package.json create mode 100644 packages/chat/rollup.config.js create mode 100644 packages/chat/src/NestiaChatApplication.tsx create mode 100644 packages/chat/src/NestiaChatUploader.tsx create mode 100644 packages/chat/src/index.ts create mode 100644 packages/chat/src/main.tsx create mode 100644 packages/chat/src/movies/NestiaChatMovie.tsx create mode 100644 packages/chat/src/movies/prompts/NestiaChatDescribePromptMovie.tsx create mode 100644 packages/chat/src/movies/prompts/NestiaChatExecutePromptMovie.tsx create mode 100644 packages/chat/src/movies/prompts/NestiaChatPromptMovie.tsx create mode 100644 packages/chat/src/movies/prompts/NestiaChatSelectPromptMovie.tsx create mode 100644 packages/chat/src/movies/prompts/NestiaChatTextPromptMovie.tsx create mode 100644 packages/chat/src/movies/sides/NestiaChatTokenUsageMovie.tsx create mode 100644 packages/chat/src/vite-env.d.ts create mode 100644 packages/chat/tsconfig.app.json create mode 100644 packages/chat/tsconfig.app.tsbuildinfo create mode 100644 packages/chat/tsconfig.json create mode 100644 packages/chat/tsconfig.lib.json create mode 100644 packages/chat/tsconfig.node.json create mode 100644 packages/chat/tsconfig.node.tsbuildinfo create mode 100644 packages/chat/tsconfig.test.json create mode 100644 packages/chat/vite.config.ts diff --git a/packages/agent/.eslintrc.cjs b/packages/agent/.eslintrc.cjs deleted file mode 100644 index d44863fb4..000000000 --- a/packages/agent/.eslintrc.cjs +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - root: true, - plugins: ["@typescript-eslint", "deprecation"], - extends: ["plugin:@typescript-eslint/recommended"], - parser: "@typescript-eslint/parser", - parserOptions: { - project: ["tsconfig.json", "test/tsconfig.json"], - }, - overrides: [ - { - files: ["src/**/*.ts", "test/**/*.ts"], - rules: { - "@typescript-eslint/consistent-type-definitions": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-require-imports": "off", - "@typescript-eslint/no-empty-object-type": "off", - }, - }, - ], -}; diff --git a/packages/agent/build/prompt.ts b/packages/agent/build/prompt.ts index 5069fd243..21aa34dd3 100644 --- a/packages/agent/build/prompt.ts +++ b/packages/agent/build/prompt.ts @@ -18,12 +18,12 @@ const main = async (): Promise => { .trim(); } await fs.promises.writeFile( - `${__dirname}/../src/internal/NestiaChatAgentSystemPrompt.ts`, + `${__dirname}/../src/internal/NestiaAgentSystemPrompt.ts`, [ - `export namespace NestiaChatAgentSystemPrompt {`, + `export namespace NestiaAgentSystemPrompt {`, ...Object.entries(record).map( ([key, value]) => - ` export const ${key.toUpperCase()} = ${JSON.stringify(value)};`, + ` export const ${key.toUpperCase()} =\n ${JSON.stringify(value)};`, ), `}`, "", diff --git a/packages/agent/eslint.config.mjs b/packages/agent/eslint.config.mjs new file mode 100644 index 000000000..c2f7f31df --- /dev/null +++ b/packages/agent/eslint.config.mjs @@ -0,0 +1,21 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "no-empty-pattern": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-namespace": "off", + }, + }, +); diff --git a/packages/agent/package.json b/packages/agent/package.json index a9d7b5f27..3a067e51b 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/agent", - "version": "0.2.3", + "version": "0.3.0", "main": "lib/index.js", "module": "lib/index.mjs", "typings": "lib/index.d.ts", @@ -11,7 +11,8 @@ "build:main": "rimraf lib && npm run build:prompt && tsc && rollup -c", "build:test": "rimraf bin && tsc -p test/tsconfig.json", "build:prompt": "ts-node build/prompt.ts", - "dev": "npm run build:prompt && npm run build:test -- --watch" + "dev": "npm run build:prompt && npm run build:test -- --watch", + "eslint": "eslint ./**/*.ts" }, "repository": { "type": "git", @@ -39,30 +40,32 @@ "LICENSE", "package.json", "lib", + "prompts", "src" ], "devDependencies": { + "@eslint/js": "^9.17.0", "@nestia/e2e": "^0.7.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.1", "@samchon/shopping-api": "^0.11.0", "@types/node": "^22.10.5", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", "chalk": "4.1.2", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", + "eslint": "^9.17.0", "rimraf": "^6.0.1", "rollup": "^4.29.1", "ts-node": "^10.9.2", "ts-patch": "^3.3.0", "tstl": "^3.0.0", "typescript": "~5.7.2", + "typescript-eslint": "^8.18.2", "typescript-transform-paths": "^3.5.3" }, "dependencies": { - "@samchon/openapi": "^2.3.2", + "@samchon/openapi": "^2.3.4", "openai": "^4.77.0", "typia": "^7.5.1", "uuid": "^11.0.4" diff --git a/packages/agent/src/NestiaAgent.ts b/packages/agent/src/NestiaAgent.ts new file mode 100644 index 000000000..8c6cb354e --- /dev/null +++ b/packages/agent/src/NestiaAgent.ts @@ -0,0 +1,229 @@ +import OpenAI from "openai"; + +import { ChatGptAgent } from "./chatgpt/ChatGptAgent"; +import { NestiaAgentCostAggregator } from "./internal/NestiaAgentCostAggregator"; +import { NestiaAgentOperationComposer } from "./internal/NestiaAgentOperationComposer"; +import { NestiaAgentPromptTransformer } from "./internal/NestiaAgentPromptTransformer"; +import { __map_take } from "./internal/__map_take"; +import { INestiaAgentEvent } from "./structures/INestiaAgentEvent"; +import { INestiaAgentOperationCollection } from "./structures/INestiaAgentOperationCollection"; +import { INestiaAgentOperationSelection } from "./structures/INestiaAgentOperationSelection"; +import { INestiaAgentPrompt } from "./structures/INestiaAgentPrompt"; +import { INestiaAgentProps } from "./structures/INestiaAgentProps"; +import { INestiaAgentTokenUsage } from "./structures/INestiaAgentTokenUsage"; + +/** + * Nestia A.I. chatbot agent. + * + * `NestiaChatAgent` is a facade class for the A.I. chatbot agent + * which performs the {@link converstate user's conversation function} + * with LLM (Large Language Model) function calling and manages the + * {@link getPromptHistories prompt histories}. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export class NestiaAgent { + // THE OPERATIONS + private readonly operations_: INestiaAgentOperationCollection; + + // STACK + private readonly stack_: INestiaAgentOperationSelection[]; + private readonly prompt_histories_: INestiaAgentPrompt[]; + private readonly listeners_: Map>; + + // STATUS + private readonly token_usage_: INestiaAgentTokenUsage; + private ready_: boolean; + + /* ----------------------------------------------------------- + CONSTRUCTOR + ----------------------------------------------------------- */ + /** + * Initializer constructor. + * + * @param props Properties to construct the agent + */ + public constructor(private readonly props: INestiaAgentProps) { + // OPERATIONS + this.operations_ = NestiaAgentOperationComposer.compose({ + controllers: props.controllers, + config: props.config, + }); + + // STATUS + this.stack_ = []; + this.listeners_ = new Map(); + this.prompt_histories_ = (props.histories ?? []).map((input) => + NestiaAgentPromptTransformer.transform({ + operations: this.operations_.group, + input, + }), + ); + + // STATUS + this.token_usage_ = { + total: 0, + prompt: { + total: 0, + audio: 0, + cached: 0, + }, + completion: { + total: 0, + accepted_prediction: 0, + audio: 0, + reasoning: 0, + rejected_prediction: 0, + }, + }; + this.ready_ = false; + } + + /* ----------------------------------------------------------- + ACCESSORS + ----------------------------------------------------------- */ + /** + * Conversate with the A.I. chatbot. + * + * User talks to the A.I. chatbot with the content. + * + * When the user's conversation implies the A.I. chatbot to execute a + * function calling, the returned chat prompts will contain the + * function calling information like {@link INestiaAgentPrompt.IExecute}. + * + * @param content The content to talk + * @returns List of newly created chat prompts + */ + public async conversate(content: string): Promise { + const prompt: INestiaAgentPrompt.IText = { + type: "text", + role: "user", + text: content, + }; + const newbie: INestiaAgentPrompt[] = await ChatGptAgent.execute({ + // APPLICATION + operations: this.operations_, + config: this.props.config, + + // STATES + histories: this.prompt_histories_, + stack: this.stack_, + ready: () => this.ready_, + prompt, + + // HANDLERS + dispatch: (event) => this.dispatch(event), + request: async (source, body) => { + // request information + const event: INestiaAgentEvent.IRequest = { + type: "request", + source, + body: { + ...body, + model: this.props.provider.model, + }, + options: this.props.provider.options, + }; + await this.dispatch(event); + + // completion + const value: OpenAI.ChatCompletion = + await this.props.provider.api.chat.completions.create( + event.body, + event.options, + ); + NestiaAgentCostAggregator.aggregate(this.token_usage_, value); + await this.dispatch({ + type: "response", + source, + body: event.body, + options: event.options, + value, + }); + return value; + }, + initialize: async () => { + this.ready_ = true; + await this.dispatch({ + type: "initialize", + }); + }, + }); + this.prompt_histories_.push(prompt, ...newbie); + return [prompt, ...newbie]; + } + + /** + * Get the chatbot's prompt histories. + * + * Get list of chat prompts that the chatbot has been conversated. + * + * @returns List of chat prompts + */ + public getPromptHistories(): INestiaAgentPrompt[] { + return this.prompt_histories_; + } + + /** + * Get token usage of the A.I. chatbot. + * + * Entire token usage of the A.I. chatbot during the conversating + * with the user by {@link conversate} method callings. + * + * @returns Cost of the A.I. chatbot + */ + public getTokenUsage(): INestiaAgentTokenUsage { + return this.token_usage_; + } + + /* ----------------------------------------------------------- + EVENT HANDLERS + ----------------------------------------------------------- */ + /** + * Add an event listener. + * + * Add an event listener to be called whenever the event is emitted. + * + * @param type Type of event + * @param listener Callback function to be called whenever the event is emitted + */ + public on( + type: Type, + listener: (event: INestiaAgentEvent.Mapper[Type]) => void | Promise, + ): void { + __map_take(this.listeners_, type, () => new Set()).add(listener); + } + + /** + * Erase an event listener. + * + * Erase an event listener to stop calling the callback function. + * + * @param type Type of event + * @param listener Callback function to erase + */ + public off( + type: Type, + listener: (event: INestiaAgentEvent.Mapper[Type]) => void | Promise, + ): void { + const set: Set | undefined = this.listeners_.get(type); + if (set) { + set.delete(listener); + if (set.size === 0) this.listeners_.delete(type); + } + } + + private async dispatch( + event: Event, + ): Promise { + const set: Set | undefined = this.listeners_.get(event.type); + if (set) + await Promise.all( + Array.from(set).map(async (listener) => { + try { + await listener(event); + } catch {} + }), + ); + } +} diff --git a/packages/agent/src/NestiaChatAgent.ts b/packages/agent/src/NestiaChatAgent.ts deleted file mode 100644 index 8f7e882bd..000000000 --- a/packages/agent/src/NestiaChatAgent.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { IHttpConnection, IHttpLlmApplication } from "@samchon/openapi"; - -import { ChatGptAgent } from "./chatgpt/ChatGptAgent"; -import { IChatGptService } from "./structures/IChatGptService"; -import { INestiaChatAgent } from "./structures/INestiaChatAgent"; -import { INestiaChatEvent } from "./structures/INestiaChatEvent"; -import { INestiaChatPrompt } from "./structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "./structures/INestiaChatTokenUsage"; - -/** - * Nestia A.I. chatbot agent. - * - * `NestiaChatAgent` is a facade class for the A.I. chatbot agent - * which performs the {@link converstate user's conversation function} - * with LLM (Large Language Model) function calling and manages the - * {@link getHistories prompt histories}. - * - * @author Jeongho Nam - https://github.com/samchon - */ -export class NestiaChatAgent implements INestiaChatAgent { - /** - * @hidden - */ - private readonly agent: INestiaChatAgent; - - /** - * Initializer constructor. - * - * @param props Properties to construct the agent - */ - public constructor(props: NestiaChatAgent.IProps) { - this.agent = new ChatGptAgent(props); - } - - /** - * Conversate with the A.I. chatbot. - * - * User talks to the A.I. chatbot with the content. - * - * When the user's conversation implies the A.I. chatbot to execute a - * function calling, the returned chat prompts will contain the - * function calling information like {@link INestiaChatPrompt.IExecute}. - * - * @param content The content to talk - * @returns List of newly created chat prompts - */ - public conversate(content: string): Promise { - return this.agent.conversate(content); - } - - /** - * Get the chatbot's history. - * - * Get list of chat prompts that the chatbot has been conversated. - * - * @returns List of chat prompts - */ - public getHistories(): INestiaChatPrompt[] { - return this.agent.getHistories(); - } - - /** - * Get token usage of the A.I. chatbot. - * - * Entire token usage of the A.I. chatbot during the conversating - * with the user by {@link conversate} method callings. - * - * @returns Cost of the A.I. chatbot - */ - public getTokenUsage(): INestiaChatTokenUsage { - return this.agent.getTokenUsage(); - } - - /** - * Add an event listener. - * - * Add an event listener to be called whenever the event is emitted. - * - * @param type Type of event - * @param listener Callback function to be called whenever the event is emitted - */ - public on( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void | Promise, - ): void { - this.agent.on(type, listener); - } - - /** - * Erase an event listener. - * - * Erase an event listener to stop calling the callback function. - * - * @param type Type of event - * @param listener Callback function to erase - */ - public off( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void | Promise, - ): void { - this.agent.off(type, listener); - } -} -export namespace NestiaChatAgent { - /** - * Properties of the A.I. chatbot agent. - */ - export interface IProps { - /** - * Application instance for LLM function calling. - */ - application: IHttpLlmApplication<"chatgpt">; - - /** - * Service of the ChatGPT (OpenAI) API. - */ - service: IChatGptService; - - /** - * HTTP connection to the backend server. - */ - connection: IHttpConnection; - - /** - * Initial chat prompts. - * - * If you configure this property, the chatbot will start the - * pre-defined conversations. - */ - histories?: INestiaChatPrompt[] | undefined; - - /** - * Configuration for the A.I. chatbot. - */ - config?: IConfig | undefined; - } - - /** - * Configuration for the A.I. chatbot. - */ - export interface IConfig { - /** - * Locale of the A.I. chatbot. - * - * If you configure this property, the A.I. chatbot will conversate with the - * given locale. You can get the locale value by - * - * - Browser: `navigator.language` - * - NodeJS: `process.env.LANG.split(".")[0]` - * - * @default your_locale - */ - locale?: string; - - /** - * Timezone of the A.I. chatbot. - * - * If you configure this property, the A.I. chatbot will consider the given timezone. - * You can get the timezone value by `Intl.DateTimeFormat().resolvedOptions().timeZone`. - * - * @default your_timezone - */ - timezone?: string; - - /** - * Retry count. - * - * If LLM function calling composed arguments are invalid, - * the A.I. chatbot will retry to call the function with - * the modified arguments. - * - * By the way, if you configure it to 0 or 1, the A.I. chatbot - * will not retry the LLM function calling for correcting the - * arguments. - * - * @default 3 - */ - retry?: number; - - /** - * Capacity of the LLM function selecting. - * - * When the A.I. chatbot selects a proper function to call, if the - * number of functions registered in the {@link IProps.application} - * is too much greater, the A.I. chatbot often fallen into the - * hallucination. - * - * In that case, if you configure this property value, `NestiaChatAgent` - * will divide the functions into the several groups with the configured - * capacity and select proper functions to call by operating the multiple - * LLM function selecting agents parallelly. - * - * @default 0 - */ - capacity?: number; - - /** - * Eliticism for the LLM function selecting. - * - * If you configure {@link capacity}, the A.I. chatbot will complete - * the candidate functions to call which are selected by the multiple - * LLM function selecting agents. - * - * Otherwise you configure this property as `false`, the A.I. chatbot - * will not complete the candidate functions to call and just accept - * every candidate functions to call which are selected by the multiple - * LLM function selecting agents. - * - * @default true - */ - eliticism?: boolean; - - /** - * System prompt messages. - * - * System prompt messages if you want to customize the system prompt - * messages for each situation. - */ - systemPrompt?: ISytemPrompt; - } - - /** - * System prompt messages. - * - * System prompt messages if you want to customize the system prompt - * messages for each situation. - */ - export interface ISytemPrompt { - common?: (config?: IConfig | undefined) => string; - initialize?: (histories: INestiaChatPrompt[]) => string; - select?: (histories: INestiaChatPrompt[]) => string; - cancel?: (histories: INestiaChatPrompt[]) => string; - execute?: (histories: INestiaChatPrompt[]) => string; - describe?: (histories: INestiaChatPrompt.IExecute[]) => string; - } -} diff --git a/packages/agent/src/chatgpt/ChatGptAgent.ts b/packages/agent/src/chatgpt/ChatGptAgent.ts index 73b4f38ab..cdebecf4d 100644 --- a/packages/agent/src/chatgpt/ChatGptAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptAgent.ts @@ -1,210 +1,52 @@ -import { IHttpLlmFunction } from "@samchon/openapi"; - -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { INestiaChatAgent } from "../structures/INestiaChatAgent"; -import { INestiaChatEvent } from "../structures/INestiaChatEvent"; -import { INestiaChatFunctionSelection } from "../structures/INestiaChatFunctionSelection"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; -import { __IChatSelectFunctionsApplication } from "../structures/internal/__IChatSelectFunctionsApplication"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { ChatGptCancelFunctionAgent } from "./ChatGptCancelFunctionAgent"; import { ChatGptDescribeFunctionAgent } from "./ChatGptDescribeFunctionAgent"; import { ChatGptExecuteFunctionAgent } from "./ChatGptExecuteFunctionAgent"; import { ChatGptInitializeFunctionAgent } from "./ChatGptInitializeFunctionAgent"; import { ChatGptSelectFunctionAgent } from "./ChatGptSelectFunctionAgent"; -export class ChatGptAgent implements INestiaChatAgent { - private readonly histories_: INestiaChatPrompt[]; - private readonly stack_: INestiaChatFunctionSelection[]; - private readonly listeners_: Map>; - - private readonly divide_?: IHttpLlmFunction<"chatgpt">[][] | undefined; - private readonly usage_: INestiaChatTokenUsage; - private initialized_: boolean; - - public constructor(private readonly props: NestiaChatAgent.IProps) { - this.stack_ = []; - this.histories_ = props.histories ? [...props.histories] : []; - this.listeners_ = new Map(); - - this.initialized_ = false; - this.usage_ = { - total: 0, - prompt: { - total: 0, - audio: 0, - cached: 0, - }, - completion: { - total: 0, - accepted_prediction: 0, - audio: 0, - reasoning: 0, - rejected_prediction: 0, - }, - }; - - if ( - !!props.config?.capacity && - props.application.functions.length > props.config.capacity - ) { - const size: number = Math.ceil( - props.application.functions.length / props.config.capacity, - ); - const capacity: number = Math.ceil( - props.application.functions.length / size, - ); - - const entireFunctions: IHttpLlmFunction<"chatgpt">[] = - props.application.functions.slice(); - this.divide_ = new Array(size) - .fill(0) - .map(() => entireFunctions.splice(0, capacity)); - } - } - - public getHistories(): INestiaChatPrompt[] { - return this.histories_; - } - - public getTokenUsage(): INestiaChatTokenUsage { - return this.usage_; - } - - public async conversate(content: string): Promise { - const index: number = this.histories_.length; - const out = () => this.histories_.slice(index); +export namespace ChatGptAgent { + export const execute = async ( + ctx: INestiaAgentContext, + ): Promise => { + const histories: INestiaAgentPrompt[] = []; // FUNCTIONS ARE NOT LISTED YET - if (this.initialized_ === false) { - const output: ChatGptInitializeFunctionAgent.IOutput = - await ChatGptInitializeFunctionAgent.execute({ - service: this.props.service, - histories: this.histories_, - config: this.props.config, - usage: this.usage_, - content, - }); - this.initialized_ ||= output.mounted; - this.histories_.push(...output.prompts); - if (this.initialized_ === false) return out(); - else this.dispatch({ type: "initialize" }); + if (ctx.ready() === false) { + histories.push(...(await ChatGptInitializeFunctionAgent.execute(ctx))); + if (ctx.ready() === false) return histories; } // CANCEL CANDIDATE FUNCTIONS - if (this.stack_.length) - this.histories_.push( - ...(await ChatGptCancelFunctionAgent.execute({ - application: this.props.application, - service: this.props.service, - histories: this.histories_, - stack: this.stack_, - dispatch: (event) => this.dispatch(event), - config: this.props.config, - usage: this.usage_, - content, - })), - ); + if (ctx.stack.length !== 0) + histories.push(...(await ChatGptCancelFunctionAgent.execute(ctx))); // SELECT CANDIDATE FUNCTIONS - this.histories_.push( - ...(await ChatGptSelectFunctionAgent.execute({ - application: this.props.application, - service: this.props.service, - histories: this.histories_, - stack: this.stack_, - dispatch: (event) => this.dispatch(event), - divide: this.divide_, - config: this.props.config, - usage: this.usage_, - content, - })), - ); - if (this.stack_.length === 0) return out(); + histories.push(...(await ChatGptSelectFunctionAgent.execute(ctx))); + if (ctx.stack.length === 0) return histories; - // CALL FUNCTIONS + // FUNCTION CALLING LOOP while (true) { - const prompts: INestiaChatPrompt[] = - await ChatGptExecuteFunctionAgent.execute({ - connection: this.props.connection, - service: this.props.service, - histories: this.histories_, - application: this.props.application, - functions: Array.from(this.stack_.values()).map( - (item) => item.function, - ), - dispatch: (event) => this.dispatch(event), - config: this.props.config, - usage: this.usage_, - content, - }); - this.histories_.push(...prompts); + // CALL FUNCTIONS + const prompts: INestiaAgentPrompt[] = + await ChatGptExecuteFunctionAgent.execute(ctx); + histories.push(...prompts); // EXPLAIN RETURN VALUES - const calls: INestiaChatPrompt.IExecute[] = prompts.filter( - (p) => p.kind === "execute", + const executes: INestiaAgentPrompt.IExecute[] = prompts.filter( + (prompt) => prompt.type === "execute", ); - for (const c of calls) - ChatGptCancelFunctionAgent.cancelFunction({ - stack: this.stack_, - reference: { - name: c.function.name, - reason: "completed", - }, - dispatch: (event) => this.dispatch(event), + for (const e of executes) + await ChatGptCancelFunctionAgent.cancelFunction(ctx, { + name: e.function.name, + reason: "completed", }); - if (calls.length !== 0) - this.histories_.push( - ...(await ChatGptDescribeFunctionAgent.execute({ - service: this.props.service, - histories: calls, - config: this.props.config, - usage: this.usage_, - })), - ); - if (calls.length === 0 || this.stack_.length === 0) break; - } - return out(); - } - - public on( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void, - ): void { - take(this.listeners_, type, () => new Set()).add(listener); - } - - public off( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void, - ): void { - const set: Set | undefined = this.listeners_.get(type); - if (set) { - set.delete(listener); - if (set.size === 0) this.listeners_.delete(type); - } - } - - private async dispatch( - event: Event, - ): Promise { - const set: Set | undefined = this.listeners_.get(event.type); - if (set) - await Promise.all( - Array.from(set).map(async (listener) => { - try { - await listener(event); - } catch {} - }), + histories.push( + ...(await ChatGptDescribeFunctionAgent.execute(ctx, executes)), ); - } + if (executes.length === 0 || ctx.stack.length === 0) break; + } + return histories; + }; } - -const take = (dict: Map, key: Key, generator: () => T): T => { - const oldbie: T | undefined = dict.get(key); - if (oldbie) return oldbie; - - const value: T = generator(); - dict.set(key, value); - return value; -}; diff --git a/packages/agent/src/chatgpt/ChatGptCancelFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptCancelFunctionAgent.ts index 212dba10c..03d9607f5 100644 --- a/packages/agent/src/chatgpt/ChatGptCancelFunctionAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptCancelFunctionAgent.ts @@ -1,58 +1,43 @@ -import { - IHttpLlmApplication, - IHttpLlmFunction, - ILlmApplication, -} from "@samchon/openapi"; +import { IHttpLlmFunction, ILlmApplication } from "@samchon/openapi"; import OpenAI from "openai"; import typia, { IValidation } from "typia"; import { v4 } from "uuid"; -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentConstant } from "../internal/NestiaChatAgentConstant"; -import { NestiaChatAgentCostAggregator } from "../internal/NestiaChatAgentCostAggregator"; -import { NestiaChatAgentDefaultPrompt } from "../internal/NestiaChatAgentDefaultPrompt"; -import { NestiaChatAgentSystemPrompt } from "../internal/NestiaChatAgentSystemPrompt"; -import { IChatGptService } from "../structures/IChatGptService"; -import { INestiaChatEvent } from "../structures/INestiaChatEvent"; -import { INestiaChatFunctionSelection } from "../structures/INestiaChatFunctionSelection"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { NestiaAgentConstant } from "../internal/NestiaAgentConstant"; +import { NestiaAgentDefaultPrompt } from "../internal/NestiaAgentDefaultPrompt"; +import { NestiaAgentPromptFactory } from "../internal/NestiaAgentPromptFactory"; +import { NestiaAgentSystemPrompt } from "../internal/NestiaAgentSystemPrompt"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentController } from "../structures/INestiaAgentController"; +import { INestiaAgentEvent } from "../structures/INestiaAgentEvent"; +import { INestiaAgentOperation } from "../structures/INestiaAgentOperation"; +import { INestiaAgentOperationSelection } from "../structures/INestiaAgentOperationSelection"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { __IChatCancelFunctionsApplication } from "../structures/internal/__IChatCancelFunctionsApplication"; import { __IChatFunctionReference } from "../structures/internal/__IChatFunctionReference"; import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; export namespace ChatGptCancelFunctionAgent { - export interface IProps { - application: IHttpLlmApplication<"chatgpt">; - service: IChatGptService; - histories: INestiaChatPrompt[]; - stack: INestiaChatFunctionSelection[]; - dispatch: (event: INestiaChatEvent) => Promise; - usage: INestiaChatTokenUsage; - content: string; - divide?: IHttpLlmFunction<"chatgpt">[][] | undefined; - config?: NestiaChatAgent.IConfig | undefined; - } - export const execute = async ( - props: IProps, - ): Promise => { - if (props.divide === undefined) - return step(props, props.application.functions, 0); + ctx: INestiaAgentContext, + ): Promise => { + if (ctx.operations.divided === undefined) + return step(ctx, ctx.operations.array, 0); - const stacks: INestiaChatFunctionSelection[][] = props.divide.map(() => []); - const events: INestiaChatEvent[] = []; - const prompts: INestiaChatPrompt.ICancel[][] = await Promise.all( - props.divide.map((candidates, i) => + const stacks: INestiaAgentOperationSelection[][] = + ctx.operations.divided.map(() => []); + const events: INestiaAgentEvent[] = []; + const prompts: INestiaAgentPrompt.ICancel[][] = await Promise.all( + ctx.operations.divided.map((operations, i) => step( { - ...props, + ...ctx, stack: stacks[i]!, dispatch: async (e) => { events.push(e); }, }, - candidates, + operations, 0, ), ), @@ -61,144 +46,148 @@ export namespace ChatGptCancelFunctionAgent { // NO FUNCTION SELECTION, SO THAT ONLY TEXT LEFT if (stacks.every((s) => s.length === 0)) return prompts[0]!; // ELITICISM - else if ( - (props.config?.eliticism ?? NestiaChatAgentConstant.ELITICISM) === true - ) + else if ((ctx.config?.eliticism ?? NestiaAgentConstant.ELITICISM) === true) return step( - props, + ctx, stacks - .map((row) => Array.from(row.values()).map((s) => s.function)) - .flat(), + .flat() + .map( + (s) => + ctx.operations.group + .get(s.controller.name)! + .get(s.function.name)!, + ), 0, ); // RE-COLLECT SELECT FUNCTION EVENTS - const collection: INestiaChatPrompt.ICancel = { + const collection: INestiaAgentPrompt.ICancel = { id: v4(), - kind: "cancel", - functions: [], + type: "cancel", + operations: [], }; for (const e of events) if (e.type === "select") { - collection.functions.push({ - function: e.function, - reason: e.reason, - }); - await cancelFunction({ - stack: props.stack, - dispatch: props.dispatch, - reference: { - name: e.function.name, + collection.operations.push( + NestiaAgentPromptFactory.selection({ + protocol: e.operation.protocol as "http", + controller: e.operation.controller as INestiaAgentController.IHttp, + function: e.operation.function as IHttpLlmFunction<"chatgpt">, reason: e.reason, - }, + name: e.operation.name, + }), + ); + await cancelFunction(ctx, { + name: e.operation.name, + reason: e.reason, }); } return [collection]; }; - export const cancelFunction = async (props: { - stack: INestiaChatFunctionSelection[]; - reference: __IChatFunctionReference; - dispatch: (event: INestiaChatEvent.ICancelFunctionEvent) => Promise; - }): Promise | null> => { - const index: number = props.stack.findIndex( - (item) => item.function.name === props.reference.name, + export const cancelFunction = async ( + ctx: INestiaAgentContext, + reference: __IChatFunctionReference, + ): Promise => { + const index: number = ctx.stack.findIndex( + (item) => item.name === reference.name, ); if (index === -1) return null; - const item: INestiaChatFunctionSelection = props.stack[index]!; - props.stack.splice(index, 1); - await props.dispatch({ + const item: INestiaAgentOperationSelection = ctx.stack[index]!; + ctx.stack.splice(index, 1); + await ctx.dispatch({ type: "cancel", - function: item.function, - reason: props.reference.reason, + operation: item, + reason: reference.reason, }); - return item.function; + return item; }; const step = async ( - props: IProps, - candidates: IHttpLlmFunction<"chatgpt">[], + ctx: INestiaAgentContext, + operations: INestiaAgentOperation[], retry: number, failures?: IFailure[], - ): Promise => { + ): Promise => { //---- // EXECUTE CHATGPT API //---- - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + const completion: OpenAI.ChatCompletion = await ctx.request("cancel", { + messages: [ + // COMMON SYSTEM PROMPT { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // CANDIDATE FUNCTIONS - { - role: "assistant", - tool_calls: [ - { - type: "function", - id: "getApiFunctions", - function: { - name: "getApiFunctions", - arguments: JSON.stringify({}), - }, - }, - ], - }, - { - role: "tool", - tool_call_id: "getApiFunctions", - content: JSON.stringify( - candidates.map((func) => ({ - name: func.name, - description: func.description, - })), - ), - }, - // PREVIOUS HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // USER INPUT - { - role: "user", - content: props.content, - }, - // SYTEM PROMPT + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // CANDIDATE FUNCTIONS + { + role: "assistant", + tool_calls: [ { - role: "system", - content: - props.config?.systemPrompt?.cancel?.(props.histories) ?? - NestiaChatAgentSystemPrompt.CANCEL, + type: "function", + id: "getApiFunctions", + function: { + name: "getApiFunctions", + arguments: JSON.stringify({}), + }, }, - // TYPE CORRECTIONS - ...emendMessages(failures ?? []), ], - // STACK FUNCTIONS - tools: CONTAINER.functions.map( - (func) => - ({ - type: "function", - function: { - name: func.name, - description: func.description, - parameters: func.parameters as any, - }, - }) satisfies OpenAI.ChatCompletionTool, + }, + { + role: "tool", + tool_call_id: "getApiFunctions", + content: JSON.stringify( + operations.map((op) => ({ + name: op.name, + description: op.function.description, + ...(op.protocol === "http" + ? { + method: op.function.method, + path: op.function.path, + tags: op.function.tags, + } + : {}), + })), ), - tool_choice: "auto", - parallel_tool_calls: true, }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); + // PREVIOUS HISTORIES + ...ctx.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: ctx.prompt.text, + }, + // SYTEM PROMPT + { + role: "system", + content: + ctx.config?.systemPrompt?.cancel?.(ctx.histories) ?? + NestiaAgentSystemPrompt.CANCEL, + }, + // TYPE CORRECTIONS + ...emendMessages(failures ?? []), + ], + // STACK FUNCTIONS + tools: CONTAINER.functions.map( + (func) => + ({ + type: "function", + function: { + name: func.name, + description: func.description, + parameters: func.parameters as any, + }, + }) satisfies OpenAI.ChatCompletionTool, + ), + tool_choice: "auto", + parallel_tool_calls: true, + }); //---- // VALIDATION //---- - if (retry++ < (props.config?.retry ?? NestiaChatAgentConstant.RETRY)) { + if (retry++ < (ctx.config?.retry ?? NestiaAgentConstant.RETRY)) { const failures: IFailure[] = []; for (const choice of completion.choices) for (const tc of choice.message.tool_calls ?? []) { @@ -213,13 +202,13 @@ export namespace ChatGptCancelFunctionAgent { validation, }); } - if (failures.length > 0) return step(props, candidates, retry, failures); + if (failures.length > 0) return step(ctx, operations, retry, failures); } //---- // PROCESS COMPLETION //---- - const prompts: INestiaChatPrompt.ICancel[] = []; + const prompts: INestiaAgentPrompt.ICancel[] = []; for (const choice of completion.choices) { // TOOL CALLING HANDLER if (choice.message.tool_calls) @@ -230,25 +219,16 @@ export namespace ChatGptCancelFunctionAgent { ); if (typia.is(input) === false) continue; else if (tc.function.name === "cancelFunctions") { - const collection: INestiaChatPrompt.ICancel = { + const collection: INestiaAgentPrompt.ICancel = { id: tc.id, - kind: "cancel", - functions: [], + type: "cancel", + operations: [], }; for (const reference of input.functions) { - const func: IHttpLlmFunction<"chatgpt"> | null = - await cancelFunction({ - stack: props.stack, - dispatch: props.dispatch, - reference, - }); - if (func !== null) - collection.functions.push({ - function: func, - reason: reference.reason, - }); + const operation = await cancelFunction(ctx, reference); + if (operation !== null) collection.operations.push(operation); } - if (collection.functions.length !== 0) prompts.push(collection); + if (collection.operations.length !== 0) prompts.push(collection); } } } diff --git a/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts index f1c54d060..78d329ff6 100644 --- a/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts @@ -1,52 +1,35 @@ import OpenAI from "openai"; -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentCostAggregator } from "../internal/NestiaChatAgentCostAggregator"; -import { NestiaChatAgentDefaultPrompt } from "../internal/NestiaChatAgentDefaultPrompt"; -import { NestiaChatAgentSystemPrompt } from "../internal/NestiaChatAgentSystemPrompt"; -import { IChatGptService } from "../structures/IChatGptService"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { NestiaAgentDefaultPrompt } from "../internal/NestiaAgentDefaultPrompt"; +import { NestiaAgentSystemPrompt } from "../internal/NestiaAgentSystemPrompt"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; export namespace ChatGptDescribeFunctionAgent { - export interface IProps { - service: IChatGptService; - histories: INestiaChatPrompt.IExecute[]; - usage: INestiaChatTokenUsage; - config?: NestiaChatAgent.IConfig; - } - export const execute = async ( - props: IProps, - ): Promise => { - if (props.histories.length === 0) return []; - - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + ctx: INestiaAgentContext, + histories: INestiaAgentPrompt.IExecute[], + ): Promise => { + if (histories.length === 0) return []; + const completion: OpenAI.ChatCompletion = await ctx.request("describe", { + messages: [ + // COMMON SYSTEM PROMPT + { + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // FUNCTION CALLING HISTORIES + ...histories.map(ChatGptHistoryDecoder.decode).flat(), + // SYTEM PROMPT { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // PREVIOUS FUNCTION CALLING HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // SYTEM PROMPT - { - role: "assistant", - content: - props.config?.systemPrompt?.describe?.(props.histories) ?? - NestiaChatAgentSystemPrompt.DESCRIBE, - }, - ], + role: "assistant", + content: + ctx.config?.systemPrompt?.describe?.(histories) ?? + NestiaAgentSystemPrompt.DESCRIBE, }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); - + ], + }); return completion.choices .map((choice) => choice.message.role === "assistant" && !!choice.message.content?.length @@ -54,10 +37,13 @@ export namespace ChatGptDescribeFunctionAgent { : null, ) .filter((str) => str !== null) - .map((content) => ({ - kind: "describe", - executions: props.histories, - text: content, - })); + .map( + (content) => + ({ + type: "describe", + executions: histories, + text: content, + }) satisfies INestiaAgentPrompt.IDescribe, + ); }; } diff --git a/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts index eb26b398b..104a326ea 100644 --- a/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts @@ -2,105 +2,94 @@ import { ChatGptTypeChecker, HttpLlm, IChatGptSchema, - IHttpConnection, - IHttpLlmApplication, - IHttpLlmFunction, IHttpMigrateRoute, IHttpResponse, } from "@samchon/openapi"; import OpenAI from "openai"; +import { IValidation } from "typia"; -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentConstant } from "../internal/NestiaChatAgentConstant"; -import { NestiaChatAgentCostAggregator } from "../internal/NestiaChatAgentCostAggregator"; -import { NestiaChatAgentDefaultPrompt } from "../internal/NestiaChatAgentDefaultPrompt"; -import { NestiaChatAgentSystemPrompt } from "../internal/NestiaChatAgentSystemPrompt"; -import { IChatGptService } from "../structures/IChatGptService"; -import { INestiaChatEvent } from "../structures/INestiaChatEvent"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { NestiaAgentConstant } from "../internal/NestiaAgentConstant"; +import { NestiaAgentDefaultPrompt } from "../internal/NestiaAgentDefaultPrompt"; +import { NestiaAgentPromptFactory } from "../internal/NestiaAgentPromptFactory"; +import { NestiaAgentSystemPrompt } from "../internal/NestiaAgentSystemPrompt"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentEvent } from "../structures/INestiaAgentEvent"; +import { INestiaAgentOperation } from "../structures/INestiaAgentOperation"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; export namespace ChatGptExecuteFunctionAgent { - export interface IProps { - service: IChatGptService; - connection: IHttpConnection; - application: IHttpLlmApplication<"chatgpt">; - functions: IHttpLlmFunction<"chatgpt">[]; - histories: INestiaChatPrompt[]; - dispatch: (event: INestiaChatEvent) => Promise; - usage: INestiaChatTokenUsage; - content: string; - config?: NestiaChatAgent.IConfig | undefined; - } - export const execute = async ( - props: IProps, - ): Promise => { + ctx: INestiaAgentContext, + ): Promise => { //---- // EXECUTE CHATGPT API //---- - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + const completion: OpenAI.ChatCompletion = await ctx.request("execute", { + messages: [ + // COMMON SYSTEM PROMPT { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // PREVIOUS HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // USER INPUT - { - role: "user", - content: props.content, - }, - // SYTEM PROMPT - { - role: "system", - content: - props.config?.systemPrompt?.execute?.(props.histories) ?? - NestiaChatAgentSystemPrompt.EXECUTE, - }, - ], - // STACKED FUNCTIONS - tools: props.functions.map( - (func) => - ({ - type: "function", - function: { - name: func.name, - description: func.description, - parameters: func.parameters as any, - }, - }) as OpenAI.ChatCompletionTool, - ), - tool_choice: "auto", - parallel_tool_calls: false, + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // PREVIOUS HISTORIES + ...ctx.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: ctx.prompt.text, }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); + // SYTEM PROMPT + { + role: "system", + content: + ctx.config?.systemPrompt?.execute?.(ctx.histories) ?? + NestiaAgentSystemPrompt.EXECUTE, + }, + ], + // STACKED FUNCTIONS + tools: ctx.stack.map( + (op) => + ({ + type: "function", + function: { + name: op.name, + description: op.function.description, + parameters: (op.function.separated + ? (op.function.separated.llm ?? + ({ + type: "object", + properties: {}, + required: [], + additionalProperties: false, + $defs: {}, + } satisfies IChatGptSchema.IParameters)) + : op.function.parameters) as Record, + }, + }) as OpenAI.ChatCompletionTool, + ), + tool_choice: "auto", + parallel_tool_calls: false, + }); //---- // PROCESS COMPLETION //---- - const closures: Array<() => Promise> = []; + const closures: Array<() => Promise> = []; for (const choice of completion.choices) { for (const tc of choice.message.tool_calls ?? []) { if (tc.type === "function") { - const func: IHttpLlmFunction<"chatgpt"> | undefined = - props.functions.find((func) => func.name === tc.function.name); - if (func === undefined) continue; + const operation: INestiaAgentOperation | undefined = + ctx.operations.flat.get(tc.function.name); + if (operation === undefined) continue; closures.push(() => propagate( - props, + ctx, { + type: "call", id: tc.id, - function: func, - input: JSON.parse(tc.function.arguments), + operation, + arguments: JSON.parse(tc.function.arguments), }, 0, ), @@ -114,71 +103,151 @@ export namespace ChatGptExecuteFunctionAgent { closures.push( async () => ({ - kind: "text", + type: "text", role: "assistant", text: choice.message.content!, - }) satisfies INestiaChatPrompt.IText, + }) satisfies INestiaAgentPrompt.IText, ); } return Promise.all(closures.map((fn) => fn())); }; const propagate = async ( - props: IProps, - call: IFunctionCall, + ctx: INestiaAgentContext, + call: INestiaAgentEvent.ICall, retry: number, - ): Promise => { - fill({ - function: call.function, - arguments: call.input, - }); - try { - await props.dispatch({ - type: "call", - function: call.function, - arguments: call.input, + ): Promise => { + if (call.operation.protocol === "http") + fillHttpArguments({ + operation: call.operation, + arguments: call.arguments, }); - const response: IHttpResponse = await HttpLlm.propagate({ - connection: props.connection, - application: props.application, - function: call.function, - input: call.input, - }); - const success: boolean = - ((response.status === 400 || - response.status === 404 || - response.status === 422) && - retry++ < (props.config?.retry ?? NestiaChatAgentConstant.RETRY) && - typeof response.body) === false; - const result: INestiaChatPrompt.IExecute = (success === false - ? await correct(props, call, retry, response.body) - : null) ?? { - kind: "execute", - role: "assistant", - function: call.function, - id: call.id, - arguments: call.input, - response: response, - }; - if (success === true) - await props.dispatch({ - type: "complete", - function: call.function, - arguments: result.arguments, - response: result.response, + await ctx.dispatch(call); + + if (call.operation.protocol === "http") { + //---- + // HTTP PROTOCOL + //---- + try { + // CALL HTTP API + const response: IHttpResponse = call.operation.controller.execute + ? await call.operation.controller.execute({ + connection: call.operation.controller.connection, + application: call.operation.controller.application, + function: call.operation.function, + arguments: call.arguments, + }) + : await HttpLlm.propagate({ + connection: call.operation.controller.connection, + application: call.operation.controller.application, + function: call.operation.function, + input: call.arguments, + }); + // CHECK STATUS + const success: boolean = + ((response.status === 400 || + response.status === 404 || + response.status === 422) && + retry++ < (ctx.config?.retry ?? NestiaAgentConstant.RETRY) && + typeof response.body) === false; + // DISPATCH EVENT + const result: INestiaAgentPrompt.IExecute = + (success === false + ? await correct(ctx, call, retry, response.body) + : null) ?? + NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: "http", + controller: call.operation.controller, + function: call.operation.function, + id: call.id, + arguments: call.arguments, + value: response, + }); + if (success === true) + await ctx.dispatch({ + type: "execute", + id: call.id, + operation: call.operation, + arguments: result.arguments, + value: result.value, + }); + return result; + } catch (error) { + // DISPATCH ERROR + return NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: "http", + controller: call.operation.controller, + function: call.operation.function, + id: call.id, + arguments: call.arguments, + value: { + status: 500, + headers: {}, + body: + error instanceof Error + ? { + ...error, + name: error.name, + message: error.message, + } + : error, + }, }); - return result; - } catch (error) { - return { - kind: "execute", - role: "assistant", - function: call.function, - id: call.id, - arguments: call.input, - response: { - status: 500, - headers: {}, - body: + } + } else { + //---- + // CLASS FUNCTION + //---- + // VALIDATE FIRST + const check: IValidation = call.operation.function.validate( + call.arguments, + ); + if (check.success === false) + return ( + (retry++ < (ctx.config?.retry ?? NestiaAgentConstant.RETRY) + ? await correct(ctx, call, retry, check.errors) + : null) ?? + NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: "class", + controller: call.operation.controller, + function: call.operation.function, + id: call.id, + arguments: call.arguments, + value: { + name: "TypeGuardError", + message: "Invalid arguments.", + errors: check.errors, + }, + }) + ); + // EXECUTE FUNCTION + try { + const value: any = await call.operation.controller.execute({ + application: call.operation.controller.application, + function: call.operation.function, + arguments: call.arguments, + }); + return NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: "class", + controller: call.operation.controller, + function: call.operation.function, + id: call.id, + arguments: call.arguments, + value, + }); + } catch (error) { + return NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: "class", + controller: call.operation.controller, + function: call.operation.function, + id: call.id, + arguments: call.arguments, + value: error instanceof Error ? { ...error, @@ -186,98 +255,91 @@ export namespace ChatGptExecuteFunctionAgent { message: error.message, } : error, - }, - } satisfies INestiaChatPrompt.IExecute; + }); + } } }; const correct = async ( - props: IProps, - call: IFunctionCall, + ctx: INestiaAgentContext, + call: INestiaAgentEvent.ICall, retry: number, error: unknown, - ): Promise => { + ): Promise => { //---- // EXECUTE CHATGPT API //---- - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + const completion: OpenAI.ChatCompletion = await ctx.request("execute", { + messages: [ + // COMMON SYSTEM PROMPT { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // PREVIOUS HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // USER INPUT - { - role: "user", - content: props.content, - }, - // TYPE CORRECTION - { - role: "system", - content: - props.config?.systemPrompt?.execute?.(props.histories) ?? - NestiaChatAgentSystemPrompt.EXECUTE, - }, - { - role: "assistant", - tool_calls: [ - { - type: "function", - id: call.id, - function: { - name: call.function.name, - arguments: JSON.stringify(call.input), - }, - } satisfies OpenAI.ChatCompletionMessageToolCall, - ], - } satisfies OpenAI.ChatCompletionAssistantMessageParam, - { - role: "tool", - content: - typeof error === "string" ? error : JSON.stringify(error), - tool_call_id: call.id, - } satisfies OpenAI.ChatCompletionToolMessageParam, - { - role: "system", - content: [ - "You A.I. assistant has composed wrong arguments.", - "", - "Correct it at the next function calling.", - ].join("\n"), - }, - ], - // STACK FUNCTIONS - tools: [ + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // PREVIOUS HISTORIES + ...ctx.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: ctx.prompt.text, + }, + // TYPE CORRECTION + { + role: "system", + content: + ctx.config?.systemPrompt?.execute?.(ctx.histories) ?? + NestiaAgentSystemPrompt.EXECUTE, + }, + { + role: "assistant", + tool_calls: [ { type: "function", + id: call.id, function: { - name: call.function.name, - description: call.function.description, - parameters: (call.function.separated - ? (call.function.separated?.llm ?? - ({ - $defs: {}, - type: "object", - properties: {}, - additionalProperties: false, - required: [], - } satisfies IChatGptSchema.IParameters)) - : call.function.parameters) as any, + name: call.operation.name, + arguments: JSON.stringify(call.arguments), }, - }, + } satisfies OpenAI.ChatCompletionMessageToolCall, ], - tool_choice: "auto", - parallel_tool_calls: false, + } satisfies OpenAI.ChatCompletionAssistantMessageParam, + { + role: "tool", + content: typeof error === "string" ? error : JSON.stringify(error), + tool_call_id: call.id, + } satisfies OpenAI.ChatCompletionToolMessageParam, + { + role: "system", + content: [ + "You A.I. assistant has composed wrong arguments.", + "", + "Correct it at the next function calling.", + ].join("\n"), }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); + ], + // STACK FUNCTIONS + tools: [ + { + type: "function", + function: { + name: call.operation.name, + description: call.operation.function.description, + parameters: (call.operation.function.separated + ? (call.operation.function.separated?.llm ?? + ({ + $defs: {}, + type: "object", + properties: {}, + additionalProperties: false, + required: [], + } satisfies IChatGptSchema.IParameters)) + : call.operation.function.parameters) as any, + }, + }, + ], + tool_choice: "auto", + parallel_tool_calls: false, + }); //---- // PROCESS COMPLETION @@ -285,32 +347,35 @@ export namespace ChatGptExecuteFunctionAgent { const toolCall: OpenAI.ChatCompletionMessageToolCall | undefined = ( completion.choices[0]?.message.tool_calls ?? [] ).find( - (tc) => tc.type === "function" && tc.function.name === call.function.name, + (tc) => + tc.type === "function" && tc.function.name === call.operation.name, ); if (toolCall === undefined) return null; return propagate( - props, + ctx, { id: toolCall.id, - function: call.function, - input: JSON.parse(toolCall.function.arguments), + type: "call", + operation: call.operation, + arguments: JSON.parse(toolCall.function.arguments), }, retry, ); }; - const fill = (props: { - function: IHttpLlmFunction<"chatgpt">; + const fillHttpArguments = (props: { + operation: INestiaAgentOperation; arguments: object; }): void => { - const route: IHttpMigrateRoute = props.function.route(); + if (props.operation.protocol !== "http") return; + const route: IHttpMigrateRoute = props.operation.function.route(); if ( route.body && route.operation().requestBody?.required === true && (props.arguments as any).body === undefined && isObject( - props.function.parameters.$defs, - props.function.parameters.properties.body!, + props.operation.function.parameters.$defs, + props.operation.function.parameters.properties.body!, ) ) (props.arguments as any).body = {}; @@ -331,9 +396,3 @@ export namespace ChatGptExecuteFunctionAgent { ); }; } - -interface IFunctionCall { - id: string; - function: IHttpLlmFunction<"chatgpt">; - input: object; -} diff --git a/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts b/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts index d23e6ecf2..a15d1aa56 100644 --- a/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts +++ b/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts @@ -1,21 +1,21 @@ import OpenAI from "openai"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; export namespace ChatGptHistoryDecoder { export const decode = ( - history: INestiaChatPrompt, + history: INestiaAgentPrompt, ): OpenAI.ChatCompletionMessageParam[] => { // NO NEED TO DECODE DESCRIBE - if (history.kind === "describe") return []; - else if (history.kind === "text") + if (history.type === "describe") return []; + else if (history.type === "text") return [ { role: history.role, content: history.text, }, ]; - else if (history.kind === "select" || history.kind === "cancel") + else if (history.type === "select" || history.type === "cancel") return [ { role: "assistant", @@ -24,9 +24,9 @@ export namespace ChatGptHistoryDecoder { type: "function", id: history.id, function: { - name: `${history.kind}Functions`, + name: `${history.type}Functions`, arguments: JSON.stringify({ - functions: history.functions.map((t) => ({ + functions: history.operations.map((t) => ({ name: t.function.name, reason: t.reason, })), @@ -60,14 +60,25 @@ export namespace ChatGptHistoryDecoder { tool_call_id: history.id, content: JSON.stringify({ function: { - method: history.function.method, - path: history.function.path, + protocol: history.protocol, description: history.function.description, parameters: history.function.parameters, output: history.function.output, + ...(history.protocol === "http" + ? { + method: history.function.method, + path: history.function.path, + } + : {}), }, - status: history.response.status, - data: history.response.body, + ...(history.protocol === "http" + ? { + status: history.value.status, + data: history.value.body, + } + : { + value: history.value, + }), }), }, ]; diff --git a/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts index 33cd584fd..ba3f034f0 100644 --- a/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts @@ -2,101 +2,83 @@ import { ILlmFunction } from "@samchon/openapi"; import OpenAI from "openai"; import typia from "typia"; -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentCostAggregator } from "../internal/NestiaChatAgentCostAggregator"; -import { NestiaChatAgentDefaultPrompt } from "../internal/NestiaChatAgentDefaultPrompt"; -import { NestiaChatAgentSystemPrompt } from "../internal/NestiaChatAgentSystemPrompt"; -import { IChatGptService } from "../structures/IChatGptService"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { NestiaAgentDefaultPrompt } from "../internal/NestiaAgentDefaultPrompt"; +import { NestiaAgentSystemPrompt } from "../internal/NestiaAgentSystemPrompt"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { __IChatInitialApplication } from "../structures/internal/__IChatInitialApplication"; import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; export namespace ChatGptInitializeFunctionAgent { - export interface IProps { - service: IChatGptService; - histories: INestiaChatPrompt[]; - content: string; - usage: INestiaChatTokenUsage; - config?: NestiaChatAgent.IConfig | undefined; - } - export interface IOutput { - mounted: boolean; - prompts: INestiaChatPrompt[]; - } - - export const execute = async (props: IProps): Promise => { + export const execute = async ( + ctx: INestiaAgentContext, + ): Promise => { //---- // EXECUTE CHATGPT API //---- - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + const completion: OpenAI.ChatCompletion = await ctx.request("initialize", { + messages: [ + // COMMON SYSTEM PROMPT + { + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // PREVIOUS HISTORIES + ...ctx.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: ctx.prompt.text, + }, { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // PREVIOUS HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // USER INPUT - { - role: "user", - content: props.content, - }, - { - // SYTEM PROMPT - role: "system", - content: - props.config?.systemPrompt?.initialize?.(props.histories) ?? - NestiaChatAgentSystemPrompt.INITIALIZE, - }, - ], - // GETTER FUNCTION - tools: [ - { - type: "function", - function: { - name: FUNCTION.name, - description: FUNCTION.description, - parameters: FUNCTION.parameters as any, - }, - }, - ], - tool_choice: "auto", - parallel_tool_calls: false, + // SYTEM PROMPT + role: "system", + content: + ctx.config?.systemPrompt?.initialize?.(ctx.histories) ?? + NestiaAgentSystemPrompt.INITIALIZE, }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); + ], + // GETTER FUNCTION + tools: [ + { + type: "function", + function: { + name: FUNCTION.name, + description: FUNCTION.description, + parameters: FUNCTION.parameters as any, + }, + }, + ], + tool_choice: "auto", + parallel_tool_calls: false, + }); //---- // PROCESS COMPLETION //---- - const prompts: INestiaChatPrompt[] = []; + const prompts: INestiaAgentPrompt[] = []; for (const choice of completion.choices) { if ( choice.message.role === "assistant" && !!choice.message.content?.length ) prompts.push({ - kind: "text", + type: "text", role: "assistant", text: choice.message.content, }); } - return { - mounted: completion.choices.some( + if ( + completion.choices.some( (c) => !!c.message.tool_calls?.some( (tc) => tc.type === "function" && tc.function.name === FUNCTION.name, ), - ), - prompts, - }; + ) + ) + await ctx.initialize(); + return prompts; }; } diff --git a/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts index e00cebf84..352535a1f 100644 --- a/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts +++ b/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts @@ -1,59 +1,43 @@ -import { - IHttpLlmApplication, - IHttpLlmFunction, - ILlmApplication, -} from "@samchon/openapi"; +import { IHttpLlmFunction, ILlmApplication } from "@samchon/openapi"; import OpenAI from "openai"; import typia, { IValidation } from "typia"; import { v4 } from "uuid"; -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentConstant } from "../internal/NestiaChatAgentConstant"; -import { NestiaChatAgentCostAggregator } from "../internal/NestiaChatAgentCostAggregator"; -import { NestiaChatAgentDefaultPrompt } from "../internal/NestiaChatAgentDefaultPrompt"; -import { NestiaChatAgentSystemPrompt } from "../internal/NestiaChatAgentSystemPrompt"; -import { IChatGptService } from "../structures/IChatGptService"; -import { INestiaChatEvent } from "../structures/INestiaChatEvent"; -import { INestiaChatFunctionSelection } from "../structures/INestiaChatFunctionSelection"; -import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { NestiaAgentConstant } from "../internal/NestiaAgentConstant"; +import { NestiaAgentDefaultPrompt } from "../internal/NestiaAgentDefaultPrompt"; +import { NestiaAgentPromptFactory } from "../internal/NestiaAgentPromptFactory"; +import { NestiaAgentSystemPrompt } from "../internal/NestiaAgentSystemPrompt"; +import { INestiaAgentContext } from "../structures/INestiaAgentContext"; +import { INestiaAgentController } from "../structures/INestiaAgentController"; +import { INestiaAgentEvent } from "../structures/INestiaAgentEvent"; +import { INestiaAgentOperation } from "../structures/INestiaAgentOperation"; +import { INestiaAgentOperationSelection } from "../structures/INestiaAgentOperationSelection"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; import { __IChatFunctionReference } from "../structures/internal/__IChatFunctionReference"; import { __IChatSelectFunctionsApplication } from "../structures/internal/__IChatSelectFunctionsApplication"; import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; export namespace ChatGptSelectFunctionAgent { - export interface IProps { - application: IHttpLlmApplication<"chatgpt">; - service: IChatGptService; - histories: INestiaChatPrompt[]; - stack: INestiaChatFunctionSelection[]; - dispatch: (event: INestiaChatEvent) => Promise; - usage: INestiaChatTokenUsage; - content: string; - config?: NestiaChatAgent.IConfig; - divide?: IHttpLlmFunction<"chatgpt">[][]; - completions?: OpenAI.ChatCompletion[]; - } - export const execute = async ( - props: IProps, - ): Promise => { - if (props.divide === undefined) - return step(props, props.application.functions, 0); + ctx: INestiaAgentContext, + ): Promise => { + if (ctx.operations.divided === undefined) + return step(ctx, ctx.operations.array, 0); - const stacks: INestiaChatFunctionSelection[][] = props.divide.map(() => []); - const events: INestiaChatEvent[] = []; - const prompts: INestiaChatPrompt[][] = await Promise.all( - props.divide.map((candidates, i) => + const stacks: INestiaAgentOperationSelection[][] = + ctx.operations.divided.map(() => []); + const events: INestiaAgentEvent[] = []; + const prompts: INestiaAgentPrompt[][] = await Promise.all( + ctx.operations.divided.map((operations, i) => step( { - ...props, + ...ctx, stack: stacks[i]!, dispatch: async (e) => { events.push(e); }, }, - candidates, + operations, 0, ), ), @@ -62,126 +46,127 @@ export namespace ChatGptSelectFunctionAgent { // NO FUNCTION SELECTION, SO THAT ONLY TEXT LEFT if (stacks.every((s) => s.length === 0)) return prompts[0]!; // ELITICISM - else if ( - (props.config?.eliticism ?? NestiaChatAgentConstant.ELITICISM) === true - ) + else if ((ctx.config?.eliticism ?? NestiaAgentConstant.ELITICISM) === true) return step( - props, + ctx, stacks - .map((row) => Array.from(row.values()).map((s) => s.function)) - .flat(), + .flat() + .map( + (s) => + ctx.operations.group + .get(s.controller.name)! + .get(s.function.name)!, + ), 0, ); // RE-COLLECT SELECT FUNCTION EVENTS - const collection: INestiaChatPrompt.ISelect = { + const collection: INestiaAgentPrompt.ISelect = { id: v4(), - kind: "select", - functions: [], + type: "select", + operations: [], }; for (const e of events) if (e.type === "select") { - collection.functions.push({ - function: e.function, - reason: e.reason, - }); - await selectFunction({ - application: props.application, - stack: props.stack, - dispatch: props.dispatch, - reference: { - name: e.function.name, + collection.operations.push( + NestiaAgentPromptFactory.selection({ + protocol: e.operation.protocol as "http", + controller: e.operation.controller as INestiaAgentController.IHttp, + function: e.operation.function as IHttpLlmFunction<"chatgpt">, reason: e.reason, - }, + name: e.operation.name, + }), + ); + await selectFunction(ctx, { + name: e.operation.name, + reason: e.reason, }); } return [collection]; }; const step = async ( - props: IProps, - candidates: IHttpLlmFunction<"chatgpt">[], + ctx: INestiaAgentContext, + operations: INestiaAgentOperation[], retry: number, failures?: IFailure[], - ): Promise => { + ): Promise => { //---- // EXECUTE CHATGPT API //---- - const completion: OpenAI.ChatCompletion = - await props.service.api.chat.completions.create( + const completion: OpenAI.ChatCompletion = await ctx.request("select", { + messages: [ + // COMMON SYSTEM PROMPT { - model: props.service.model, - messages: [ - // COMMON SYSTEM PROMPT - { - role: "system", - content: NestiaChatAgentDefaultPrompt.write(props.config), - } satisfies OpenAI.ChatCompletionSystemMessageParam, - // CANDIDATE FUNCTIONS - { - role: "assistant", - tool_calls: [ - { - type: "function", - id: "getApiFunctions", - function: { - name: "getApiFunctions", - arguments: JSON.stringify({}), - }, - }, - ], - }, - { - role: "tool", - tool_call_id: "getApiFunctions", - content: JSON.stringify( - candidates.map((func) => ({ - name: func.name, - method: func.method, - path: func.path, - description: func.description, - })), - ), - }, - // PREVIOUS HISTORIES - ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), - // USER INPUT - { - role: "user", - content: props.content, - }, - // SYTEM PROMPT + role: "system", + content: NestiaAgentDefaultPrompt.write(ctx.config), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + // CANDIDATE FUNCTIONS + { + role: "assistant", + tool_calls: [ { - role: "system", - content: NestiaChatAgentSystemPrompt.SELECT, + type: "function", + id: "getApiFunctions", + function: { + name: "getApiFunctions", + arguments: JSON.stringify({}), + }, }, - // TYPE CORRECTIONS - ...emendMessages(failures ?? []), ], - // STACK FUNCTIONS - tools: CONTAINER.functions.map( - (func) => - ({ - type: "function", - function: { - name: func.name, - description: func.description, - parameters: func.parameters as any, - }, - }) satisfies OpenAI.ChatCompletionTool, + }, + { + role: "tool", + tool_call_id: "getApiFunctions", + content: JSON.stringify( + operations.map((op) => ({ + name: op.name, + description: op.function.description, + ...(op.protocol === "http" + ? { + method: op.function.method, + path: op.function.path, + tags: op.function.tags, + } + : {}), + })), ), - tool_choice: "auto", - parallel_tool_calls: false, }, - props.service.options, - ); - NestiaChatAgentCostAggregator.aggregate(props.usage, completion); - if (props.completions !== undefined) props.completions.push(completion); + // PREVIOUS HISTORIES + ...ctx.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: ctx.prompt.text, + }, + // SYTEM PROMPT + { + role: "system", + content: NestiaAgentSystemPrompt.SELECT, + }, + // TYPE CORRECTIONS + ...emendMessages(failures ?? []), + ], + // STACK FUNCTIONS + tools: CONTAINER.functions.map( + (func) => + ({ + type: "function", + function: { + name: func.name, + description: func.description, + parameters: func.parameters as any, + }, + }) satisfies OpenAI.ChatCompletionTool, + ), + tool_choice: "auto", + parallel_tool_calls: false, + }); //---- // VALIDATION //---- - if (retry++ < (props.config?.retry ?? NestiaChatAgentConstant.RETRY)) { + if (retry++ < (ctx.config?.retry ?? NestiaAgentConstant.RETRY)) { const failures: IFailure[] = []; for (const choice of completion.choices) for (const tc of choice.message.tool_calls ?? []) { @@ -196,13 +181,13 @@ export namespace ChatGptSelectFunctionAgent { validation, }); } - if (failures.length > 0) return step(props, candidates, retry, failures); + if (failures.length > 0) return step(ctx, operations, retry, failures); } //---- // PROCESS COMPLETION //---- - const prompts: INestiaChatPrompt[] = []; + const prompts: INestiaAgentPrompt[] = []; for (const choice of completion.choices) { // TOOL CALLING HANDLER if (choice.message.tool_calls) @@ -214,26 +199,27 @@ export namespace ChatGptSelectFunctionAgent { ); if (typia.is(input) === false) continue; else if (tc.function.name === "selectFunctions") { - const collection: INestiaChatPrompt.ISelect = { + const collection: INestiaAgentPrompt.ISelect = { id: tc.id, - kind: "select", - functions: [], + type: "select", + operations: [], }; for (const reference of input.functions) { - const func: IHttpLlmFunction<"chatgpt"> | null = - await selectFunction({ - application: props.application, - stack: props.stack, - dispatch: props.dispatch, - reference, - }); - if (func !== null) - collection.functions.push({ - function: func, - reason: reference.reason, - }); + const operation: INestiaAgentOperation | null = + await selectFunction(ctx, reference); + if (operation !== null) + collection.operations.push( + NestiaAgentPromptFactory.selection({ + protocol: operation.protocol as "http", + controller: + operation.controller as INestiaAgentController.IHttp, + function: operation.function as IHttpLlmFunction<"chatgpt">, + name: operation.name, + reason: reference.reason, + }), + ); } - if (collection.functions.length !== 0) prompts.push(collection); + if (collection.operations.length !== 0) prompts.push(collection); } } @@ -243,7 +229,7 @@ export namespace ChatGptSelectFunctionAgent { !!choice.message.content?.length ) prompts.push({ - kind: "text", + type: "text", role: "assistant", text: choice.message.content, }); @@ -251,28 +237,29 @@ export namespace ChatGptSelectFunctionAgent { return prompts; }; - const selectFunction = async (props: { - application: IHttpLlmApplication<"chatgpt">; - stack: INestiaChatFunctionSelection[]; - reference: __IChatFunctionReference; - dispatch: (event: INestiaChatEvent.ISelectFunctionEvent) => Promise; - }): Promise | null> => { - const func: IHttpLlmFunction<"chatgpt"> | undefined = - props.application.functions.find( - (func) => func.name === props.reference.name, - ); - if (func === undefined) return null; + const selectFunction = async ( + ctx: INestiaAgentContext, + reference: __IChatFunctionReference, + ): Promise => { + const operation: INestiaAgentOperation | undefined = + ctx.operations.flat.get(reference.name); + if (operation === undefined) return null; - props.stack.push({ - function: func, - reason: props.reference.reason, - }); - await props.dispatch({ + ctx.stack.push( + NestiaAgentPromptFactory.selection({ + protocol: operation.protocol as "http", + controller: operation.controller as INestiaAgentController.IHttp, + function: operation.function as IHttpLlmFunction<"chatgpt">, + name: reference.name, + reason: reference.reason, + }), + ); + await ctx.dispatch({ type: "select", - function: func, - reason: props.reference.reason, + reason: reference.reason, + operation, }); - return func; + return operation; }; const emendMessages = ( diff --git a/packages/agent/src/functional/createHttpLlmApplication.ts b/packages/agent/src/functional/createHttpLlmApplication.ts new file mode 100644 index 000000000..8b16233a7 --- /dev/null +++ b/packages/agent/src/functional/createHttpLlmApplication.ts @@ -0,0 +1,35 @@ +import { + HttpLlm, + IHttpLlmApplication, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import typia, { IValidation } from "typia"; + +export const createHttpLlmApplication = (props: { + model: "chatgpt"; + document: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument + | OpenApi.IDocument; + options?: IHttpLlmApplication.IOptions<"chatgpt">; +}): IValidation> => { + const inspect: IValidation< + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument + | OpenApi.IDocument + > = typia.validate(props.document); + if (inspect.success === false) return inspect; + return { + success: true, + data: HttpLlm.application({ + model: props.model, + document: OpenApi.convert(props.document), + options: props.options, + }), + }; +}; diff --git a/packages/agent/src/internal/NestiaChatAgentConstant.ts b/packages/agent/src/internal/NestiaAgentConstant.ts similarity index 58% rename from packages/agent/src/internal/NestiaChatAgentConstant.ts rename to packages/agent/src/internal/NestiaAgentConstant.ts index ebd3ceb8c..29cbf1bfc 100644 --- a/packages/agent/src/internal/NestiaChatAgentConstant.ts +++ b/packages/agent/src/internal/NestiaAgentConstant.ts @@ -1,4 +1,4 @@ -export namespace NestiaChatAgentConstant { +export namespace NestiaAgentConstant { export const RETRY = 3; export const ELITICISM = true; } diff --git a/packages/agent/src/internal/NestiaChatAgentCostAggregator.ts b/packages/agent/src/internal/NestiaAgentCostAggregator.ts similarity index 86% rename from packages/agent/src/internal/NestiaChatAgentCostAggregator.ts rename to packages/agent/src/internal/NestiaAgentCostAggregator.ts index c90e2bc21..0f5c2ee7d 100644 --- a/packages/agent/src/internal/NestiaChatAgentCostAggregator.ts +++ b/packages/agent/src/internal/NestiaAgentCostAggregator.ts @@ -1,10 +1,10 @@ import OpenAI from "openai"; -import { INestiaChatTokenUsage } from "../structures/INestiaChatTokenUsage"; +import { INestiaAgentTokenUsage } from "../structures/INestiaAgentTokenUsage"; -export namespace NestiaChatAgentCostAggregator { +export namespace NestiaAgentCostAggregator { export const aggregate = ( - cost: INestiaChatTokenUsage, + cost: INestiaAgentTokenUsage, completion: OpenAI.ChatCompletion, ): void => { if (!completion.usage) return; diff --git a/packages/agent/src/internal/NestiaChatAgentDefaultPrompt.ts b/packages/agent/src/internal/NestiaAgentDefaultPrompt.ts similarity index 66% rename from packages/agent/src/internal/NestiaChatAgentDefaultPrompt.ts rename to packages/agent/src/internal/NestiaAgentDefaultPrompt.ts index bc946cc18..43374cd67 100644 --- a/packages/agent/src/internal/NestiaChatAgentDefaultPrompt.ts +++ b/packages/agent/src/internal/NestiaAgentDefaultPrompt.ts @@ -1,21 +1,19 @@ -import { NestiaChatAgent } from "../NestiaChatAgent"; -import { NestiaChatAgentSystemPrompt } from "./NestiaChatAgentSystemPrompt"; +import { INestiaAgentConfig } from "../structures/INestiaAgentConfig"; +import { NestiaAgentSystemPrompt } from "./NestiaAgentSystemPrompt"; import { Singleton } from "./Singleton"; -export namespace NestiaChatAgentDefaultPrompt { - export const write = ( - config?: NestiaChatAgent.IConfig | undefined, - ): string => { +export namespace NestiaAgentDefaultPrompt { + export const write = (config?: INestiaAgentConfig): string => { if (config?.systemPrompt?.common) return config?.systemPrompt?.common(config); const locale: string = config?.locale ?? getLocale.get(); const timezone: string = config?.timezone ?? getTimezone.get(); - return NestiaChatAgentSystemPrompt.COMMON.replace( - "${locale}", - locale, - ).replace("${timezone}", timezone); + return NestiaAgentSystemPrompt.COMMON.replace("${locale}", locale).replace( + "${timezone}", + timezone, + ); }; } diff --git a/packages/agent/src/internal/NestiaAgentOperationComposer.ts b/packages/agent/src/internal/NestiaAgentOperationComposer.ts new file mode 100644 index 000000000..ae7fa90e2 --- /dev/null +++ b/packages/agent/src/internal/NestiaAgentOperationComposer.ts @@ -0,0 +1,82 @@ +import { INestiaAgentConfig } from "../structures/INestiaAgentConfig"; +import { INestiaAgentController } from "../structures/INestiaAgentController"; +import { INestiaAgentOperation } from "../structures/INestiaAgentOperation"; +import { INestiaAgentOperationCollection } from "../structures/INestiaAgentOperationCollection"; +import { __map_take } from "./__map_take"; + +export namespace NestiaAgentOperationComposer { + export const compose = (props: { + controllers: INestiaAgentController[]; + config?: INestiaAgentConfig | undefined; + }): INestiaAgentOperationCollection => { + const unique: boolean = + props.controllers.length === 1 || + (() => { + const names: string[] = props.controllers + .map((controller) => + controller.application.functions.map((func) => func.name), + ) + .flat(); + return new Set(names).size === names.length; + })(); + const naming = (func: string, ci: number) => + unique ? func : `_${ci}_${func}`; + + const array: INestiaAgentOperation[] = props.controllers + .map((controller, ci) => + controller.protocol === "http" + ? controller.application.functions.map( + (func) => + ({ + protocol: "http", + controller, + function: func, + name: naming(func.name, ci), + }) satisfies INestiaAgentOperation.IHttp, + ) + : controller.application.functions.map( + (func) => + ({ + protocol: "class", + controller, + function: func, + name: naming(func.name, ci), + }) satisfies INestiaAgentOperation.IClass, + ), + ) + .flat(); + const divided: INestiaAgentOperation[][] | undefined = + !!props.config?.capacity && array.length > props.config.capacity + ? divideOperations({ + array, + capacity: props.config.capacity, + }) + : undefined; + + const flat: Map = new Map(); + const group: Map> = new Map(); + for (const item of array) { + flat.set(item.name, item); + __map_take(group, item.controller.name, () => new Map()).set( + item.name, + item, + ); + } + return { + array, + divided, + flat, + group, + }; + }; + + const divideOperations = (props: { + array: INestiaAgentOperation[]; + capacity: number; + }): INestiaAgentOperation[][] => { + const size: number = Math.ceil(props.array.length / props.capacity); + const capacity: number = Math.ceil(props.array.length / size); + const replica: INestiaAgentOperation[] = props.array.slice(); + return new Array(size).fill(0).map(() => replica.splice(0, capacity)); + }; +} diff --git a/packages/agent/src/internal/NestiaAgentPromptFactory.ts b/packages/agent/src/internal/NestiaAgentPromptFactory.ts new file mode 100644 index 000000000..adc5ad4f2 --- /dev/null +++ b/packages/agent/src/internal/NestiaAgentPromptFactory.ts @@ -0,0 +1,30 @@ +import { INestiaAgentOperationSelection } from "../structures/INestiaAgentOperationSelection"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; + +export namespace NestiaAgentPromptFactory { + export const execute = ( + props: Omit, + ): INestiaAgentPrompt.IExecute => + ({ + ...props, + toJSON: () => + ({ + ...props, + controller: props.controller.name, + function: props.function.name, + }) as any, + }) as INestiaAgentPrompt.IExecute; + + export const selection = ( + props: Omit, + ): INestiaAgentOperationSelection => + ({ + ...props, + toJSON: () => + ({ + ...props, + controller: props.controller.name, + function: props.function.name, + }) as any, + }) as INestiaAgentOperationSelection; +} diff --git a/packages/agent/src/internal/NestiaAgentPromptTransformer.ts b/packages/agent/src/internal/NestiaAgentPromptTransformer.ts new file mode 100644 index 000000000..4f40f3aca --- /dev/null +++ b/packages/agent/src/internal/NestiaAgentPromptTransformer.ts @@ -0,0 +1,82 @@ +import { Primitive } from "typia"; + +import { INestiaAgentOperation } from "../structures/INestiaAgentOperation"; +import { INestiaAgentPrompt } from "../structures/INestiaAgentPrompt"; +import { NestiaAgentPromptFactory } from "./NestiaAgentPromptFactory"; + +export namespace NestiaAgentPromptTransformer { + export const transform = (props: { + operations: Map>; + input: Primitive; + }): INestiaAgentPrompt => { + // TEXT + if (props.input.type === "text") return props.input; + // SELECT & CANCEL + else if (props.input.type === "select" || props.input.type === "cancel") + return { + ...props.input, + operations: props.input.operations.map((func) => + NestiaAgentPromptFactory.selection({ + ...findOperation({ + operations: props.operations, + input: func, + }), + reason: func.reason, + }), + ), + } satisfies INestiaAgentPrompt.ISelect | INestiaAgentPrompt.ICancel; + // EXECUTE + else if (props.input.type === "execute") + return transformExecute({ + operations: props.operations, + input: props.input, + }) satisfies INestiaAgentPrompt.IExecute; + // DESCRIBE + return { + type: "describe", + text: props.input.text, + executions: props.input.executions.map((next) => + transformExecute({ + operations: props.operations, + input: next, + }), + ), + } satisfies INestiaAgentPrompt.IDescribe; + }; + + const transformExecute = (props: { + operations: Map>; + input: Primitive; + }): INestiaAgentPrompt.IExecute => { + const operation = findOperation({ + operations: props.operations, + input: props.input, + }); + return NestiaAgentPromptFactory.execute({ + type: "execute", + protocol: operation.protocol as "http", + controller: operation.controller, + function: operation.function, + id: props.input.id, + arguments: props.input.arguments, + value: props.input.value, + }); + }; + + const findOperation = (props: { + operations: Map>; + input: { + controller: string; + function: string; + }; + }): INestiaAgentOperation.IHttp => { + const found: INestiaAgentOperation | undefined = props.operations + .get(props.input.controller) + ?.get(props.input.function); + if (found === undefined) + throw new Error( + `No operation found: (controller: ${props.input.controller}, function: ${props.input.function})`, + ); + return found as INestiaAgentOperation.IHttp; + }; +} diff --git a/packages/agent/src/internal/NestiaAgentSystemPrompt.ts b/packages/agent/src/internal/NestiaAgentSystemPrompt.ts new file mode 100644 index 000000000..ead011280 --- /dev/null +++ b/packages/agent/src/internal/NestiaAgentSystemPrompt.ts @@ -0,0 +1,14 @@ +export namespace NestiaAgentSystemPrompt { + export const CANCEL = + "You are a helpful assistant for cancelling functions which are prepared to call.\n\nUse the supplied tools to select some functions to cancel of `getApiFunctions()` returned.\n\nIf you can't find any proper function to select, don't talk, don't do anything."; + export const COMMON = + "At first, the user's language locale code is \"${locale}\". When you are conversating with the user, consider it and always translate to the target locale language. Never conversate with different locale language text with the user.\n\nAt second, the user's timezone is \"${timezone}\", and ISO datetime is ${datetime}. When you are conversating with the user, consider current time and user belonged timezone."; + export const DESCRIBE = + "You are a helpful assistant describing return values of function calls.\n\nAbove messages are the list of function call histories. When decribing the return values, please do not too much shortly summarize them. Instead, provide detailed descriptions as much as.\n\nAlso, its content format must be markdown. If required, utilize the mermaid syntax for drawing some diagrams. When image contents are, just put them through the markdown image syntax."; + export const EXECUTE = + "You are a helpful assistant for tool calling.\n\nUse the supplied tools to assist the user.\n\nIf previous messsages are not enough to compose the arguments, you can ask the user to write more information. By the way, when asking the user to write more informations, make the text concise and clear.\n\nFor reference, in the \"tool\" role message content, the `function` property means metadata of the API operation. In other words, it is the function schema describing its purpose, parameters and return value types. And then the `data` property is the return value from the target function calling."; + export const INITIALIZE = + "You are a helpful assistant.\n\nUse the supplied tools to assist the user."; + export const SELECT = + "You are a helpful assistant for selecting functions to call.\n\nUse the supplied tools to select some functions of `getApiFunctions()` returned.\n\nWhen selecting functions to call, pay attention to the relationship between functions. In particular, check the prerequisites between each function.\n\nIf you can't find any proper function to select, just type your own message."; +} diff --git a/packages/agent/src/internal/NestiaChatAgentSystemPrompt.ts b/packages/agent/src/internal/NestiaChatAgentSystemPrompt.ts deleted file mode 100644 index cde71c783..000000000 --- a/packages/agent/src/internal/NestiaChatAgentSystemPrompt.ts +++ /dev/null @@ -1,8 +0,0 @@ -export namespace NestiaChatAgentSystemPrompt { - export const CANCEL = "You are a helpful assistant for cancelling functions which are prepared to call.\n\nUse the supplied tools to select some functions to cancel of `getApiFunctions()` returned.\n\nIf you can't find any proper function to select, don't talk, don't do anything."; - export const COMMON = "At first, the user's language locale code is \"${locale}\". When you are conversating with the user, consider it and always translate to the target locale language. Never conversate with different locale language text with the user.\n\nAt second, the user's timezone is \"${timezone}\", and ISO datetime is ${datetime}. When you are conversating with the user, consider current time and user belonged timezone."; - export const DESCRIBE = "You are a helpful assistant describing return values of function calls.\n\nAbove messages are the list of function call histories. When decribing the return values, please do not too much shortly summarize them. Instead, provide detailed descriptions as much as.\n\nAlso, its content format must be markdown. If required, utilize the mermaid syntax for drawing some diagrams. When image contents are, just put them through the markdown image syntax."; - export const EXECUTE = "You are a helpful assistant for tool calling.\n\nUse the supplied tools to assist the user.\n\nIf previous messsages are not enough to compose the arguments, you can ask the user to write more information. By the way, when asking the user to write more informations, make the text concise and clear.\n\nFor reference, in the \"tool\" role message content, the `function` property means metadata of the API operation. In other words, it is the function schema describing its purpose, parameters and return value types. And then the `data` property is the return value from the target function calling."; - export const INITIALIZE = "You are a helpful assistant.\n\nUse the supplied tools to assist the user."; - export const SELECT = "You are a helpful assistant for selecting functions to call.\n\nUse the supplied tools to select some functions of `getApiFunctions()` returned.\n\nWhen selecting functions to call, pay attention to the relationship between functions. In particular, check the prerequisites between each function.\n\nIf you can't find any proper function to select, just type your own message."; -} diff --git a/packages/agent/src/internal/__map_take.ts b/packages/agent/src/internal/__map_take.ts new file mode 100644 index 000000000..07f59f32a --- /dev/null +++ b/packages/agent/src/internal/__map_take.ts @@ -0,0 +1,15 @@ +/** + * @internal + */ +export const __map_take = ( + dict: Map, + key: Key, + generator: () => T, +): T => { + const oldbie: T | undefined = dict.get(key); + if (oldbie) return oldbie; + + const value: T = generator(); + dict.set(key, value); + return value; +}; diff --git a/packages/agent/src/module.ts b/packages/agent/src/module.ts index 5c335bceb..fac82ba3f 100644 --- a/packages/agent/src/module.ts +++ b/packages/agent/src/module.ts @@ -1,7 +1,13 @@ -export * from "./structures/IChatGptService"; -export * from "./structures/INestiaChatEvent"; -export * from "./structures/INestiaChatFunctionSelection"; -export * from "./structures/INestiaChatPrompt"; -export * from "./structures/INestiaChatTokenUsage"; +export * from "./structures/INestiaAgentConfig"; +export * from "./structures/INestiaAgentController"; +export * from "./structures/INestiaAgentEvent"; +export * from "./structures/INestiaAgentOperation"; +export * from "./structures/INestiaAgentOperationSelection"; +export * from "./structures/INestiaAgentPrompt"; +export * from "./structures/INestiaAgentProps"; +export * from "./structures/INestiaAgentProvider"; +export * from "./structures/INestiaAgentSystemPrompt"; +export * from "./structures/INestiaAgentTokenUsage"; -export * from "./NestiaChatAgent"; +export * from "./functional/createHttpLlmApplication"; +export * from "./NestiaAgent"; diff --git a/packages/agent/src/structures/IChatGptService.ts b/packages/agent/src/structures/IChatGptService.ts deleted file mode 100644 index 818654554..000000000 --- a/packages/agent/src/structures/IChatGptService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import OpenAI from "openai"; - -/** - * Service of the ChatGPT (OpenAI) API. - */ -export interface IChatGptService { - /** - * OpenAI API instance. - */ - api: OpenAI; - - /** - * Chat model to be used. - */ - model: OpenAI.ChatModel; - - /** - * Options for the request. - */ - options?: OpenAI.RequestOptions | undefined; -} diff --git a/packages/agent/src/structures/INestiaAgentConfig.ts b/packages/agent/src/structures/INestiaAgentConfig.ts new file mode 100644 index 000000000..a9d341d92 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentConfig.ts @@ -0,0 +1,88 @@ +import { INestiaAgentSystemPrompt } from "./INestiaAgentSystemPrompt"; + +/** + * Configuration for Nestia Agent. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export interface INestiaAgentConfig { + /** + * Locale of the A.I. chatbot. + * + * If you configure this property, the A.I. chatbot will conversate with + * the given locale. You can get the locale value by + * + * - Browser: `navigator.language` + * - NodeJS: `process.env.LANG.split(".")[0]` + * + * @default your_locale + */ + locale?: string; + + /** + * Timezone of the A.I. chatbot. + * + * If you configure this property, the A.I. chatbot will consider the + * given timezone. You can get the timezone value by + * `Intl.DateTimeFormat().resolvedOptions().timeZone`. + * + * @default your_timezone + */ + timezone?: string; + + /** + * Retry count. + * + * If LLM function calling composed arguments are invalid, + * the A.I. chatbot will retry to call the function with + * the modified arguments. + * + * By the way, if you configure it to 0 or 1, the A.I. chatbot + * will not retry the LLM function calling for correcting the + * arguments. + * + * @default 3 + */ + retry?: number; + + /** + * Capacity of the LLM function selecting. + * + * When the A.I. chatbot selects a proper function to call, if the + * number of functions registered in the + * {@link INestiaAgentProps.applications} is too much greater, + * the A.I. chatbot often fallen into the hallucination. + * + * In that case, if you configure this property value, `NestiaChatAgent` + * will divide the functions into the several groups with the configured + * capacity and select proper functions to call by operating the multiple + * LLM function selecting agents parallelly. + * + * @default 0 + */ + capacity?: number; + + /** + * Eliticism for the LLM function selecting. + * + * If you configure {@link capacity}, the A.I. chatbot will complete + * the candidate functions to call which are selected by the multiple + * LLM function selecting agents. + * + * Otherwise you configure this property as `false`, the A.I. chatbot + * will not complete the candidate functions to call and just accept + * every candidate functions to call which are selected by the multiple + * LLM function selecting agents. + * + * @default true + */ + eliticism?: boolean; + + /** + * System prompt messages. + * + * System prompt messages if you want to customize the system prompt + * messages for each situation. + */ + systemPrompt?: INestiaAgentSystemPrompt; +} diff --git a/packages/agent/src/structures/INestiaAgentContext.ts b/packages/agent/src/structures/INestiaAgentContext.ts new file mode 100644 index 000000000..d4c114c97 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentContext.ts @@ -0,0 +1,28 @@ +import OpenAI from "openai"; + +import { NestiaAgentSource } from "../typings/NestiaAgentSource"; +import { INestiaAgentConfig } from "./INestiaAgentConfig"; +import { INestiaAgentEvent } from "./INestiaAgentEvent"; +import { INestiaAgentOperationCollection } from "./INestiaAgentOperationCollection"; +import { INestiaAgentOperationSelection } from "./INestiaAgentOperationSelection"; +import { INestiaAgentPrompt } from "./INestiaAgentPrompt"; + +export interface INestiaAgentContext { + // APPLICATION + operations: INestiaAgentOperationCollection; + config: INestiaAgentConfig | undefined; + + // STATES + histories: INestiaAgentPrompt[]; + stack: INestiaAgentOperationSelection[]; + prompt: INestiaAgentPrompt.IText; + ready: () => boolean; + + // HANDLERS + dispatch: (event: INestiaAgentEvent) => Promise; + request: ( + source: NestiaAgentSource, + body: Omit, + ) => Promise; + initialize: () => Promise; +} diff --git a/packages/agent/src/structures/INestiaAgentController.ts b/packages/agent/src/structures/INestiaAgentController.ts new file mode 100644 index 000000000..ea9f919e0 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentController.ts @@ -0,0 +1,35 @@ +import { + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpResponse, +} from "@samchon/openapi"; +import { ILlmApplicationOfValidate, ILlmFunctionOfValidate } from "typia"; + +export type INestiaAgentController = + | INestiaAgentController.IHttp + | INestiaAgentController.IClass; +export namespace INestiaAgentController { + export interface IHttp extends IBase<"http", IHttpLlmApplication<"chatgpt">> { + connection: IHttpConnection; + execute?: (props: { + connection: IHttpConnection; + application: IHttpLlmApplication<"chatgpt">; + function: IHttpLlmFunction<"chatgpt">; + arguments: object; + }) => Promise; + } + export interface IClass + extends IBase<"class", ILlmApplicationOfValidate<"chatgpt">> { + execute: (props: { + application: ILlmApplicationOfValidate<"chatgpt">; + function: ILlmFunctionOfValidate<"chatgpt">; + arguments: object; + }) => Promise; + } + interface IBase { + protocol: Protocol; + name: string; + application: Application; + } +} diff --git a/packages/agent/src/structures/INestiaAgentEvent.ts b/packages/agent/src/structures/INestiaAgentEvent.ts new file mode 100644 index 000000000..21ee139e3 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentEvent.ts @@ -0,0 +1,184 @@ +import OpenAI from "openai"; + +import { NestiaAgentSource } from "../typings/NestiaAgentSource"; +import { INestiaAgentOperation } from "./INestiaAgentOperation"; + +/** + * Nestia A.I. chatbot event. + * + * `INestiaAgentEvent` is an union type of all possible events that can + * be emitted by the A.I. chatbot of the {@link NestiaAgent} class. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export type INestiaAgentEvent = + | INestiaAgentEvent.IInitialize + | INestiaAgentEvent.ISelect + | INestiaAgentEvent.ICancel + | INestiaAgentEvent.ICall + | INestiaAgentEvent.IExecute + | INestiaAgentEvent.IRequest + | INestiaAgentEvent.IResponse; +export namespace INestiaAgentEvent { + export type Type = INestiaAgentEvent["type"]; + export type Mapper = { + initialize: IInitialize; + select: ISelect; + cancel: ICancel; + call: ICall; + execute: IExecute; + request: IRequest; + response: IResponse; + }; + + /** + * Event of initializing the chatbot. + */ + export interface IInitialize extends IBase<"initialize"> {} + + /** + * Event of selecting a function to call. + */ + export interface ISelect extends IBase<"select"> { + /** + * Selected operation. + * + * Operation that has been selected to prepare LLM function calling. + */ + operation: INestiaAgentOperation; + + /** + * Reason of selecting the function. + * + * The A.I. chatbot will fill this property describing why the function + * has been selected. + */ + reason: string; + } + + /** + * Event of canceling a function calling. + */ + export interface ICancel extends IBase<"cancel"> { + /** + * Selected operation to cancel. + * + * Operation that has been selected to prepare LLM function calling, + * but canceled due to no more required. + */ + operation: INestiaAgentOperation; + + /** + * Reason of selecting the function. + * + * The A.I. chatbot will fill this property describing why the function + * has been cancelled. + * + * For reference, if the A.I. chatbot successfully completes the LLM + * function calling, the reason of the fnction cancellation will be + * "complete". + */ + reason: string; + } + + /** + * Event of calling a function. + */ + export interface ICall extends IBase<"call"> { + /** + * ID of the tool calling. + */ + id: string; + + /** + * Target operation to call. + */ + operation: INestiaAgentOperation; + + /** + * Arguments of the function calling. + * + * If you modify this {@link arguments} property, it actually modifies + * the backend server's request. Therefore, be careful when you're + * trying to modify this property. + */ + arguments: object; + } + + /** + * Event of function calling execution. + */ + export interface IExecute extends IBase<"execute"> { + /** + * ID of the tool calling. + */ + id: string; + + /** + * Target operation had called. + */ + operation: INestiaAgentOperation; + + /** + * Arguments of the function calling. + */ + arguments: object; + + /** + * Return value. + */ + value: any; + } + + /** + * Request event of LLM provider API. + */ + export interface IRequest extends IBase<"request"> { + /** + * The source agent of the request. + */ + source: NestiaAgentSource; + + /** + * Request body. + */ + body: OpenAI.ChatCompletionCreateParamsNonStreaming; + + /** + * Options for the request. + */ + options?: OpenAI.RequestOptions | undefined; + } + + /** + * Response event of LLM provider API. + */ + export interface IResponse extends IBase<"response"> { + /** + * The source agent of the response. + */ + source: NestiaAgentSource; + + /** + * Request body. + */ + body: OpenAI.ChatCompletionCreateParamsNonStreaming; + + /** + * Options for the request. + */ + options?: OpenAI.RequestOptions | undefined; + + /** + * Return value from the LLM provider API. + */ + value: OpenAI.ChatCompletion; + } + + interface IBase { + /** + * Discriminator type. + */ + type: Type; + } +} diff --git a/packages/agent/src/structures/INestiaAgentOperation.ts b/packages/agent/src/structures/INestiaAgentOperation.ts new file mode 100644 index 000000000..82364a60a --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentOperation.ts @@ -0,0 +1,43 @@ +import { IHttpLlmFunction } from "@samchon/openapi"; +import { ILlmFunctionOfValidate } from "typia"; + +import { INestiaAgentController } from "./INestiaAgentController"; + +export type INestiaAgentOperation = + | INestiaAgentOperation.IHttp + | INestiaAgentOperation.IClass; +export namespace INestiaAgentOperation { + export type IHttp = IBase< + "http", + INestiaAgentController.IHttp, + IHttpLlmFunction<"chatgpt"> + >; + + export type IClass = IBase< + "class", + INestiaAgentController.IClass, + ILlmFunctionOfValidate<"chatgpt"> + >; + + interface IBase { + /** + * Protocol discriminator. + */ + protocol: Protocol; + + /** + * Belonged controller of the target function. + */ + controller: Application; + + /** + * Target function to call. + */ + function: Function; + + /** + * Identifier name. + */ + name: string; + } +} diff --git a/packages/agent/src/structures/INestiaAgentOperationCollection.ts b/packages/agent/src/structures/INestiaAgentOperationCollection.ts new file mode 100644 index 000000000..d28939be8 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentOperationCollection.ts @@ -0,0 +1,8 @@ +import { INestiaAgentOperation } from "./INestiaAgentOperation"; + +export interface INestiaAgentOperationCollection { + array: INestiaAgentOperation[]; + divided?: INestiaAgentOperation[][] | undefined; + flat: Map; + group: Map>; +} diff --git a/packages/agent/src/structures/INestiaAgentOperationSelection.ts b/packages/agent/src/structures/INestiaAgentOperationSelection.ts new file mode 100644 index 000000000..0aeab9554 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentOperationSelection.ts @@ -0,0 +1,61 @@ +import { IHttpLlmFunction } from "@samchon/openapi"; +import { ILlmFunctionOfValidate } from "typia"; + +import { INestiaAgentController } from "./INestiaAgentController"; + +/** + * Nestia agent operation selection. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export type INestiaAgentOperationSelection = + | INestiaAgentOperationSelection.IHttp + | INestiaAgentOperationSelection.IClass; +export namespace INestiaAgentOperationSelection { + export type IHttp = IBase< + "http", + INestiaAgentController.IHttp, + IHttpLlmFunction<"chatgpt"> + >; + + export type IClass = IBase< + "class", + INestiaAgentController.IClass, + ILlmFunctionOfValidate<"chatgpt"> + >; + + interface IBase { + /** + * Discriminator protocol. + */ + protocol: Protocol; + + /** + * Belonged controller of the target function. + */ + controller: Controller; + + /** + * Target function. + * + * Function that has been selected to prepare LLM function calling, + * or canceled due to no more required. + */ + function: Function; + + /** + * Identifier name of the target function. + * + * If {@link NestiaAgent} has multiple {@link INestiaAgentController}s, + * the `name` can be different from target function's name. + */ + name: string; + + /** + * The reason of the function selection or cancellation. + */ + reason: string; + + toJSON(): Omit, "toJSON">; + } +} diff --git a/packages/agent/src/structures/INestiaAgentPrompt.ts b/packages/agent/src/structures/INestiaAgentPrompt.ts new file mode 100644 index 000000000..ba1f92c31 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentPrompt.ts @@ -0,0 +1,155 @@ +import { IHttpLlmFunction, IHttpResponse } from "@samchon/openapi"; +import { ILlmFunctionOfValidate } from "typia"; + +import { INestiaAgentController } from "./INestiaAgentController"; +import { INestiaAgentOperationSelection } from "./INestiaAgentOperationSelection"; + +/** + * Nestia A.I. chatbot prompt. + * + * `INestiaChatPrompt` is an union type of all possible prompts that can + * be generated by the A.I. chatbot of the {@link NestiaChatAgent} class. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export type INestiaAgentPrompt = + | INestiaAgentPrompt.IText + | INestiaAgentPrompt.ISelect + | INestiaAgentPrompt.ICancel + | INestiaAgentPrompt.IExecute + | INestiaAgentPrompt.IDescribe; +export namespace INestiaAgentPrompt { + /** + * Select prompt. + * + * Selection prompt about candidate functions to call. + */ + export interface ISelect extends IBase<"select"> { + /** + * ID of the LLM tool call result. + */ + id: string; + + /** + * Operations that have been selected. + */ + operations: INestiaAgentOperationSelection[]; + } + + /** + * Cancel prompt. + * + * Cancellation prompt about the candidate functions to be discarded. + */ + export interface ICancel extends IBase<"cancel"> { + /** + * ID of the LLM tool call result. + */ + id: string; + + /** + * Operations that have been cancelled. + */ + operations: INestiaAgentOperationSelection[]; + } + + /** + * Execute prompt. + * + * Execution prompt about the LLM function calling. + */ + export type IExecute = IExecute.IHttp | IExecute.IClass; + export namespace IExecute { + export type IHttp = IBase< + "http", + INestiaAgentController.IHttp, + IHttpLlmFunction<"chatgpt">, + IHttpResponse + >; + export type IClass = IBase< + "class", + INestiaAgentController.IClass, + ILlmFunctionOfValidate<"chatgpt">, + any + >; + interface IBase { + /** + * Discriminator type. + */ + type: "execute"; + + /** + * Protocol discriminator. + */ + protocol: Protocol; + + /** + * Belonged controller of the target function. + */ + controller: Controller; + + /** + * Target function to call. + */ + function: Function; + + /** + * ID of the LLM tool call result. + */ + id: string; + + /** + * Arguments of the LLM function calling. + */ + arguments: object; + + /** + * Return value. + */ + value: Value; + + toJSON(): Omit, "toJSON">; + } + } + + /** + * Description prompt. + * + * Description prompt about the return value of the LLM function calling. + */ + export interface IDescribe extends IBase<"describe"> { + /** + * Executions of the LLM function calling. + * + * This prompt describes the return value of them. + */ + executions: IExecute[]; + + /** + * Description text. + */ + text: string; + } + + /** + * Text prompt. + */ + export interface IText extends IBase<"text"> { + /** + * Role of the orator. + */ + role: "assistant" | "user"; + + /** + * The text content. + */ + text: string; + } + + interface IBase { + /** + * Discriminator type. + */ + type: Type; + } +} diff --git a/packages/agent/src/structures/INestiaAgentProps.ts b/packages/agent/src/structures/INestiaAgentProps.ts new file mode 100644 index 000000000..d40415299 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentProps.ts @@ -0,0 +1,13 @@ +import { Primitive } from "typia"; + +import { INestiaAgentConfig } from "./INestiaAgentConfig"; +import { INestiaAgentController } from "./INestiaAgentController"; +import { INestiaAgentPrompt } from "./INestiaAgentPrompt"; +import { INestiaAgentProvider } from "./INestiaAgentProvider"; + +export interface INestiaAgentProps { + controllers: INestiaAgentController[]; + provider: INestiaAgentProvider; + config?: INestiaAgentConfig; + histories?: Primitive[]; +} diff --git a/packages/agent/src/structures/INestiaAgentProvider.ts b/packages/agent/src/structures/INestiaAgentProvider.ts new file mode 100644 index 000000000..e376831f4 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentProvider.ts @@ -0,0 +1,31 @@ +import OpenAI from "openai"; + +/** + * LLM Provider for Nestia Chat. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export type INestiaAgentProvider = INestiaAgentProvider.IChatGpt; +export namespace INestiaAgentProvider { + export interface IChatGpt { + /** + * Discriminator type. + */ + type: "chatgpt"; + + /** + * OpenAI API instance. + */ + api: OpenAI; + + /** + * Chat model to be used. + */ + model: OpenAI.ChatModel; + + /** + * Options for the request. + */ + options?: OpenAI.RequestOptions | undefined; + } +} diff --git a/packages/agent/src/structures/INestiaAgentSystemPrompt.ts b/packages/agent/src/structures/INestiaAgentSystemPrompt.ts new file mode 100644 index 000000000..c0fa4fde1 --- /dev/null +++ b/packages/agent/src/structures/INestiaAgentSystemPrompt.ts @@ -0,0 +1,11 @@ +import { INestiaAgentConfig } from "./INestiaAgentConfig"; +import { INestiaAgentPrompt } from "./INestiaAgentPrompt"; + +export interface INestiaAgentSystemPrompt { + common?: (config?: INestiaAgentConfig | undefined) => string; + initialize?: (histories: INestiaAgentPrompt[]) => string; + select?: (histories: INestiaAgentPrompt[]) => string; + cancel?: (histories: INestiaAgentPrompt[]) => string; + execute?: (histories: INestiaAgentPrompt[]) => string; + describe?: (histories: INestiaAgentPrompt.IExecute[]) => string; +} diff --git a/packages/agent/src/structures/INestiaChatTokenUsage.ts b/packages/agent/src/structures/INestiaAgentTokenUsage.ts similarity index 61% rename from packages/agent/src/structures/INestiaChatTokenUsage.ts rename to packages/agent/src/structures/INestiaAgentTokenUsage.ts index 98b4b3d09..3976a8beb 100644 --- a/packages/agent/src/structures/INestiaChatTokenUsage.ts +++ b/packages/agent/src/structures/INestiaAgentTokenUsage.ts @@ -1,9 +1,9 @@ -export interface INestiaChatTokenUsage { +export interface INestiaAgentTokenUsage { total: number; - prompt: INestiaChatTokenUsage.IPrompt; - completion: INestiaChatTokenUsage.ICompletion; + prompt: INestiaAgentTokenUsage.IPrompt; + completion: INestiaAgentTokenUsage.ICompletion; } -export namespace INestiaChatTokenUsage { +export namespace INestiaAgentTokenUsage { export interface IPrompt { total: number; audio: number; diff --git a/packages/agent/src/structures/INestiaChatAgent.ts b/packages/agent/src/structures/INestiaChatAgent.ts deleted file mode 100644 index 0684055a6..000000000 --- a/packages/agent/src/structures/INestiaChatAgent.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { INestiaChatEvent } from "./INestiaChatEvent"; -import { INestiaChatPrompt } from "./INestiaChatPrompt"; -import { INestiaChatTokenUsage } from "./INestiaChatTokenUsage"; - -export interface INestiaChatAgent { - conversate(content: string): Promise; - - getHistories(): INestiaChatPrompt[]; - - getTokenUsage(): INestiaChatTokenUsage; - - on( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void | Promise, - ): void; - - off( - type: Type, - listener: (event: INestiaChatEvent.Mapper[Type]) => void | Promise, - ): void; -} diff --git a/packages/agent/src/structures/INestiaChatEvent.ts b/packages/agent/src/structures/INestiaChatEvent.ts deleted file mode 100644 index 159963f00..000000000 --- a/packages/agent/src/structures/INestiaChatEvent.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { IHttpLlmFunction, IHttpResponse } from "@samchon/openapi"; - -/** - * Nestia A.I. chatbot event. - * - * `INestiaChatEvent` is an union type of all possible events that can - * be emitted by the A.I. chatbot of the {@link NestiaChatAgent} class. - * - * @author Jeongho Nam - https://github.com/samchon - */ -export type INestiaChatEvent = - | INestiaChatEvent.IIintializeEvent - | INestiaChatEvent.ISelectFunctionEvent - | INestiaChatEvent.ICancelFunctionEvent - | INestiaChatEvent.ICallFunctionEvent - | INestiaChatEvent.IFunctionCompleteEvent; -export namespace INestiaChatEvent { - /** - * Event of initializing the chatbot. - */ - export interface IIintializeEvent { - type: "initialize"; - } - - /** - * Event of selecting a function to call. - */ - export interface ISelectFunctionEvent { - type: "select"; - - /** - * Selected function. - * - * Function that has been selected to prepare LLM function calling. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * Reason of selecting the function. - * - * The A.I. chatbot will fill this property describing why the function - * has been selected. - */ - reason: string; - } - - /** - * Event of canceling a function calling. - */ - export interface ICancelFunctionEvent { - type: "cancel"; - - /** - * Selected function to cancel. - * - * Function that has been selected to prepare LLM function calling, - * but canceled due to no more required. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * Reason of selecting the function. - * - * The A.I. chatbot will fill this property describing why the function - * has been cancelled. - * - * For reference, if the A.I. chatbot successfully completes the LLM - * function calling, the reason of the fnction cancellation will be - * "complete". - */ - reason: string; - } - - /** - * Event of calling a function. - */ - export interface ICallFunctionEvent { - type: "call"; - - /** - * Target function to call. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * Arguments of the function calling. - * - * If you modify this {@link arguments} property, it actually modifies - * the backend server's request. Therefore, be careful when you're - * trying to modify this property. - */ - arguments: object; - } - - /** - * Event of completing a function calling. - */ - export interface IFunctionCompleteEvent { - type: "complete"; - - /** - * Target funtion that has been called. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * Arguments of the function calling. - */ - arguments: object; - - /** - * Response of the function calling. - */ - response: IHttpResponse; - } - - export type Type = INestiaChatEvent["type"]; - export type Mapper = { - initialize: IIintializeEvent; - select: ISelectFunctionEvent; - cancel: ICancelFunctionEvent; - call: ICallFunctionEvent; - complete: IFunctionCompleteEvent; - }; -} diff --git a/packages/agent/src/structures/INestiaChatFunctionSelection.ts b/packages/agent/src/structures/INestiaChatFunctionSelection.ts deleted file mode 100644 index 13cc1bc56..000000000 --- a/packages/agent/src/structures/INestiaChatFunctionSelection.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IHttpLlmFunction } from "@samchon/openapi"; - -/** - * Nestia A.I. chatbot function selection. - * - * @author Jeongho Nam - https://github.com/samchon - */ -export interface INestiaChatFunctionSelection { - /** - * Target function. - * - * Function that has been selected to prepare LLM function calling, - * or canceled due to no more required. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * The reason of the function selection or cancellation. - */ - reason: string; -} diff --git a/packages/agent/src/structures/INestiaChatPrompt.ts b/packages/agent/src/structures/INestiaChatPrompt.ts deleted file mode 100644 index 2bd8514c9..000000000 --- a/packages/agent/src/structures/INestiaChatPrompt.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { IHttpLlmFunction, IHttpResponse } from "@samchon/openapi"; - -import { INestiaChatFunctionSelection } from "./INestiaChatFunctionSelection"; - -/** - * Nestia A.I. chatbot prompt. - * - * `INestiaChatPrompt` is an union type of all possible prompts that can - * be generated by the A.I. chatbot of the {@link NestiaChatAgent} class. - * - * @author Jeongho Nam - https://github.com/samchon - */ -export type INestiaChatPrompt = - | INestiaChatPrompt.IText - | INestiaChatPrompt.ISelect - | INestiaChatPrompt.ICancel - | INestiaChatPrompt.IExecute - | INestiaChatPrompt.IDescribe; -export namespace INestiaChatPrompt { - /** - * Select prompt. - * - * Selection prompt about candidate functions to call. - */ - export interface ISelect { - kind: "select"; - - /** - * ID of the LLM tool call result. - */ - id: string; - - /** - * Functions that have been selected. - */ - functions: INestiaChatFunctionSelection[]; - } - - /** - * Cancel prompt. - * - * Cancellation prompt about the candidate functions to be discarded. - */ - export interface ICancel { - kind: "cancel"; - - /** - * ID of the LLM tool call result. - */ - id: string; - - /** - * Functions that have been cancelled. - */ - functions: INestiaChatFunctionSelection[]; - } - - /** - * Execute prompt. - * - * Execution prompt about the LLM function calling. - */ - export interface IExecute { - kind: "execute"; - role: "assistant"; - - /** - * ID of the LLM tool call result. - */ - id: string; - - /** - * Target function to call. - */ - function: IHttpLlmFunction<"chatgpt">; - - /** - * Arguments of the LLM function calling. - */ - arguments: object; - - /** - * Response of the LLM function calling execution. - */ - response: IHttpResponse; - } - - /** - * Description prompt. - * - * Description prompt about the return value of the LLM function calling. - */ - export interface IDescribe { - kind: "describe"; - - /** - * Executions of the LLM function calling. - * - * This prompt describes the return value of them. - */ - executions: IExecute[]; - - /** - * Description text. - */ - text: string; - } - - /** - * Text prompt. - */ - export interface IText { - kind: "text"; - - /** - * Role of the orator. - */ - role: "assistant" | "user"; - - /** - * The text content. - */ - text: string; - } -} diff --git a/packages/agent/src/typings/NestiaAgentSource.ts b/packages/agent/src/typings/NestiaAgentSource.ts new file mode 100644 index 000000000..1bca1fa95 --- /dev/null +++ b/packages/agent/src/typings/NestiaAgentSource.ts @@ -0,0 +1,6 @@ +export type NestiaAgentSource = + | "initialize" + | "select" + | "cancel" + | "execute" + | "describe"; diff --git a/packages/agent/test/cli.ts b/packages/agent/test/cli.ts index 9c5c798ab..0a1f72046 100644 --- a/packages/agent/test/cli.ts +++ b/packages/agent/test/cli.ts @@ -1,4 +1,4 @@ -import { INestiaChatPrompt, NestiaChatAgent } from "@nestia/agent"; +import { INestiaAgentPrompt, NestiaAgent } from "@nestia/agent"; import { HttpLlm, IHttpConnection, @@ -70,8 +70,9 @@ const main = async (): Promise => { ); // COMPOSE CHAT AGENT - const agent: NestiaChatAgent = new NestiaChatAgent({ - service: { + const agent: NestiaAgent = new NestiaAgent({ + provider: { + type: "chatgpt", api: new OpenAI({ apiKey: TestGlobal.env.CHATGPT_API_KEY, baseURL: TestGlobal.env.CHATGPT_BASE_URL, @@ -81,32 +82,42 @@ const main = async (): Promise => { ? JSON.parse(TestGlobal.env.CHATGPT_OPTIONS) : undefined, }, - connection, - application, + controllers: [ + { + protocol: "http", + name: "shopping", + connection, + application, + }, + ], config: { locale: "en-US", }, }); agent.on("initialize", () => console.log(chalk.greenBright("Initialized"))); agent.on("select", (e) => - console.log(chalk.cyanBright("selected"), e.function.name, e.reason), + console.log( + chalk.cyanBright("selected"), + e.operation.function.name, + e.reason, + ), ); agent.on("call", (e) => - console.log(chalk.blueBright("call"), e.function.name), + console.log(chalk.blueBright("call"), e.operation.function.name), ); - agent.on("complete", (e) => { + agent.on("execute", (e) => { console.log( - chalk.greenBright("completed"), - e.function.name, - e.response.status, + chalk.greenBright("execute"), + e.operation.function.name, + e.value.status, ), fs.writeFileSync( - `${TestGlobal.ROOT}/logs/${e.function.name}.log`, + `${TestGlobal.ROOT}/logs/${e.operation.function.name}.log`, JSON.stringify( { type: "function", arguments: e.arguments, - response: e.response, + response: e.value, }, null, 2, @@ -115,7 +126,11 @@ const main = async (): Promise => { ); }); agent.on("cancel", (e) => - console.log(chalk.redBright("canceled"), e.function.name, e.reason), + console.log( + chalk.redBright("canceled"), + e.operation.function.name, + e.reason, + ), ); // START CONVERSATION @@ -131,11 +146,11 @@ const main = async (): Promise => { JSON.stringify(agent.getTokenUsage(), null, 2), ); else { - const histories: INestiaChatPrompt[] = await agent.conversate(content); - for (const h of histories) - if (h.kind === "text") + const histories: INestiaAgentPrompt[] = await agent.conversate(content); + for (const h of histories.slice(1)) + if (h.type === "text") trace(chalk.yellow("Text"), chalk.blueBright(h.role), "\n\n", h.text); - else if (h.kind === "describe") + else if (h.type === "describe") trace( chalk.whiteBright("Describe"), chalk.blueBright("agent"), diff --git a/packages/agent/test/index.ts b/packages/agent/test/index.ts index 4aa37baee..a0e7034b9 100644 --- a/packages/agent/test/index.ts +++ b/packages/agent/test/index.ts @@ -1,4 +1,4 @@ -import { NestiaChatAgent } from "@nestia/agent"; +import { NestiaAgent } from "@nestia/agent"; import { DynamicExecutor } from "@nestia/e2e"; import { HttpLlm, @@ -57,15 +57,22 @@ const main = async (): Promise => { ); // COMPOSE CHAT AGENT - const agent: NestiaChatAgent = new NestiaChatAgent({ - service: { + const agent: NestiaAgent = new NestiaAgent({ + provider: { + type: "chatgpt", api: new OpenAI({ apiKey: TestGlobal.env.CHATGPT_API_KEY, }), model: "gpt-4o", }, - connection, - application, + controllers: [ + { + protocol: "http", + name: "shopping", + application, + connection, + }, + ], }); const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ diff --git a/packages/chat/.gitignore b/packages/chat/.gitignore new file mode 100644 index 000000000..b48a9caf1 --- /dev/null +++ b/packages/chat/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +lib +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/chat/LICENSE b/packages/chat/LICENSE new file mode 100644 index 000000000..078d83d13 --- /dev/null +++ b/packages/chat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jeongho Nam + +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. diff --git a/packages/chat/README.md b/packages/chat/README.md new file mode 100644 index 000000000..8e40440ee --- /dev/null +++ b/packages/chat/README.md @@ -0,0 +1,87 @@ +# Nestia +![Nestia Logo](https://nestia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/fetcher.svg)](https://www.npmjs.com/package/@nestia/fetcher) +[![Downloads](https://img.shields.io/npm/dm/@nestia/fetcher.svg)](https://www.npmjs.com/package/@nestia/fetcher) +[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) +[![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ) + +Nestia is a set of helper libraries for NestJS, supporting below features: + + - `@nestia/core`: + - Super-fast/easy decorators + - Advanced WebSocket routes + - `@nestia/sdk`: + - Swagger generator evolved than ever + - SDK library generator for clients + - Mockup Simulator for client applications + - Automatic E2E test functions generator + - `@nestia/e2e`: Test program utilizing e2e test functions + - `@nestia/benchmark`: Benchmark program using e2e test functions + - `@nestia/migrate`: OpenAPI generator from Swagger to NestJS/SDK + - `@nestia/editor`: Swagger-UI with Online TypeScript Editor + - `nestia`: Just CLI (command line interface) tool + +> [!NOTE] +> +> - **Only one line** required, with pure TypeScript type +> - Enhance performance **30x** up +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` +> - Software Development Kit +> - Collection of typed `fetch` functions with DTO structures like [tRPC](https://trpc.io/) +> - Mockup simulator means embedded backend simulator in the SDK +> - similar with [msw](https://mswjs.io/), but fully automated + +![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif) + +> Left is NestJS server code, and right is client (frontend) code utilizing SDK + + + + +## Sponsors and Backers +Thanks for your support. + +Your donation would encourage `nestia` development. + +[![Backers](https://opencollective.com/nestia/backers.svg?avatarHeight=75&width=600)](https://opencollective.com/nestia) + + + + +## Guide Documents +Check out the document in the [website](https://nestia.io/docs/): + +### 🏠 Home + - [Introduction](https://nestia.io/docs/) + - [Setup](https://nestia.io/docs/setup/) + - [Pure TypeScript](https://nestia.io/docs/pure) + +### 📖 Features + - Core Library + - [WebSocketRoute](https://nestia.io/docs/core/WebSocketRoute) + - [TypedRoute](https://nestia.io/docs/core/TypedRoute/) + - [TypedBody](https://nestia.io/docs/core/TypedBody/) + - [TypedParam](https://nestia.io/docs/core/TypedParam/) + - [TypedQuery](https://nestia.io/docs/core/TypedQuery/) + - [TypedHeaders](https://nestia.io/docs/core/TypedHeaders/) + - [TypedException](https://nestia.io/docs/core/TypedException/) + - Generators + - [Swagger Documents](https://nestia.io/docs/sdk/swagger/) + - [Software Development Kit](https://nestia.io/docs/sdk/sdk/) + - [E2E Functions](https://nestia.io/docs/sdk/e2e/) + - [Mockup Simulator](https://nestia.io/docs/sdk/simulator/) + - E2E Testing + - [Why E2E Test?](https://nestia.io/docs/e2e/why/) + - [Test Program Development](https://nestia.io/docs/e2e/development/) + - [Performance Benchmark](https://nestia.io/docs/e2e/benchmark/) + - [Swagger to NestJS](https://nestia.io/docs/migrate/) + - [TypeScript Swagger Editor](https://nestia.io/docs/editor/) + +### 🔗 Appendix + - [API Documents](https://nestia.io/api) + - [⇲ Benchmark Result](https://github.com/samchon/nestia/tree/master/benchmark/results/11th%20Gen%20Intel(R)%20Core(TM)%20i5-1135G7%20%40%202.40GHz) + - [⇲ `dev.to` Articles](https://dev.to/samchon/series/22751) diff --git a/packages/chat/eslint.config.js b/packages/chat/eslint.config.js new file mode 100644 index 000000000..092408a9f --- /dev/null +++ b/packages/chat/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/packages/chat/package.json b/packages/chat/package.json new file mode 100644 index 000000000..9483d109d --- /dev/null +++ b/packages/chat/package.json @@ -0,0 +1,78 @@ +{ + "name": "@nestia/chat", + "version": "0.2.1", + "main": "lib/index.js", + "module": "lib/index.mjs", + "typings": "lib/index.d.ts", + "description": "Super A.I. Chatbot application by Swagger Document", + "scripts": { + "build": "npm run build:static && npm run build:lib", + "build:static": "rimraf dist && tsc -b && vite build", + "build:lib": "rimraf lib && tsc --project tsconfig.lib.json && rollup -c", + "dev": "vite", + "lint": "eslint .", + "preview": "vite preview" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/nestia" + }, + "keywords": [ + "openapi", + "swagger", + "generator", + "typescript", + "editor", + "sdk", + "nestjs", + "nestia" + ], + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/nestia/issues" + }, + "homepage": "https://nestia.io", + "dependencies": { + "@mui/material": "^5.15.6", + "@nestia/agent": "workspace:^", + "@samchon/openapi": "^2.3.2", + "js-yaml": "^4.1.0", + "prettier": "3.3.3", + "react-mui-fileuploader": "^0.5.2", + "typia": "^7.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@nestjs/common": "^10.4.13", + "@nestjs/core": "^10.4.13", + "@nestjs/platform-express": "^10.4.13", + "@nestjs/platform-fastify": "^10.4.13", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.1", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.8.6", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.13", + "globals": "^15.11.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rollup": "^4.24.2", + "ts-node": "^10.9.2", + "typescript": "~5.7.2", + "typescript-eslint": "^8.10.0", + "vite": "^5.4.9" + }, + "files": [ + "README.md", + "LICENSE", + "package.json", + "dist", + "lib", + "src" + ] +} \ No newline at end of file diff --git a/packages/chat/rollup.config.js b/packages/chat/rollup.config.js new file mode 100644 index 000000000..9dac511e6 --- /dev/null +++ b/packages/chat/rollup.config.js @@ -0,0 +1,29 @@ +const typescript = require("@rollup/plugin-typescript"); +const terser = require("@rollup/plugin-terser"); + +module.exports = { + input: "./src/index.ts", + output: { + dir: "lib", + format: "esm", + entryFileNames: "[name].mjs", + sourcemap: true, + }, + plugins: [ + typescript({ + tsconfig: "tsconfig.lib.json", + module: "ES2020", + target: "ES2020", + }), + terser({ + format: { + comments: "some", + beautify: true, + ecma: "2020", + }, + compress: false, + mangle: false, + module: true, + }), + ], +}; diff --git a/packages/chat/src/NestiaChatApplication.tsx b/packages/chat/src/NestiaChatApplication.tsx new file mode 100644 index 000000000..b6095ad93 --- /dev/null +++ b/packages/chat/src/NestiaChatApplication.tsx @@ -0,0 +1,11 @@ +import { IChatGptService } from "@nestia/agent"; +import { IHttpConnection, OpenApi } from "@samchon/openapi"; + +export const NestiaChatApplication = () => {}; +export namespace NestiaChatApplication { + export interface IProps { + swagger?: string | OpenApi.IDocument; + connection?: IHttpConnection; + service?: IChatGptService; + } +} diff --git a/packages/chat/src/NestiaChatUploader.tsx b/packages/chat/src/NestiaChatUploader.tsx new file mode 100644 index 000000000..5c9076c55 --- /dev/null +++ b/packages/chat/src/NestiaChatUploader.tsx @@ -0,0 +1,10 @@ +import { IChatGptService } from "@nestia/agent"; +import { IHttpConnection } from "@samchon/openapi"; + +export const NestiaChatUploader = () => {}; +export namespace NestiaChatUploader { + export interface IProps { + connection?: IHttpConnection; + service?: IChatGptService; + } +} diff --git a/packages/chat/src/index.ts b/packages/chat/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/chat/src/main.tsx b/packages/chat/src/main.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/chat/src/movies/NestiaChatMovie.tsx b/packages/chat/src/movies/NestiaChatMovie.tsx new file mode 100644 index 000000000..fc1d3a743 --- /dev/null +++ b/packages/chat/src/movies/NestiaChatMovie.tsx @@ -0,0 +1,41 @@ +import { AppBar, Button, Input, Toolbar, Typography } from "@mui/material"; +import { NestiaChatAgent } from "@nestia/agent"; +import React from "react"; + +export const NestiaChatMovie = () => { + const handleKeyUp = async (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + await handleSend(); + } + }; + + const handleSend = async () => {}; + + return ( + + + + Nestia A.I. Chatbot + + + + + + + + + + ); +}; +export namespace NestiaChatMovie { + export interface IProps { + agent: NestiaChatAgent; + } +} diff --git a/packages/chat/src/movies/prompts/NestiaChatDescribePromptMovie.tsx b/packages/chat/src/movies/prompts/NestiaChatDescribePromptMovie.tsx new file mode 100644 index 000000000..9f4812134 --- /dev/null +++ b/packages/chat/src/movies/prompts/NestiaChatDescribePromptMovie.tsx @@ -0,0 +1,8 @@ +import { INestiaChatPrompt } from "@nestia/agent"; + +export const NestiaChatDescribePromptMovie = () => {}; +export namespace NestiaChatDescribePromptMovie { + export interface IProps { + prompt: INestiaChatPrompt.IDescribe; + } +} diff --git a/packages/chat/src/movies/prompts/NestiaChatExecutePromptMovie.tsx b/packages/chat/src/movies/prompts/NestiaChatExecutePromptMovie.tsx new file mode 100644 index 000000000..b97812143 --- /dev/null +++ b/packages/chat/src/movies/prompts/NestiaChatExecutePromptMovie.tsx @@ -0,0 +1,7 @@ +import { INestiaChatPrompt } from "@nestia/agent"; + +export namespace NestiaChatFunctionPromptMovie { + export interface IProps { + prompt: INestiaChatPrompt.IExecute; + } +} diff --git a/packages/chat/src/movies/prompts/NestiaChatPromptMovie.tsx b/packages/chat/src/movies/prompts/NestiaChatPromptMovie.tsx new file mode 100644 index 000000000..e973b233e --- /dev/null +++ b/packages/chat/src/movies/prompts/NestiaChatPromptMovie.tsx @@ -0,0 +1,8 @@ +import { INestiaChatPrompt } from "@nestia/agent"; + +export const NestiaChatPromptMovie = () => {}; +export namespace NestiaChatPromptMovie { + export interface IProps { + prompt: INestiaChatPrompt; + } +} diff --git a/packages/chat/src/movies/prompts/NestiaChatSelectPromptMovie.tsx b/packages/chat/src/movies/prompts/NestiaChatSelectPromptMovie.tsx new file mode 100644 index 000000000..7a1521b38 --- /dev/null +++ b/packages/chat/src/movies/prompts/NestiaChatSelectPromptMovie.tsx @@ -0,0 +1,8 @@ +import { INestiaChatPrompt } from "@nestia/agent"; + +export const NestiaChatSelectPromptMovie = () => {}; +export namespace NestiaChatSelectPromptMovie { + export interface IProps { + select: INestiaChatPrompt.ISelect; + } +} diff --git a/packages/chat/src/movies/prompts/NestiaChatTextPromptMovie.tsx b/packages/chat/src/movies/prompts/NestiaChatTextPromptMovie.tsx new file mode 100644 index 000000000..65919abd7 --- /dev/null +++ b/packages/chat/src/movies/prompts/NestiaChatTextPromptMovie.tsx @@ -0,0 +1,8 @@ +import { INestiaChatPrompt } from "@nestia/agent"; + +export const NestiaChatTextPromptMovie = () => {}; +export namespace NestiaChatTextPromptMovie { + export interface IProps { + prompt: INestiaChatPrompt.IText; + } +} diff --git a/packages/chat/src/movies/sides/NestiaChatTokenUsageMovie.tsx b/packages/chat/src/movies/sides/NestiaChatTokenUsageMovie.tsx new file mode 100644 index 000000000..a6402c076 --- /dev/null +++ b/packages/chat/src/movies/sides/NestiaChatTokenUsageMovie.tsx @@ -0,0 +1,65 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@mui/material"; +import { INestiaChatTokenUsage } from "@nestia/agent"; + +export const NestiaChatTokenUsageMovie = ( + props: NestiaChatTokenUsageMovie.IProps, +) => { + const price: IPrice = compute(props.usage); + return ( + + + + Type + Token Usage + Price + + + + + Total + {props.usage.total.toLocaleString()} + ${price.total.toLocaleString()} + + + Prompt + {props.usage.prompt.total.toLocaleString()} + ${price.prompt.toLocaleString()} + + + Completion + {props.usage.completion.total.toLocaleString()} + ${price.completion.toLocaleString()} + + +
+ ); +}; +export namespace NestiaChatTokenUsageMovie { + export interface IProps { + usage: INestiaChatTokenUsage; + } +} + +interface IPrice { + total: number; + prompt: number; + completion: number; +} + +const compute = (usage: INestiaChatTokenUsage): IPrice => { + const prompt: number = + (usage.prompt.total - usage.prompt.cached) * (2.5 / 1_000_000) + + usage.prompt.cached * (1.25 / 1_000_000); + const completion: number = usage.completion.total * (10.0 / 1_000_000); + return { + total: prompt + completion, + prompt, + completion, + }; +}; diff --git a/packages/chat/src/vite-env.d.ts b/packages/chat/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/chat/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/chat/tsconfig.app.json b/packages/chat/tsconfig.app.json new file mode 100644 index 000000000..5a2def4b7 --- /dev/null +++ b/packages/chat/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/packages/chat/tsconfig.app.tsbuildinfo b/packages/chat/tsconfig.app.tsbuildinfo new file mode 100644 index 000000000..923ae6b81 --- /dev/null +++ b/packages/chat/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/nestiaeditorapplication.tsx","./src/nestiaeditoriframe.tsx","./src/nestiaeditormodule.ts","./src/nestiaeditoruploader.tsx","./src/index.ts","./src/main.tsx","./src/vite-env.d.ts","./src/internal/nestiaeditorcomposer.ts","./src/internal/nestiaeditorfileuploader.tsx"],"version":"5.7.2"} \ No newline at end of file diff --git a/packages/chat/tsconfig.json b/packages/chat/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/packages/chat/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/packages/chat/tsconfig.lib.json b/packages/chat/tsconfig.lib.json new file mode 100644 index 000000000..1624b34c2 --- /dev/null +++ b/packages/chat/tsconfig.lib.json @@ -0,0 +1,105 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "DOM", + "ES2020" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "react-jsx", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + "newLine": "lf", /* Set the newline character for emitting files. */ + "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src"], +} diff --git a/packages/chat/tsconfig.node.json b/packages/chat/tsconfig.node.json new file mode 100644 index 000000000..9dad70185 --- /dev/null +++ b/packages/chat/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/chat/tsconfig.node.tsbuildinfo b/packages/chat/tsconfig.node.tsbuildinfo new file mode 100644 index 000000000..1e7ed2791 --- /dev/null +++ b/packages/chat/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.7.2"} \ No newline at end of file diff --git a/packages/chat/tsconfig.test.json b/packages/chat/tsconfig.test.json new file mode 100644 index 000000000..e6f067419 --- /dev/null +++ b/packages/chat/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "target": "ES2015", + "module": "CommonJS", + "outDir": "bin", + "noEmit": true, + }, + "include": ["src", "test"] +} \ No newline at end of file diff --git a/packages/chat/vite.config.ts b/packages/chat/vite.config.ts new file mode 100644 index 000000000..581248e21 --- /dev/null +++ b/packages/chat/vite.config.ts @@ -0,0 +1,16 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vite.dev/config/ +export default defineConfig({ + base: "./", + plugins: [ + react(), + { + name: "no-attribute", + transformIndexHtml(html) { + return html.replace(`crossorigin`, ""); + }, + }, + ], +}); diff --git a/packages/editor/package.json b/packages/editor/package.json index 344afd7ed..f3dde5f98 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,9 +1,10 @@ { "name": "@nestia/editor", "version": "4.4.0-dev.20241216-3", - "typings": "lib/index.d.ts", "main": "lib/index.js", "module": "lib/index.mjs", + "typings": "lib/index.d.ts", + "description": "Swagger-UI + Cloud TypeScript Editor", "scripts": { "build": "npm run build:static && npm run build:lib", "build:static": "rimraf dist && tsc -b && vite build", @@ -20,6 +21,7 @@ "openapi", "swagger", "generator", + "cloud", "typescript", "editor", "sdk",