diff --git a/.gitignore b/.gitignore index cd80e2461..941166d71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ packages/*/*.tgz packages/*/docs package-lock.json pnpm-lock.yaml -*.log \ No newline at end of file +*.log +.env \ No newline at end of file diff --git a/benchmark/package.json b/benchmark/package.json index bf50ea41f..f664147a5 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -50,7 +50,7 @@ "@types/express": "^4.17.17", "@types/node": "^20.1.4", "@types/physical-cpu-count": "^2.0.0", - "rimraf": "^5.0.1", + "rimraf": "^6.0.1", "ts-node": "^10.9.1", "ts-patch": "^3.3.0", "typescript": "~5.7.2" diff --git a/package.json b/package.json index f31d81700..b84392f6f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^22.10.1", "prettier": "^3.2.4", - "rimraf": "^5.0.1", + "rimraf": "^6.0.1", "sloc": "^0.3.0" } } diff --git a/packages/agent/.eslintrc.cjs b/packages/agent/.eslintrc.cjs new file mode 100644 index 000000000..d44863fb4 --- /dev/null +++ b/packages/agent/.eslintrc.cjs @@ -0,0 +1,28 @@ +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/README.md b/packages/agent/README.md new file mode 100644 index 000000000..8e40440ee --- /dev/null +++ b/packages/agent/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/agent/package.json b/packages/agent/package.json new file mode 100644 index 000000000..da4ec808d --- /dev/null +++ b/packages/agent/package.json @@ -0,0 +1,68 @@ +{ + "name": "@nestia/agent", + "version": "0.1.0", + "description": "Super A.I. Chatbot agent by Swagger Document", + "main": "lib/index.js", + "module": "lib/index.mjs", + "typings": "lib/index.d.ts", + "scripts": { + "prepare": "ts-patch install && typia patch", + "build": "npm run build:main && npm run build:test", + "build:main": "rimraf lib && tsc && rollup -c", + "build:test": "rimraf bin && tsc -p test/tsconfig.json", + "dev": "npm run build:test -- --watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/nestia" + }, + "keywords": [ + "openai", + "chatgpt", + "anthropic", + "claude", + "ai", + "chatbot", + "nestia", + "swagger", + "openapi" + ], + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/nestia/issues" + }, + "homepage": "https://nestia.io", + "files": [ + "README.md", + "LICENSE", + "package.json", + "lib", + "src" + ], + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.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", + "chalk": "4.1.2", + "dotenv": "^16.3.1", + "dotenv-expand": "^10.0.0", + "rimraf": "^6.0.1", + "rollup": "^4.29.1", + "ts-patch": "^3.3.0", + "tstl": "^3.0.0", + "typescript": "~5.7.2", + "typescript-transform-paths": "^3.5.3" + }, + "dependencies": { + "@samchon/openapi": "^2.3.2", + "openai": "^4.77.0", + "typia": "^7.5.1", + "uuid": "^11.0.4" + } +} \ No newline at end of file diff --git a/packages/agent/rollup.config.js b/packages/agent/rollup.config.js new file mode 100644 index 000000000..f948ecc19 --- /dev/null +++ b/packages/agent/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.json", + module: "ES2020", + target: "ES2020", + }), + terser({ + format: { + comments: "some", + beautify: true, + ecma: "2020", + }, + compress: false, + mangle: false, + module: true, + }), + ], +}; diff --git a/packages/agent/src/NestiaChatAgent.ts b/packages/agent/src/NestiaChatAgent.ts new file mode 100644 index 000000000..3503b390d --- /dev/null +++ b/packages/agent/src/NestiaChatAgent.ts @@ -0,0 +1,199 @@ +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"; + +/** + * 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(); + } + + /** + * 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 { + /** + * 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?: Partial; + } + + /** + * System prompt messages. + * + * System prompt messages if you want to customize the system prompt + * messages for each situation. + */ + export interface ISytemPrompt { + initial?: (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 new file mode 100644 index 000000000..94742eb7c --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptAgent.ts @@ -0,0 +1,182 @@ +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 { __IChatSelectFunctionsApplication } from "../structures/internal/__IChatSelectFunctionsApplication"; +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 initialized_: boolean; + + public constructor(private readonly props: NestiaChatAgent.IProps) { + this.stack_ = []; + this.histories_ = props.histories ? [...props.histories] : []; + this.listeners_ = new Map(); + this.initialized_ = false; + 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 async conversate(content: string): Promise { + const index: number = this.histories_.length; + const out = () => this.histories_.slice(index); + + // 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, + content, + }); + this.initialized_ ||= output.mounted; + this.histories_.push(...output.prompts); + if (this.initialized_ === false) return out(); + else this.dispatch({ type: "initialize" }); + } + + // 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, + content, + })), + ); + + // 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, + content, + })), + ); + if (this.stack_.length === 0) return out(); + + // CALL FUNCTIONS + 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, + content, + }); + this.histories_.push(...prompts); + + // EXPLAIN RETURN VALUES + const calls: INestiaChatPrompt.IExecute[] = prompts.filter( + (p) => p.kind === "execute", + ); + for (const c of calls) + ChatGptCancelFunctionAgent.cancelFunction({ + stack: this.stack_, + reference: { + name: c.function.name, + reason: "completed", + }, + dispatch: (event) => this.dispatch(event), + }); + if (calls.length !== 0) + this.histories_.push( + ...(await ChatGptDescribeFunctionAgent.execute({ + service: this.props.service, + histories: calls, + config: this.props.config, + })), + ); + 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 {} + }), + ); + } +} + +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 new file mode 100644 index 000000000..ce32b80dc --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptCancelFunctionAgent.ts @@ -0,0 +1,299 @@ +import { + IHttpLlmApplication, + 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 { IChatGptService } from "../structures/IChatGptService"; +import { INestiaChatEvent } from "../structures/INestiaChatEvent"; +import { INestiaChatFunctionSelection } from "../structures/INestiaChatFunctionSelection"; +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +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; + 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); + + const stacks: INestiaChatFunctionSelection[][] = props.divide.map(() => []); + const events: INestiaChatEvent[] = []; + const prompts: INestiaChatPrompt.ICancel[][] = await Promise.all( + props.divide.map((candidates, i) => + step( + { + ...props, + stack: stacks[i]!, + dispatch: async (e) => { + events.push(e); + }, + }, + candidates, + 0, + ), + ), + ); + + // 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 + ) + return step( + props, + stacks + .map((row) => Array.from(row.values()).map((s) => s.function)) + .flat(), + 0, + ); + + // RE-COLLECT SELECT FUNCTION EVENTS + const collection: INestiaChatPrompt.ICancel = { + id: v4(), + kind: "cancel", + functions: [], + }; + 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, + 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, + ); + if (index === -1) return null; + + const item: INestiaChatFunctionSelection = props.stack[index]!; + props.stack.splice(index, 1); + await props.dispatch({ + type: "cancel", + function: item.function, + reason: props.reference.reason, + }); + return item.function; + }; + + const step = async ( + props: IProps, + candidates: IHttpLlmFunction<"chatgpt">[], + retry: number, + failures?: IFailure[], + ): Promise => { + //---- + // EXECUTE CHATGPT API + //---- + const completion: OpenAI.ChatCompletion = + await props.service.api.chat.completions.create( + { + model: props.service.model, + messages: [ + // 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: + props.config?.systemPrompt?.cancel?.(props.histories) ?? + SYSTEM_PROMPT, + }, + // 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, + }, + props.service.options, + ); + + //---- + // VALIDATION + //---- + if (retry++ < (props.config?.retry ?? NestiaChatAgentConstant.RETRY)) { + const failures: IFailure[] = []; + for (const choice of completion.choices) + for (const tc of choice.message.tool_calls ?? []) { + if (tc.function.name !== "cancelFunctions") continue; + const input: object = JSON.parse(tc.function.arguments); + const validation: IValidation<__IChatFunctionReference.IProps> = + typia.validate<__IChatFunctionReference.IProps>(input); + if (validation.success === false) + failures.push({ + id: tc.id, + name: tc.function.name, + validation, + }); + } + if (failures.length > 0) return step(props, candidates, retry, failures); + } + + //---- + // PROCESS COMPLETION + //---- + const prompts: INestiaChatPrompt.ICancel[] = []; + for (const choice of completion.choices) { + // TOOL CALLING HANDLER + if (choice.message.tool_calls) + for (const tc of choice.message.tool_calls) { + if (tc.type !== "function") continue; + const input: __IChatFunctionReference.IProps = JSON.parse( + tc.function.arguments, + ); + if (typia.is(input) === false) continue; + else if (tc.function.name === "cancelFunctions") { + const collection: INestiaChatPrompt.ICancel = { + id: tc.id, + kind: "cancel", + functions: [], + }; + 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, + }); + } + if (collection.functions.length !== 0) prompts.push(collection); + } + } + } + return prompts; + }; + + const emendMessages = ( + failures: IFailure[], + ): OpenAI.ChatCompletionMessageParam[] => + failures + .map((f) => [ + { + role: "assistant", + tool_calls: [ + { + type: "function", + id: f.id, + function: { + name: f.name, + arguments: JSON.stringify(f.validation.data), + }, + }, + ], + } satisfies OpenAI.ChatCompletionAssistantMessageParam, + { + role: "tool", + content: JSON.stringify(f.validation.errors), + tool_call_id: f.id, + } satisfies OpenAI.ChatCompletionToolMessageParam, + { + role: "system", + content: [ + "You A.I. assistant has composed wrong typed arguments.", + "", + "Correct it at the next function calling.", + ].join("\n"), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + ]) + .flat(); +} + +const CONTAINER: ILlmApplication<"chatgpt"> = typia.llm.application< + __IChatCancelFunctionsApplication, + "chatgpt" +>(); + +interface IFailure { + id: string; + name: string; + validation: IValidation.IFailure; +} + +const SYSTEM_PROMPT: string = [ + "You are a helpful assistant for selecting functions to call.", + "", + "Use the supplied tools to select some functions of `getApiFunctions()` returned", + "", + "If you can't find any proper function to select, just type your own message.", +].join("\n"); diff --git a/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts new file mode 100644 index 000000000..80b73e467 --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptDescribeFunctionAgent.ts @@ -0,0 +1,63 @@ +import OpenAI from "openai"; + +import { NestiaChatAgent } from "../NestiaChatAgent"; +import { IChatGptService } from "../structures/IChatGptService"; +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; + +export namespace ChatGptDescribeFunctionAgent { + export interface IProps { + service: IChatGptService; + histories: INestiaChatPrompt.IExecute[]; + 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( + { + model: props.service.model, + messages: [ + // PREVIOUS FUNCTION CALLING HISTORIES + ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), + // SYTEM PROMPT + { + role: "assistant", + content: + props.config?.systemPrompt?.describe?.(props.histories) ?? + SYSTEM_PROMPT, + }, + ], + }, + props.service.options, + ); + return completion.choices + .map((choice) => + choice.message.role === "assistant" && !!choice.message.content?.length + ? choice.message.content + : null, + ) + .filter((str) => str !== null) + .map((content) => ({ + kind: "describe", + executions: props.histories, + text: content, + })); + }; +} + +const SYSTEM_PROMPT = [ + "You are a helpful assistant describing return values of function calls.", + "", + "Above 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.", + "", + "Also, 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.", +].join("\n"); diff --git a/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts new file mode 100644 index 000000000..1db962b59 --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptExecuteFunctionAgent.ts @@ -0,0 +1,332 @@ +import { + ChatGptTypeChecker, + HttpLlm, + IChatGptSchema, + IHttpConnection, + IHttpLlmApplication, + IHttpLlmFunction, + IHttpMigrateRoute, + IHttpResponse, +} from "@samchon/openapi"; +import OpenAI from "openai"; + +import { NestiaChatAgent } from "../NestiaChatAgent"; +import { NestiaChatAgentConstant } from "../internal/NestiaChatAgentConstant"; +import { IChatGptService } from "../structures/IChatGptService"; +import { INestiaChatEvent } from "../structures/INestiaChatEvent"; +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +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; + content: string; + config?: NestiaChatAgent.IConfig | undefined; + } + + export const execute = async ( + props: IProps, + ): Promise => { + //---- + // EXECUTE CHATGPT API + //---- + const completion: OpenAI.ChatCompletion = + await props.service.api.chat.completions.create( + { + model: props.service.model, + messages: [ + // 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) ?? + SYSTEM_PROMPT, + }, + ], + // 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, + }, + props.service.options, + ); + + //---- + // PROCESS COMPLETION + //---- + 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; + closures.push(() => + propagate( + props, + { + id: tc.id, + function: func, + input: JSON.parse(tc.function.arguments), + }, + 0, + ), + ); + } + } + if ( + choice.message.role === "assistant" && + !!choice.message.content?.length + ) + closures.push( + async () => + ({ + kind: "text", + role: "assistant", + text: choice.message.content!, + }) satisfies INestiaChatPrompt.IText, + ); + } + return Promise.all(closures.map((fn) => fn())); + }; + + const propagate = async ( + props: IProps, + call: IFunctionCall, + retry: number, + ): Promise => { + fill({ + function: call.function, + arguments: call.input, + }); + try { + await props.dispatch({ + type: "call", + function: call.function, + arguments: call.input, + }); + 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, + }); + return result; + } catch (error) { + return { + kind: "execute", + role: "assistant", + function: call.function, + id: call.id, + arguments: call.input, + response: { + status: 500, + headers: {}, + body: + error instanceof Error + ? { + ...error, + name: error.name, + message: error.message, + } + : error, + }, + } satisfies INestiaChatPrompt.IExecute; + } + }; + + const correct = async ( + props: IProps, + call: IFunctionCall, + retry: number, + error: unknown, + ): Promise => { + //---- + // EXECUTE CHATGPT API + //---- + const completion: OpenAI.ChatCompletion = + await props.service.api.chat.completions.create( + { + model: props.service.model, + messages: [ + // 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) ?? + SYSTEM_PROMPT, + }, + { + 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: [ + { + type: "function", + 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, + }, + }, + ], + tool_choice: "auto", + parallel_tool_calls: false, + }, + props.service.options, + ); + + //---- + // PROCESS COMPLETION + //---- + const toolCall: OpenAI.ChatCompletionMessageToolCall | undefined = ( + completion.choices[0]?.message.tool_calls ?? [] + ).find( + (tc) => tc.type === "function" && tc.function.name === call.function.name, + ); + if (toolCall === undefined) return null; + return propagate( + props, + { + id: toolCall.id, + function: call.function, + input: JSON.parse(toolCall.function.arguments), + }, + retry, + ); + }; + + const fill = (props: { + function: IHttpLlmFunction<"chatgpt">; + arguments: object; + }): void => { + const route: IHttpMigrateRoute = props.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.arguments as any).body = {}; + if (route.query && (props.arguments as any).query === undefined) + (props.arguments as any).query = {}; + }; + + const isObject = ( + $defs: Record, + schema: IChatGptSchema, + ): boolean => { + return ( + ChatGptTypeChecker.isObject(schema) || + (ChatGptTypeChecker.isReference(schema) && + isObject($defs, $defs[schema.$ref.split("/").at(-1)!]!)) || + (ChatGptTypeChecker.isAnyOf(schema) && + schema.anyOf.every((schema) => isObject($defs, schema))) + ); + }; +} + +interface IFunctionCall { + id: string; + function: IHttpLlmFunction<"chatgpt">; + input: object; +} + +const SYSTEM_PROMPT = [ + "You are a helpful assistant for tool calling.", + "", + "Use the supplied tools to assist the user.", + "", + "If 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.", +].join("\n"); diff --git a/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts b/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts new file mode 100644 index 000000000..74a91bb6a --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptHistoryDecoder.ts @@ -0,0 +1,68 @@ +import OpenAI from "openai"; + +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; + +export namespace ChatGptHistoryDecoder { + export const decode = ( + history: INestiaChatPrompt, + ): OpenAI.ChatCompletionMessageParam[] => { + // NO NEED TO DECODE DESCRIBE + if (history.kind === "describe") return []; + else if (history.kind === "text") + return [ + { + role: history.role, + content: history.text, + }, + ]; + else if (history.kind === "select" || history.kind === "cancel") + return [ + { + role: "assistant", + tool_calls: [ + { + type: "function", + id: history.id, + function: { + name: `${history.kind}Functions`, + arguments: JSON.stringify({ + functions: history.functions.map((t) => ({ + name: t.function.name, + reason: t.reason, + })), + }), + }, + }, + ], + }, + { + role: "tool", + tool_call_id: history.id, + content: "", + }, + ]; + return [ + { + role: "assistant", + tool_calls: [ + { + type: "function", + id: history.id, + function: { + name: history.function.name, + arguments: JSON.stringify(history.arguments), + }, + }, + ], + }, + { + role: "tool", + tool_call_id: history.id, + content: + typeof history.response.body === "string" + ? history.response.body + : JSON.stringify(history.response.body), + }, + ]; + }; +} diff --git a/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts new file mode 100644 index 000000000..0f91a8446 --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptInitializeFunctionAgent.ts @@ -0,0 +1,101 @@ +import { ILlmFunction } from "@samchon/openapi"; +import OpenAI from "openai"; +import typia from "typia"; + +import { NestiaChatAgent } from "../NestiaChatAgent"; +import { IChatGptService } from "../structures/IChatGptService"; +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +import { __IChatInitialApplication } from "../structures/internal/__IChatInitialApplication"; +import { ChatGptHistoryDecoder } from "./ChatGptHistoryDecoder"; + +export namespace ChatGptInitializeFunctionAgent { + export interface IProps { + service: IChatGptService; + histories: INestiaChatPrompt[]; + content: string; + config?: NestiaChatAgent.IConfig | undefined; + } + export interface IOutput { + mounted: boolean; + prompts: INestiaChatPrompt[]; + } + + export const execute = async (props: IProps): Promise => { + //---- + // EXECUTE CHATGPT API + //---- + const completion: OpenAI.ChatCompletion = + await props.service.api.chat.completions.create( + { + model: props.service.model, + messages: [ + { + // SYTEM PROMPT + role: "system", + content: + props.config?.systemPrompt?.initial?.(props.histories) ?? + SYSTEM_PROMPT, + }, + // PREVIOUS HISTORIES + ...props.histories.map(ChatGptHistoryDecoder.decode).flat(), + // USER INPUT + { + role: "user", + content: props.content, + }, + ], + // GETTER FUNCTION + tools: [ + { + type: "function", + function: { + name: FUNCTION.name, + description: FUNCTION.description, + parameters: FUNCTION.parameters as any, + }, + }, + ], + tool_choice: "auto", + parallel_tool_calls: false, + }, + props.service.options, + ); + + //---- + // PROCESS COMPLETION + //---- + const prompts: INestiaChatPrompt[] = []; + for (const choice of completion.choices) { + if ( + choice.message.role === "assistant" && + !!choice.message.content?.length + ) + prompts.push({ + kind: "text", + role: "assistant", + text: choice.message.content, + }); + } + return { + mounted: completion.choices.some( + (c) => + !!c.message.tool_calls?.some( + (tc) => + tc.type === "function" && tc.function.name === FUNCTION.name, + ), + ), + prompts, + }; + }; +} + +const FUNCTION: ILlmFunction<"chatgpt"> = typia.llm.application< + __IChatInitialApplication, + "chatgpt" +>().functions[0]!; + +const SYSTEM_PROMPT = [ + "You are a helpful assistant.", + "", + "Use the supplied tools to assist the user.", +].join("\n"); diff --git a/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts b/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts new file mode 100644 index 000000000..782b841c9 --- /dev/null +++ b/packages/agent/src/chatgpt/ChatGptSelectFunctionAgent.ts @@ -0,0 +1,317 @@ +import { + IHttpLlmApplication, + 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 { IChatGptService } from "../structures/IChatGptService"; +import { INestiaChatEvent } from "../structures/INestiaChatEvent"; +import { INestiaChatFunctionSelection } from "../structures/INestiaChatFunctionSelection"; +import { INestiaChatPrompt } from "../structures/INestiaChatPrompt"; +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; + 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); + + const stacks: INestiaChatFunctionSelection[][] = props.divide.map(() => []); + const events: INestiaChatEvent[] = []; + const prompts: INestiaChatPrompt[][] = await Promise.all( + props.divide.map((candidates, i) => + step( + { + ...props, + stack: stacks[i]!, + dispatch: async (e) => { + events.push(e); + }, + }, + candidates, + 0, + ), + ), + ); + + // 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 + ) + return step( + props, + stacks + .map((row) => Array.from(row.values()).map((s) => s.function)) + .flat(), + 0, + ); + + // RE-COLLECT SELECT FUNCTION EVENTS + const collection: INestiaChatPrompt.ISelect = { + id: v4(), + kind: "select", + functions: [], + }; + 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, + reason: e.reason, + }, + }); + } + return [collection]; + }; + + const step = async ( + props: IProps, + candidates: IHttpLlmFunction<"chatgpt">[], + retry: number, + failures?: IFailure[], + ): Promise => { + //---- + // EXECUTE CHATGPT API + //---- + const completion: OpenAI.ChatCompletion = + await props.service.api.chat.completions.create( + { + model: props.service.model, + messages: [ + // 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: SYSTEM_MESSAGE_OF_ROLE, + }, + // 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, + }, + props.service.options, + ); + if (props.completions !== undefined) props.completions.push(completion); + + //---- + // VALIDATION + //---- + if (retry++ < (props.config?.retry ?? NestiaChatAgentConstant.RETRY)) { + const failures: IFailure[] = []; + for (const choice of completion.choices) + for (const tc of choice.message.tool_calls ?? []) { + if (tc.function.name !== "selectFunctions") continue; + const input: object = JSON.parse(tc.function.arguments); + const validation: IValidation<__IChatFunctionReference.IProps> = + typia.validate<__IChatFunctionReference.IProps>(input); + if (validation.success === false) + failures.push({ + id: tc.id, + name: tc.function.name, + validation, + }); + } + if (failures.length > 0) return step(props, candidates, retry, failures); + } + + //---- + // PROCESS COMPLETION + //---- + const prompts: INestiaChatPrompt[] = []; + for (const choice of completion.choices) { + // TOOL CALLING HANDLER + if (choice.message.tool_calls) + for (const tc of choice.message.tool_calls) { + if (tc.type !== "function") continue; + + const input: __IChatFunctionReference.IProps = JSON.parse( + tc.function.arguments, + ); + if (typia.is(input) === false) continue; + else if (tc.function.name === "selectFunctions") { + const collection: INestiaChatPrompt.ISelect = { + id: tc.id, + kind: "select", + functions: [], + }; + 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, + }); + } + if (collection.functions.length !== 0) prompts.push(collection); + } + } + + // ASSISTANT MESSAGE + if ( + choice.message.role === "assistant" && + !!choice.message.content?.length + ) + prompts.push({ + kind: "text", + role: "assistant", + text: choice.message.content, + }); + } + 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; + + props.stack.push({ + function: func, + reason: props.reference.reason, + }); + await props.dispatch({ + type: "select", + function: func, + reason: props.reference.reason, + }); + return func; + }; + + const emendMessages = ( + failures: IFailure[], + ): OpenAI.ChatCompletionMessageParam[] => + failures + .map((f) => [ + { + role: "assistant", + tool_calls: [ + { + type: "function", + id: f.id, + function: { + name: f.name, + arguments: JSON.stringify(f.validation.data), + }, + }, + ], + } satisfies OpenAI.ChatCompletionAssistantMessageParam, + { + role: "tool", + content: JSON.stringify(f.validation.errors), + tool_call_id: f.id, + } satisfies OpenAI.ChatCompletionToolMessageParam, + { + role: "system", + content: [ + "You A.I. assistant has composed wrong typed arguments.", + "", + "Correct it at the next function calling.", + ].join("\n"), + } satisfies OpenAI.ChatCompletionSystemMessageParam, + ]) + .flat(); +} + +const CONTAINER: ILlmApplication<"chatgpt"> = typia.llm.application< + __IChatSelectFunctionsApplication, + "chatgpt" +>(); + +interface IFailure { + id: string; + name: string; + validation: IValidation.IFailure; +} + +const SYSTEM_MESSAGE_OF_ROLE: string = [ + "You are a helpful assistant for selecting functions to call.", + "", + "Use the supplied tools to select some functions of `getApiFunctions()` returned", + "", + "If you can't find any proper function to select, just type your own message.", +].join("\n"); diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts new file mode 100644 index 000000000..5604f6dcd --- /dev/null +++ b/packages/agent/src/index.ts @@ -0,0 +1,4 @@ +import * as agent from "./module"; + +export default agent; +export * from "./module"; diff --git a/packages/agent/src/internal/NestiaChatAgentConstant.ts b/packages/agent/src/internal/NestiaChatAgentConstant.ts new file mode 100644 index 000000000..ebd3ceb8c --- /dev/null +++ b/packages/agent/src/internal/NestiaChatAgentConstant.ts @@ -0,0 +1,4 @@ +export namespace NestiaChatAgentConstant { + export const RETRY = 3; + export const ELITICISM = true; +} diff --git a/packages/agent/src/module.ts b/packages/agent/src/module.ts new file mode 100644 index 000000000..50d65bfa6 --- /dev/null +++ b/packages/agent/src/module.ts @@ -0,0 +1,6 @@ +export * from "./structures/IChatGptService"; +export * from "./structures/INestiaChatEvent"; +export * from "./structures/INestiaChatFunctionSelection"; +export * from "./structures/INestiaChatPrompt"; + +export * from "./NestiaChatAgent"; diff --git a/packages/agent/src/structures/IChatGptService.ts b/packages/agent/src/structures/IChatGptService.ts new file mode 100644 index 000000000..818654554 --- /dev/null +++ b/packages/agent/src/structures/IChatGptService.ts @@ -0,0 +1,21 @@ +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/INestiaChatAgent.ts b/packages/agent/src/structures/INestiaChatAgent.ts new file mode 100644 index 000000000..903538ca7 --- /dev/null +++ b/packages/agent/src/structures/INestiaChatAgent.ts @@ -0,0 +1,18 @@ +import { INestiaChatEvent } from "./INestiaChatEvent"; +import { INestiaChatPrompt } from "./INestiaChatPrompt"; + +export interface INestiaChatAgent { + conversate(content: string): Promise; + + getHistories(): INestiaChatPrompt[]; + + 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 new file mode 100644 index 000000000..159963f00 --- /dev/null +++ b/packages/agent/src/structures/INestiaChatEvent.ts @@ -0,0 +1,125 @@ +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 new file mode 100644 index 000000000..13cc1bc56 --- /dev/null +++ b/packages/agent/src/structures/INestiaChatFunctionSelection.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..2bd8514c9 --- /dev/null +++ b/packages/agent/src/structures/INestiaChatPrompt.ts @@ -0,0 +1,125 @@ +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/structures/internal/__IChatCancelFunctionsApplication.js b/packages/agent/src/structures/internal/__IChatCancelFunctionsApplication.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatCancelFunctionsApplication.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/packages/agent/src/structures/internal/__IChatCancelFunctionsApplication.ts b/packages/agent/src/structures/internal/__IChatCancelFunctionsApplication.ts new file mode 100644 index 000000000..d1df8bb73 --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatCancelFunctionsApplication.ts @@ -0,0 +1,23 @@ +import { __IChatFunctionReference } from "./__IChatFunctionReference"; + +export interface __IChatCancelFunctionsApplication { + /** + * Cancel a function from the candidate list to call. + * + * If you A.I. agent has understood that the user wants to cancel + * some candidate functions to call from the conversation, please cancel + * them through this function. + * + * Also, when you A.I. find a function that has been selected by the candidate + * pooling, cancel the function by calling this function. For reference, the + * candidate pooling means that user wants only one function to call, but you A.I. + * agent selects multiple candidate functions because the A.I. agent can't specify + * only one thing due to lack of specificity or homogeneity of candidate functions. + * + * Additionally, if you A.I. agent wants to cancel same function multiply, you can + * do it by assigning the same function name multiply in the `functions` property. + * + * @param props Properties of the function + */ + cancelFunctions(props: __IChatFunctionReference.IProps): Promise; +} diff --git a/packages/agent/src/structures/internal/__IChatFunctionReference.js b/packages/agent/src/structures/internal/__IChatFunctionReference.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatFunctionReference.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/packages/agent/src/structures/internal/__IChatFunctionReference.ts b/packages/agent/src/structures/internal/__IChatFunctionReference.ts new file mode 100644 index 000000000..454b8cc6c --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatFunctionReference.ts @@ -0,0 +1,21 @@ +export interface __IChatFunctionReference { + /** + * The reason of the function selection. + * + * Just write the reason why you've determined to select this function. + */ + reason: string; + + /** + * Name of the target function to call. + */ + name: string; +} +export namespace __IChatFunctionReference { + export interface IProps { + /** + * List of target functions. + */ + functions: __IChatFunctionReference[]; + } +} diff --git a/packages/agent/src/structures/internal/__IChatInitialApplication.js b/packages/agent/src/structures/internal/__IChatInitialApplication.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatInitialApplication.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/packages/agent/src/structures/internal/__IChatInitialApplication.ts b/packages/agent/src/structures/internal/__IChatInitialApplication.ts new file mode 100644 index 000000000..00087313e --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatInitialApplication.ts @@ -0,0 +1,15 @@ +import { IHttpLlmFunction } from "@samchon/openapi"; + +export interface __IChatInitialApplication { + /** + * Get list of API functions. + * + * If user seems like to request some function calling except this one, + * call this `getApiFunctions()` to get the list of candidate API functions + * provided from this application. + * + * Also, user just wants to list up every remote API functions that can be + * called from the backend server, utilize this function too. + */ + getApiFunctions({}): Promise>>; +} diff --git a/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.js b/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.ts b/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.ts new file mode 100644 index 000000000..679aca74e --- /dev/null +++ b/packages/agent/src/structures/internal/__IChatSelectFunctionsApplication.ts @@ -0,0 +1,24 @@ +import { __IChatFunctionReference } from "./__IChatFunctionReference"; + +export interface __IChatSelectFunctionsApplication { + /** + * Select proper API functions to call. + * + * If you A.I. agent has found some proper API functions to call + * from the conversation with user, please select the API functions + * just by calling this function. + * + * When user wants to call a same function multiply, you A.I. agent must + * list up it multiply in the `functions` property. Otherwise the user has + * requested to call many different functions, you A.I. agent have to assign + * them all into the `functions` property. + * + * Also, if you A.I. agent can't speciify a specific function to call due to lack + * of specificity or homogeneity of candidate functions, just assign all of them + * by in the` functions` property` too. Instead, when you A.I. agent can specify + * a specific function to call, the others would be eliminated. + * + * @param props Properties of the function + */ + selectFunctions(props: __IChatFunctionReference.IProps): Promise; +} diff --git a/packages/agent/test/TestGlobal.ts b/packages/agent/test/TestGlobal.ts new file mode 100644 index 000000000..f5bc2e469 --- /dev/null +++ b/packages/agent/test/TestGlobal.ts @@ -0,0 +1,39 @@ +import dotenv from "dotenv"; +import dotenvExpand from "dotenv-expand"; +import { Singleton } from "tstl"; +import typia from "typia"; + +export class TestGlobal { + public static readonly ROOT: string = + __filename.substring(__filename.length - 2) === "js" + ? `${__dirname}/../..` + : `${__dirname}/..`; + + public static get env(): IEnvironments { + return environments.get(); + } + + public static getArguments(type: string): string[] { + const from: number = process.argv.indexOf(`--${type}`) + 1; + if (from === 0) return []; + const to: number = process.argv + .slice(from) + .findIndex((str) => str.startsWith("--"), from); + return process.argv.slice( + from, + to === -1 ? process.argv.length : to + from, + ); + } +} + +interface IEnvironments { + CHATGPT_API_KEY?: string; + CHATGPT_BASE_URL?: string; + CHATGPT_OPTIONS?: string; +} + +const environments = new Singleton(() => { + const env = dotenv.config(); + dotenvExpand.expand(env); + return typia.assert(process.env); +}); diff --git a/packages/agent/test/cli.ts b/packages/agent/test/cli.ts new file mode 100644 index 000000000..316c78ba4 --- /dev/null +++ b/packages/agent/test/cli.ts @@ -0,0 +1,119 @@ +import { INestiaChatPrompt, NestiaChatAgent } from "@nestia/agent"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import ShoppingApi from "@samchon/shopping-api"; +import chalk from "chalk"; +import OpenAI from "openai"; +import typia from "typia"; + +import { TestGlobal } from "./TestGlobal"; +import { ConsoleScanner } from "./utils/ConsoleScanner"; + +const trace = (...args: any[]): void => { + console.log("----------------------------------------------"); + console.log(...args); + console.log("----------------------------------------------"); +}; + +const main = async (): Promise => { + if (!TestGlobal.env.CHATGPT_API_KEY?.length) return; + + // GET LLM APPLICATION SCHEMA + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = await fetch( + "https://raw.githubusercontent.com/samchon/shopping-backend/refs/heads/master/packages/api/swagger.json", + ).then((r) => r.json()); + const document: OpenApi.IDocument = OpenApi.convert(typia.assert(swagger)); + const application: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({ + model: "chatgpt", + document, + }); + application.functions = application.functions.filter((f) => + f.path.startsWith("/shoppings/customers"), + ); + + // HANDSHAKE WITH SHOPPING BACKEND + const connection: IHttpConnection = { + host: "http://localhost:37001", + }; + await ShoppingApi.functional.shoppings.customers.authenticate.create( + connection, + { + channel_code: "samchon", + external_user: null, + href: "htts://127.0.0.1/NodeJS", + referrer: null, + }, + ); + await ShoppingApi.functional.shoppings.customers.authenticate.activate( + connection, + { + mobile: "821012345678", + name: "John Doe", + }, + ); + + // COMPOSE CHAT AGENT + const agent: NestiaChatAgent = new NestiaChatAgent({ + service: { + api: new OpenAI({ + apiKey: TestGlobal.env.CHATGPT_API_KEY, + baseURL: TestGlobal.env.CHATGPT_BASE_URL, + }), + model: "gpt-4o", + options: TestGlobal.env.CHATGPT_OPTIONS + ? JSON.parse(TestGlobal.env.CHATGPT_OPTIONS) + : undefined, + }, + connection, + application, + }); + agent.on("initialize", () => console.log(chalk.greenBright("Initialized"))); + agent.on("select", (e) => + console.log(chalk.cyanBright("selected"), e.function.name, e.reason), + ); + agent.on("call", (e) => + console.log(chalk.blueBright("call"), e.function.name), + ); + agent.on("complete", (e) => + console.log( + chalk.greenBright("completed"), + e.function.name, + e.response.status, + ), + ); + agent.on("cancel", (e) => + console.log(chalk.redBright("canceled"), e.function.name, e.reason), + ); + + // START CONVERSATION + while (true) { + const content: string = await ConsoleScanner.read("Input: "); + if (content === "exit") break; + + const histories: INestiaChatPrompt[] = await agent.conversate(content); + for (const h of histories) + if (h.kind === "text") + trace(chalk.yellow("Text"), chalk.blueBright(h.role), "\n\n", h.text); + else if (h.kind === "describe") + trace( + chalk.whiteBright("Describe"), + chalk.blueBright("agent"), + "\n\n", + h.text, + ); + } +}; +main().catch((error) => { + console.log(error); + process.exit(-1); +}); diff --git a/packages/agent/test/index.ts b/packages/agent/test/index.ts new file mode 100644 index 000000000..4aa37baee --- /dev/null +++ b/packages/agent/test/index.ts @@ -0,0 +1,103 @@ +import { NestiaChatAgent } from "@nestia/agent"; +import { DynamicExecutor } from "@nestia/e2e"; +import { + HttpLlm, + IHttpConnection, + IHttpLlmApplication, + OpenApi, + OpenApiV3, + OpenApiV3_1, + SwaggerV2, +} from "@samchon/openapi"; +import ShoppingApi from "@samchon/shopping-api"; +import chalk from "chalk"; +import OpenAI from "openai"; +import typia from "typia"; + +import { TestGlobal } from "./TestGlobal"; + +const main = async (): Promise => { + if (!TestGlobal.env.CHATGPT_API_KEY?.length) return; + + // GET LLM APPLICATION SCHEMA + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = await fetch( + "https://raw.githubusercontent.com/samchon/shopping-backend/refs/heads/master/packages/api/swagger.json", + ).then((r) => r.json()); + const document: OpenApi.IDocument = OpenApi.convert(typia.assert(swagger)); + const application: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({ + model: "chatgpt", + document, + }); + application.functions = application.functions.filter((f) => + f.path.startsWith("/shoppings/customers"), + ); + + // HANDSHAKE WITH SHOPPING BACKEND + const connection: IHttpConnection = { + host: "http://localhost:37001", + }; + await ShoppingApi.functional.shoppings.customers.authenticate.create( + connection, + { + channel_code: "samchon", + external_user: null, + href: "htts://127.0.0.1/NodeJS", + referrer: null, + }, + ); + await ShoppingApi.functional.shoppings.customers.authenticate.activate( + connection, + { + mobile: "821012345678", + name: "John Doe", + }, + ); + + // COMPOSE CHAT AGENT + const agent: NestiaChatAgent = new NestiaChatAgent({ + service: { + api: new OpenAI({ + apiKey: TestGlobal.env.CHATGPT_API_KEY, + }), + model: "gpt-4o", + }, + connection, + application, + }); + + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + prefix: "test_", + location: __dirname + "/features", + parameters: () => [agent], + onComplete: (exec) => { + const trace = (str: string) => + console.log(` - ${chalk.green(exec.name)}: ${str}`); + if (exec.error === null) { + const elapsed: number = + new Date(exec.completed_at).getTime() - + new Date(exec.started_at).getTime(); + trace(`${chalk.yellow(elapsed.toLocaleString())} ms`); + } else trace(chalk.red(exec.error.name)); + }, + }); + + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log("Success"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + } else { + for (const exp of exceptions) console.log(exp); + console.log("Failed"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + process.exit(-1); + } +}; +main().catch((error) => { + console.error(error); + process.exit(-1); +}); diff --git a/packages/agent/test/tsconfig.json b/packages/agent/test/tsconfig.json new file mode 100644 index 000000000..6bb866274 --- /dev/null +++ b/packages/agent/test/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ES2015", + "outDir": "../bin", + "paths": { + "@nestia/agent": ["../src/index.ts"], + "@nestia/agent/lib/*": ["../src/*"], + }, + "plugins": [ + { "transform": "typescript-transform-paths" }, + { "transform": "typia/lib/transform" }, + ], + }, + "include": ["../src", "./"], +} \ No newline at end of file diff --git a/packages/agent/test/utils/ConsoleScanner.ts b/packages/agent/test/utils/ConsoleScanner.ts new file mode 100644 index 000000000..04717ba02 --- /dev/null +++ b/packages/agent/test/utils/ConsoleScanner.ts @@ -0,0 +1,16 @@ +import readline from "readline"; + +export namespace ConsoleScanner { + export const read = (question: string): Promise => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); + }; +} diff --git a/packages/agent/tsconfig.json b/packages/agent/tsconfig.json new file mode 100644 index 000000000..c0a403399 --- /dev/null +++ b/packages/agent/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "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": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "DOM", + "ES2015", + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental 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. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* 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. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "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. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "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. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + "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. */ + // "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": "crlf", /* 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. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "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. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when 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, /* Add 'undefined' to a type when accessed using an index. */ + // "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. */ + "plugins": [ + { + "transform": "typia/lib/transform" + } + ], + "strictNullChecks": true + }, + "include": ["src"], +} \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index de0cf1b0b..8b73c0b68 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,7 @@ "@nestia/sdk": "workspace:^", "@types/inquirer": "^9.0.3", "@types/node": "^18.11.16", - "rimraf": "^3.0.2", + "rimraf": "^6.0.1", "typescript": "~5.7.2" }, "files": [ diff --git a/packages/core/package.json b/packages/core/package.json index d097365b9..56e1d43c3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,7 +76,7 @@ "fastify": "^4.28.1", "git-last-commit": "^1.0.1", "inquirer": "^8.2.5", - "rimraf": "^3.0.2", + "rimraf": "^6.0.1", "ts-node": "^10.9.1", "ts-patch": "^3.3.0", "tstl": "^3.0.0", diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 76a6453cc..06dd7dd80 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -36,7 +36,7 @@ "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", - "rimraf": "^4.1.2", + "rimraf": "^6.0.1", "ts-node": "^10.9.1", "ts-patch": "^3.3.0", "typescript": "~5.7.2", diff --git a/packages/fetcher/package.json b/packages/fetcher/package.json index d588d5382..9967f956d 100644 --- a/packages/fetcher/package.json +++ b/packages/fetcher/package.json @@ -36,7 +36,7 @@ "@types/node": "^18.11.14", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", - "rimraf": "^3.0.2", + "rimraf": "^6.0.1", "typescript": "~5.7.2", "typia": "^7.5.1" }, diff --git a/packages/migrate/package.json b/packages/migrate/package.json index 9d94e46af..595019d6c 100644 --- a/packages/migrate/package.json +++ b/packages/migrate/package.json @@ -60,7 +60,7 @@ "dotenv-expand": "^10.0.0", "express": "^4.19.2", "multer": "^1.4.5-lts.1", - "rimraf": "^5.0.1", + "rimraf": "^6.0.1", "rollup": "^4.24.3", "serialize-error": "^4.1.0", "source-map-support": "^0.5.21", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 861f9cd57..215c09d34 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -67,7 +67,7 @@ "@typescript-eslint/parser": "^5.46.1", "eslint": "^8.29.0", "eslint-plugin-deprecation": "^1.4.1", - "rimraf": "^3.0.2", + "rimraf": "^6.0.1", "tgrid": "^1.1.0", "ts-patch": "^3.3.0", "typescript": "~5.7.2", diff --git a/website/package.json b/website/package.json index 2789833f9..2b3e42b10 100644 --- a/website/package.json +++ b/website/package.json @@ -36,7 +36,7 @@ "gh-pages": "^5.0.0", "jszip": "^3.10.1", "next-sitemap": "^4.2.3", - "rimraf": "^5.0.0", + "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typedoc": "^0.27.3", "typescript": "~5.7.2"