diff --git a/.env.dist b/.env.dist index 5d84fea..670d60a 100644 --- a/.env.dist +++ b/.env.dist @@ -1,5 +1,9 @@ # OpenAI API Key OPENAI_API_KEY= - # Assistant ID - leave it empty if you don't have an assistant yet ASSISTANT_ID= + +# Agents: +# ------------------------------------------------------------------- +# OpenWeather (Current Weather Data) +OPENWEATHER_API_KEY= diff --git a/.github/workflows/demo-deploy.yml b/.github/workflows/demo-deploy.yml index e64a64f..d4e360a 100644 --- a/.github/workflows/demo-deploy.yml +++ b/.github/workflows/demo-deploy.yml @@ -1,6 +1,9 @@ name: Demo deploy on: + push: + branches: + - preview release: types: [published] diff --git a/README.md b/README.md index 4aeba56..6e2d0dc 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,19 @@ Introducing the NestJS library, designed to harness the power of OpenAI's Assist #### AI Assistant library features -- **WebSockets**: The library provides a WebSocket server for real-time communication between the client and the assistant. -- **REST API**: The library provides a REST API for communication with the assistant. - **Function calling**: The library provides a way to create functions, which allows you to extend the assistant's capabilities with custom logic. -- **File support**: The library provides a way to add files to the assistant, which allows you to extend the assistant's knowledge base with custom data. - **TTS (Text-to-Speech)**: The library provides a way to convert text to speech, which allows you to create voice-based interactions with the assistant. - **STT (Speech-to-Text)**: The library provides a way to convert speech to text, which allows you to create voice-based interactions with the assistant. +- **File support**: The library provides a way to add files to the assistant, which allows you to extend the assistant's knowledge base with custom data. +- **WebSockets**: The library provides a WebSocket server for real-time communication between the client and the assistant. +- **REST API**: The library provides a REST API for communication with the assistant. #### Additional features in the repository - **Embedded chatbot**: The library provides a way to embed the chatbot on various websites through JavaScript scripts. - **Chatbot client application**: The repository includes an example client application (SPA) with a chatbot. -## Getting started +## 🏆 Getting started In this section, you will learn how to integrate the AI Assistant library into your NestJS application. The following steps will guide you through the process of setting up the library and creating simple functionalities. @@ -55,10 +55,10 @@ npm i @boldare/openai-assistant --save ### Step 2: Env variables -Set up your environment variables, create environment variables in the `.env` file in the root directory of the project, and populate it with the necessary secrets. You will need to add the OpenAI API Key and the Assistant ID. The Assistant ID is optional, and you can leave it empty if you don't have an assistant yet. +Set up your environment variables, create environment variables in the `.env` file in the root directory of the project, and populate it with the necessary secrets. The assistant ID is optional and serves as a unique identifier for your assistant. When the environment variable is not set, the assistant will be created automatically. You can use the assistant ID to connect to an existing assistant, which can be found in the OpenAI platform after creating an assistant. Create a `.env` file in the root directory of your project and populate it with the necessary secrets: - + ```bash touch .env ``` @@ -73,54 +73,14 @@ OPENAI_API_KEY= ASSISTANT_ID= ``` -### Step 3: Configuration - -Configure the settings for your assistant. For more information about assistant parameters, you can refer to the [OpenAI documentation](https://platform.openai.com/docs/assistants/how-it-works/creating-assistants). A sample configuration can be found in ([chat.config.ts](apps%2Fapi%2Fsrc%2Fapp%2Fchat%2Fchat.config.ts)). - -```js -// chat.config.ts file - -// Default OpenAI configuration -export const assistantParams: AssistantCreateParams = { - name: 'Your assistant name', - instructions: `You are a chatbot assistant. Speak briefly and clearly.`, - tools: [ - { type: 'code_interpreter' }, - { type: 'retrieval' }, - // (...) function calling - functions are configured by extended services - ], - model: 'gpt-4-1106-preview', - metadata: {}, -}; - -// Additional configuration for our assistant -export const assistantConfig: AssistantConfigParams = { - id: process.env['ASSISTANT_ID'], // OpenAI API Key - params: assistantParams, // AssistantCreateParams - filesDir: './apps/api/src/app/knowledge', // Path to the directory with files (the final path is "fileDir" + "single file") - files: ['file1.txt', 'file2.json'], // List of file names (or paths if you didn't fill in the above parameter) -}; -``` - -Import the AI Assistant module with your configuration into the module file where you intend to use it: +Please note that the `.env` file should not be committed to the repository. Add it to the `.gitignore` file to prevent it from being committed. -```js -@Module({ - imports: [AssistantModule.forRoot(assistantConfig)], -}) -export class ChatbotModule {} -``` - -Automatically, the library will add WebSockets ([chat.gateway.ts](libs/openai-assistant/src/lib/chat/chat.gateway.ts)) and a [REST API](https://assistant.ai.boldare.dev/api/docs) for the assistant. The WebSocket server will be available at the `/` endpoint, and the [REST API](https://assistant.ai.boldare.dev/api/docs) will be available at the `/api` endpoint (depending on the API prefix). +### Step 3: Configuration -#### Websockets events +The library provides a way to configure the assistant with the `AssistantModule.forRoot` method. The method takes a configuration object as an argument. Create a new configuration file like in a [sample configuration file (chat.config.ts)](apps%2Fapi%2Fsrc%2Fapp%2Fchat%2Fchat.config.ts) and fill it with the necessary configuration. -Currently, the library provides the following WebSocket events: +More details about the configuration with code examples can be found in the [wiki](https://github.com/boldare/openai-assistant/wiki/%F0%9F%A4%96-AI-Assistant#step-3-configuration). -| Event name | Description | -| ------------------ | -------------------------------------------------------- | -| `send_message` | The event is emitted when the user sends a message. | -| `message_received` | The event is emitted when the assistant sends a message. | ### Step 4: Function calling @@ -130,117 +90,16 @@ Create a new service that extends the `AgentBase` class, fill the definition and - The `definition` property is an object that describes the function and its parameters. For more information about function calling, you can refer to the [OpenAI documentation](https://platform.openai.com/docs/assistants/tools/defining-functions). -Below is an example of a service that extends the `AgentBase` class: - -```js -@Injectable() -export class GetNicknameAgent extends AgentBase { - definition: AssistantCreateParams.AssistantToolsFunction = { - type: 'function', - function: { - name: this.constructor.name, - description: `Get the nickname of a city`, - parameters: { - type: 'object', - properties: { - location: { - type: 'string', - description: 'The city and state e.g. San Francisco, CA', - }, - }, - required: ['location'], - }, - }, - }; - - constructor(protected readonly agentService: AgentService) { - super(agentService); - } - - async output(data: AgentData): Promise { - // TODO: Your logic here - return 'Your string value'; - } -} -``` - -More examples can be found in the [agents](apps/api/src/app/chat/agents) directory. - -Import the service into the module file where you intend to use it: -```js -import { Module } from '@nestjs/common'; -import { AgentModule } from '@boldare/openai-assistant'; -import { GetNicknameAgent } from './get-nickname.agent'; - -@Module({ - imports: [AgentModule], - providers: [GetNicknameAgent], -}) -export class AgentsModule {} -``` - -and remember to add the `AgentsModule` above the `AssistantModule` in your main module file (e.g. `chat.module.ts`): - -```js -@Module({ - imports: [AgentsModule, AssistantModule.forRoot(assistantConfig)], -}) -export class ChatModule {} -``` +The instructions for creating a function can be found in the [wiki](https://github.com/boldare/openai-assistant/wiki/%F0%9F%A4%96-AI-Assistant#step-4-function-calling), while examples can be found in the [agents](apps/api/src/app/chat/agents) directory. --- # 👨‍💻 Repository -The repository includes a library with an AI assistant as well as other useful parts: +The complete documentation on how to run the demo with all applications and libraries from the repository can be found in the [wiki](https://github.com/boldare/openai-assistant/wiki/%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB-Repository). -| Name | Description | More | -|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| -| `@boldare/openai-assistant` | A NestJS library based on the OpenAI Assistant for building efficient, scalable, and quick solutions for AI assistants/chatbots | [Documentation](https://github.com/boldare/openai-assistant/wiki/%F0%9F%A4%96-AI-Assistant) | -| `@boldare/ai-embedded` | The code enables embedding the chatbot on various websites through JavaScript scripts. | [Documentation](https://github.com/boldare/openai-assistant/wiki/%F0%9F%96%87-Integrating-Chatbot-into-Your-Website) | -| `api` | Example usage of the `@boldare/openai-assistant` library. | [Documentation](https://github.com/boldare/openai-assistant/wiki/%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB-Repository) | -| `spa` | Example client application (SPA) with a chatbot. | [Documenation](https://github.com/boldare/openai-assistant/wiki/%F0%9F%92%AC-Chatbot-%E2%80%90-Client-application) | - -## Getting started - -### Step 1: Install dependencies - -```bash -npm install -``` - -### Step 2: Env variables - -Set up your environment variables, copy the `.env.dist` file to `.env` file in the root directory of the project, and populate it with the necessary secrets. - -```bash -cp .env.dist .env -``` - -### Step 3: Run applications - -```bash -# Start the app (api and spa) -npm run start:dev - -# Start the api -npm run start:api - -# Start the spa -npm run start:spa -``` - -Now you can open your browser and navigate to: - -| URL | Description | -| ------------------------------ | --------------------------------------- | -| http://localhost:4200/ | Client application (Angular) | -| http://localhost:3000/ | API application, WebSockets (socket.io) | -| http://localhost:3000/api/ | API endpoints | -| http://localhost:3000/api/docs | API documentation (swagger) | - -### 🎉 Happy coding 🎉 +--- # License diff --git a/apps/api/src/app/chat/agents/agents.module.ts b/apps/api/src/app/chat/agents/agents.module.ts index 32e62ef..225e463 100644 --- a/apps/api/src/app/chat/agents/agents.module.ts +++ b/apps/api/src/app/chat/agents/agents.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { GetAnimalAgent } from './get-animal.agent'; -import { AgentModule } from '@boldare/openai-assistant'; +import { WeatherModule } from './weather/weather.module'; +import { PokemonModule } from './pokemon/pokemon.module'; +import { CurrencyModule } from './currency/currency.module'; @Module({ - imports: [AgentModule], - providers: [GetAnimalAgent], + imports: [WeatherModule, PokemonModule, CurrencyModule], }) export class AgentsModule {} diff --git a/apps/api/src/app/chat/agents/currency/currency.module.ts b/apps/api/src/app/chat/agents/currency/currency.module.ts new file mode 100644 index 0000000..dec16f2 --- /dev/null +++ b/apps/api/src/app/chat/agents/currency/currency.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { GetCurrencyAgent } from './get-currency.agent'; +import { HttpModule } from '@nestjs/axios'; +import { CurrencyService } from './currency.service'; +import { AgentModule } from '@boldare/openai-assistant'; + +@Module({ + imports: [AgentModule, HttpModule], + providers: [CurrencyService, GetCurrencyAgent], +}) +export class CurrencyModule {} diff --git a/apps/api/src/app/chat/agents/currency/currency.service.ts b/apps/api/src/app/chat/agents/currency/currency.service.ts new file mode 100644 index 0000000..0754197 --- /dev/null +++ b/apps/api/src/app/chat/agents/currency/currency.service.ts @@ -0,0 +1,30 @@ +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { catchError, firstValueFrom } from 'rxjs'; +import { AxiosError } from 'axios'; + +@Injectable() +export class CurrencyService { + private readonly logger = new Logger(CurrencyService.name); + + constructor(private httpService: HttpService) {} + + async getExchangeRate(currency: string) { + const params = { from: currency }; + const { data } = await firstValueFrom( + this.httpService + .get('https://api.frankfurter.app/latest', { params }) + .pipe( + catchError((error: AxiosError) => { + const message = error?.response?.data || { + message: 'Unknown error', + }; + this.logger.error(message); + throw new HttpException(message, error?.response?.status || 500); + }), + ), + ); + + return data; + } +} diff --git a/apps/api/src/app/chat/agents/currency/get-currency.agent.ts b/apps/api/src/app/chat/agents/currency/get-currency.agent.ts new file mode 100644 index 0000000..0989851 --- /dev/null +++ b/apps/api/src/app/chat/agents/currency/get-currency.agent.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; +import { CurrencyService } from './currency.service'; + +@Injectable() +export class GetCurrencyAgent extends AgentBase { + override definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: this.constructor.name, + description: 'Get the current currency exchange rate.', + parameters: { + type: 'object', + properties: { + currency: { + type: 'string', + description: 'Currency code e.g. USD, EUR, GBP, etc.', + }, + }, + required: ['currency'], + }, + }, + }; + + constructor( + override readonly agentService: AgentService, + private readonly currencyService: CurrencyService, + ) { + super(agentService); + } + + override async output(data: AgentData): Promise { + try { + // Parse the parameters from the input data + const params = JSON.parse(data.params); + const currency = params?.currency; + + // Check if the currency is provided + if (!currency) { + return 'No currency provided'; + } + + // Get the current currency exchange rate + const response = await this.currencyService.getExchangeRate(currency); + + // Return the result + return `The current exchange rate for ${currency} is: ${JSON.stringify( + response, + )}`; + } catch (errors) { + // Handle the errors + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/apps/api/src/app/chat/agents/get-animal.agent.ts b/apps/api/src/app/chat/agents/get-animal.agent.ts deleted file mode 100644 index 7ab3bc2..0000000 --- a/apps/api/src/app/chat/agents/get-animal.agent.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AssistantCreateParams } from 'openai/resources/beta'; -import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; - -@Injectable() -export class GetAnimalAgent extends AgentBase { - definition: AssistantCreateParams.AssistantToolsFunction = { - type: 'function', - function: { - name: this.constructor.name, - description: `Display name od the animal`, - parameters: { - type: 'object', - properties: { - animal: { - type: 'string', - description: `Type of the animal`, - }, - }, - required: ['animal'], - }, - }, - }; - - constructor(protected readonly agentService: AgentService) { - super(agentService); - } - - async output(data: AgentData): Promise { - try { - const params = JSON.parse(data.params); - const animal = params?.animal; - const animals = [ - { id: 1, animal: 'dog', name: 'Rex' }, - { id: 2, animal: 'cat', name: 'Mittens' }, - { id: 3, animal: 'bird', name: 'Tweety' }, - { id: 4, animal: 'fish', name: 'Goldie' }, - { id: 5, animal: 'rabbit', name: 'Bugs' }, - ]; - - if (!animal) { - return 'No animal provided'; - } - - return animals.find(a => a.animal === animal)?.name || 'No such animal'; - } catch (errors) { - return `Invalid data: ${JSON.stringify(errors)}`; - } - } -} diff --git a/apps/api/src/app/chat/agents/pokemon/get-pokemon-list.agent.ts b/apps/api/src/app/chat/agents/pokemon/get-pokemon-list.agent.ts new file mode 100644 index 0000000..e55e533 --- /dev/null +++ b/apps/api/src/app/chat/agents/pokemon/get-pokemon-list.agent.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentBase, AgentService } from '@boldare/openai-assistant'; +import { PokemonService } from './pokemon.service'; + +@Injectable() +export class GetPokemonListAgent extends AgentBase { + override definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: this.constructor.name, + description: 'Get list of Pokemon.', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + }, + }; + + constructor( + override readonly agentService: AgentService, + private readonly pokemonService: PokemonService, + ) { + super(agentService); + } + + override async output(): Promise { + try { + // Get the list of Pokemon + const pokemon = await this.pokemonService.getPokemonList(); + + // Return the result + return `The list of Pokemon: ${JSON.stringify(pokemon)}`; + } catch (errors) { + // Handle the errors + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/apps/api/src/app/chat/agents/pokemon/get-pokemon-stats.agent.ts b/apps/api/src/app/chat/agents/pokemon/get-pokemon-stats.agent.ts new file mode 100644 index 0000000..6fcf1f9 --- /dev/null +++ b/apps/api/src/app/chat/agents/pokemon/get-pokemon-stats.agent.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; +import { PokemonService } from './pokemon.service'; + +@Injectable() +export class GetPokemonStatsAgent extends AgentBase { + override definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: this.constructor.name, + description: 'Get the stats of a Pokemon', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: + 'Name of the Pokemon e.g. Pikachu, Bulbasaur, Charmander, etc.', + }, + }, + required: ['name'], + }, + }, + }; + + constructor( + override readonly agentService: AgentService, + private readonly pokemonService: PokemonService, + ) { + super(agentService); + } + + override async output(data: AgentData): Promise { + try { + // Parse the parameters from the input data + const params = JSON.parse(data.params); + const name = params?.name; + + // Check if the name is provided + if (!name) { + return 'No name provided'; + } + + // Get the stats for the Pokemon + const pokemon = await this.pokemonService.getPokemon(name); + + // Return the result + return `The stats of ${name} are: ${JSON.stringify(pokemon)}`; + } catch (errors) { + // Handle the errors + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/apps/api/src/app/chat/agents/pokemon/pokemon.module.ts b/apps/api/src/app/chat/agents/pokemon/pokemon.module.ts new file mode 100644 index 0000000..6f5549d --- /dev/null +++ b/apps/api/src/app/chat/agents/pokemon/pokemon.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { AgentModule } from '@boldare/openai-assistant'; +import { GetPokemonStatsAgent } from './get-pokemon-stats.agent'; +import { PokemonService } from './pokemon.service'; +import { GetPokemonListAgent } from './get-pokemon-list.agent'; + +@Module({ + imports: [AgentModule, HttpModule], + providers: [PokemonService, GetPokemonStatsAgent, GetPokemonListAgent], +}) +export class PokemonModule {} diff --git a/apps/api/src/app/chat/agents/pokemon/pokemon.service.ts b/apps/api/src/app/chat/agents/pokemon/pokemon.service.ts new file mode 100644 index 0000000..6fd9cdb --- /dev/null +++ b/apps/api/src/app/chat/agents/pokemon/pokemon.service.ts @@ -0,0 +1,40 @@ +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { catchError, firstValueFrom } from 'rxjs'; +import { AxiosError } from 'axios'; + +@Injectable() +export class PokemonService { + private readonly logger = new Logger(PokemonService.name); + private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon'; + + constructor(private httpService: HttpService) {} + + async getPokemonList() { + const { data } = await firstValueFrom( + this.httpService.get(`${this.apiUrl}/?limit=30`).pipe( + catchError((error: AxiosError) => { + const message = error?.response?.data || { message: 'Unknown error' }; + this.logger.error(message); + throw new HttpException(message, error?.response?.status || 500); + }), + ), + ); + + return data; + } + + async getPokemon(name: string) { + const { data } = await firstValueFrom( + this.httpService.get(`${this.apiUrl}/${name?.toLowerCase()}`).pipe( + catchError((error: AxiosError) => { + const message = error?.response?.data || { message: 'Unknown error' }; + this.logger.error(message); + throw new HttpException(message, error?.response?.status || 500); + }), + ), + ); + + return data; + } +} diff --git a/apps/api/src/app/chat/agents/weather/get-current-weather.agent.ts b/apps/api/src/app/chat/agents/weather/get-current-weather.agent.ts new file mode 100644 index 0000000..012d055 --- /dev/null +++ b/apps/api/src/app/chat/agents/weather/get-current-weather.agent.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; +import { WeatherService } from './weather.service'; + +@Injectable() +export class GetCurrentWeatherAgent extends AgentBase { + override definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: this.constructor.name, + description: 'Get the current weather in location', + parameters: { + type: 'object', + properties: { + city: { + type: 'string', + description: + 'Name of the city e.g. Warsaw, San Francisco, Paris, etc.', + }, + }, + required: ['city'], + }, + }, + }; + + constructor( + override readonly agentService: AgentService, + private readonly weatherService: WeatherService, + ) { + super(agentService); + } + + override async output(data: AgentData): Promise { + try { + // Parse the parameters from the input data + const params = JSON.parse(data.params); + const city = params?.city; + + // Check if the city is provided + if (!city) { + return 'No city provided'; + } + + // Get the current weather for the city + const weather = await this.weatherService.getCurrentWeather(city); + + // Return the result + return `The current weather in ${city} is: ${JSON.stringify(weather)}`; + } catch (errors) { + // Handle the errors + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/apps/api/src/app/chat/agents/weather/weather.module.ts b/apps/api/src/app/chat/agents/weather/weather.module.ts new file mode 100644 index 0000000..7558de9 --- /dev/null +++ b/apps/api/src/app/chat/agents/weather/weather.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { GetCurrentWeatherAgent } from './get-current-weather.agent'; +import { HttpModule } from '@nestjs/axios'; +import { WeatherService } from './weather.service'; +import { AgentModule } from '@boldare/openai-assistant'; + +@Module({ + imports: [AgentModule, HttpModule], + providers: [WeatherService, GetCurrentWeatherAgent], +}) +export class WeatherModule {} diff --git a/apps/api/src/app/chat/agents/weather/weather.service.ts b/apps/api/src/app/chat/agents/weather/weather.service.ts new file mode 100644 index 0000000..cb1189c --- /dev/null +++ b/apps/api/src/app/chat/agents/weather/weather.service.ts @@ -0,0 +1,34 @@ +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { catchError, firstValueFrom } from 'rxjs'; +import { AxiosError } from 'axios'; + +@Injectable() +export class WeatherService { + private readonly logger = new Logger(WeatherService.name); + + constructor(private httpService: HttpService) {} + + async getCurrentWeather(city: string) { + const params = { + q: city, + appid: process.env['OPENWEATHER_API_KEY'] || '', + }; + + const { data } = await firstValueFrom( + this.httpService + .get('https://api.openweathermap.org/data/2.5/weather', { params }) + .pipe( + catchError((error: AxiosError) => { + const message = error?.response?.data || { + message: 'Unknown error', + }; + this.logger.error(message); + throw new HttpException(message, error?.response?.status || 500); + }), + ), + ); + + return data; + } +} diff --git a/apps/api/src/app/chat/chat.config.ts b/apps/api/src/app/chat/chat.config.ts index 719c0bc..a00b783 100644 --- a/apps/api/src/app/chat/chat.config.ts +++ b/apps/api/src/app/chat/chat.config.ts @@ -14,5 +14,5 @@ export const assistantConfig: AssistantConfigParams = { id: process.env['ASSISTANT_ID'] || '', params: assistantParams, filesDir: './apps/api/src/app/knowledge', - files: [], + files: ['33-things-to-ask-your-digital-product-development-partner.md'], }; diff --git a/apps/api/src/app/chat/chat.module.ts b/apps/api/src/app/chat/chat.module.ts index 5358d5c..51263e0 100644 --- a/apps/api/src/app/chat/chat.module.ts +++ b/apps/api/src/app/chat/chat.module.ts @@ -2,8 +2,10 @@ import { Module } from '@nestjs/common'; import { AssistantModule } from '@boldare/openai-assistant'; import { assistantConfig } from './chat.config'; import { AgentsModule } from './agents/agents.module'; +import { ChatSockets } from './chat.sockets'; @Module({ imports: [AgentsModule, AssistantModule.forRoot(assistantConfig)], + providers: [ChatSockets], }) export class ChatModule {} diff --git a/apps/api/src/app/chat/chat.sockets.ts b/apps/api/src/app/chat/chat.sockets.ts new file mode 100644 index 0000000..4e0d0ad --- /dev/null +++ b/apps/api/src/app/chat/chat.sockets.ts @@ -0,0 +1,10 @@ +import { ChatGateway, ChatService } from '@boldare/openai-assistant'; +import { WebSocketGateway } from '@nestjs/websockets'; +import { cors } from '../cors.config'; + +@WebSocketGateway({ cors }) +export class ChatSockets extends ChatGateway { + constructor(override readonly chatsService: ChatService) { + super(chatsService); + } +} diff --git a/apps/api/src/app/cors.config.ts b/apps/api/src/app/cors.config.ts new file mode 100644 index 0000000..bc2fb25 --- /dev/null +++ b/apps/api/src/app/cors.config.ts @@ -0,0 +1,6 @@ +export const isDevelopment = process.env['NODE_ENV'] === 'development'; +export const cors = { + origin: isDevelopment ? '*' : true, + methods: ['GET', 'POST'], + credentials: true, +}; diff --git a/apps/api/src/app/knowledge/33-things-to-ask-your-digital-product-development-partner.md b/apps/api/src/app/knowledge/33-things-to-ask-your-digital-product-development-partner.md new file mode 100644 index 0000000..0f1d060 --- /dev/null +++ b/apps/api/src/app/knowledge/33-things-to-ask-your-digital-product-development-partner.md @@ -0,0 +1,107 @@ +# 33 things to ask your digital product development partner + +When searching for the development partner who will be responsible for delivering your organization’s new digital product, you’re faced with a really rigorous selection process. How to choose the best solution provider, who will be able to not only to deliver on time, but also make a digital product valuable to both your users and your business? See this list of must-ask questions for potential collaborators, and a set of top-tips which will help you in your selection process. + + +#### GENERAL QUESTIONS + +**Why are you better than other, similar companies?** +Pro tip: What is the unique “thing” they have to offer to you? If the only thing they have to offer is an attractive price, then be careful. + +**What’s your experience in digital product design and development?** +Pro tip: Experience should not be the only indicator to take into account, but years of activity on the market and a long list of created products are desirable assets. + +**What’s your experience working with clients of my industry/size/type/region?** +Pro tip: The more experience they have in a similar industry or even region, the more insights and quality they can put into your product and organization. + +**How can I find information about your previous collaborations?** +Pro tip: A company with successfully delivered digital products will have a rich portfolio of well-documented and data-driven case studies to read and see. + +**Where can I find your previous partner’s opinions on how you collaborate?** +Pro tip: Look for honest and reliable reviews and opinions, and not only thoseon their own website. Services like Clutch.co are good sources of reliable testimonials. + +**What are the next steps after product release?** +Pro tip: Check they have a clear process for the product’s further development and maintenance. + +**What kind of contracts do you work with?** +Pro tip: Look up for those giving you the most freedom when changing priorities during the product development process, but at the same time allowing you to sleep well because you know exactly how much money you are spending. The time and materials (T&M) model would be the best option in this case. + +#### WORKFLOW: + +**How do you usually start a collaboration, what are the first steps?** +Pro tip: Examine their setup process for a new partner and how they will react to, care about and answer your needs. Some companies may offer you a brief discovery workshop for better understanding of your needs, expectations and business goals - every proactive action is a good sign. + +**What are the usual challenges (like major technical issues, lack of human resources, other problems) we may expect and how do you dealing with them?** +Pro tip: Ask are they prepared for most common kinds of problems and do they have processes to deal with them? + +**How flexible you are regarding adding new features and additions to the scope?** +Pro tip: See how adjustable their approach to new ideas, out-of-the-box solutions and general changes is. Most likely, your product will be slightly different than the original concept, and its backlog will grow quickly. + +**Are you going to use any external companies to work on my product?** +Pro tip: Be sure not to fall into a so-called white labeling situation when the collaborator outsources part of the works to a cheaper and usually less reliable solution provider. This will affect the final quality of your product. + +**How do you define transparency in your organization and development process?** +Pro tip: If you want to be always up to date with the project’s progress, you need to be sure that you have 24/7 access to both the vital components and also the knowledge base of the product. This means access to the proper tools but also assigned, personal points of contact. + +**Do you have experience working with in-house dev or design teams?** +Pro tip: Test whether they have experience in co-working between internal teams and how they handle it. From your point of view, this is an amazing opportunity for instant knowledge transfer. + +**Will I be the legitimate owner of the source code after the development work is done?** +Pro tip: This is a crucial issue that needs to be taken into account: what will happen with the written code when it’s time to finish the contract. + +**What is your definition of “done” when it comes to a developed digital product?** +Pro tip: Make sure you are all on the same page and make sure definitions are clear and acceptable for both sides. + +#### METHODOLOGY AND TOOLS: +**What communication and project management tools do you use on a daily basis?** +Pro tip: Check if they use modern and safe tools, and make sure there will be no communication problems because of a prefered (by one side or the other) toolset. + +**What framework/methodology do you use during your product development process?** +Pro tip: Let’s get one thing straight: the waterfall model is obsolete and inefficient. Focus on agile and scrum-backed collaborators that will give you more room for adjustments. + +**How will you keep me updated about work progress?** +Pro tip: Set rules on how often you want to be informed of progress and events during the whole process. + +**Do you have a single point of contact at your end or can I discuss issues directly with the development team?** +Pro tip: To avoid “Hmm, who should I ask to get this done” situations ask for a direct contact to someone who will be able to help you with questions. It doesn’t have to be one, dedicated person, as long as you will get what you need within a reasonable time. + +**How fast can I see a working piece of my product?** +Pro tip: While the development process is not easy, it’s not rocket science. You should expect to see results, in the form of a working piece of code, within the first or second sprint. + +**Which type of products do you have experience with?** +Pro tip: Make sure that your potential partner knows how to work on different product types, like MVPs or prototypes, among others. + +**How do you protect your (my) data?** +Pro tip: What tools and technology do they use to keep your property safe from variable problems? + +**What is your approximate delivery time?** +Pro tip: Ask if you want to make sure their calculations are based on previous experience, and not wishful thinking. + +**What technologies do you use?** +Pro tip: If you’re looking for a partner to maintain, scale or add features to an existing product, make sure they have the required knowledge and experience in the necessary technologies. + +**Who will be assigned to my project, can I meet the team?** +Pro tip: Transparency is essential while working with external partners, and knowing who will work on your product is extremely important. Mutual trust influences quality and final results of the project. + +**How do you assure quality of the code and other components of the product?** +Pro tip: There are plenty of ways of checking if the emerging digital product meets high standards, not only from the code perspective. Ask your potential partner about unit tests, functional tests, smoke tests, user interface tests, user experience tests, regression tests, security audits, performance tests and manual acceptance testing. Not all of them are universal but combined, they will make the difference + +**Will your team be focused only on my product, or it will be working simultaneously on other things?** +Pro tip: Make sure your external team will be devoted only to your project and nothing will distract them from the main goal. + +**How will you assure me during the product development that everything is going well?** +Pro tip: Mutual relations are essential while working on such complex products. Make sure you can visit your collaborator, meet the team and see the product in the making. + +**How safe, taking in account infrastructure of different kinds (internet connectivity, secure and interrupted electric grid, recurring and unpredictable weather conditions) is your office?** +Pro tip: You’re looking for the most profitable offer but don’t forget to pay attention to the geographical and political situation in your partner’s country. Most likely, the cheapest offer won’t guarantee final success and may carry some risk. + +#### COSTS: + +**Do you charge per hour, per month or per team?** +Pro tip: There are many ways to charge for work and you should choose the most suitable for you. However, the most beneficial one is to hire a dedicated team, including developers and designers - you can be sure that such a team will be focused solely on your product, guaranteeing you quality and efficiency. + +**What is your hourly rate? How do you charge for the work done?** +Pro tip: Once again, an experienced collaborator should have no problems with providing a clear and plain price plan. + +**Do you provide Project Managers to support the project development and if yes, at what cost?** +Pro Tip: You definitely want to be kept informed of the progress of works and be up to date with everything. A Product Manager or dedicated Scrum Master should take care of issues like budget tracking and development progress. Ask if the potential partner provides this kind of assistance and at what cost. diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index bfddc7f..8526b4c 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,8 +1,8 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; - import { AppModule } from './app/app.module'; +import { cors } from './app/cors.config'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -12,15 +12,14 @@ async function bootstrap() { .setVersion('0.1.0') .build(); const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); app.setGlobalPrefix(globalPrefix); - app.enableCors({ - origin: '*', - credentials: true, - }); - const port = process.env.PORT || 3000; + app.enableCors(cors); + + const port = process.env['PORT'] || 3000; await app.listen(port); Logger.log( diff --git a/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.html b/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.html index ff6060a..464ecbd 100644 --- a/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.html +++ b/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.html @@ -1,2 +1,16 @@ -A NestJS library for building efficient, scalable, and quick solutions based on -the OpenAI Assistant API (chatbots) 🤖 🚀 +
+ A NestJS library for building efficient, scalable, and quick solutions based + on the OpenAI Assistant API (chatbots) 🤖 🚀 +
+ +
+ The following are some examples of what you can do with the demo: + +
    +
  • Speak about the weather (eg. What's the weather in London?)
  • +
  • + Speak about the exchange rate (eg. What's the exchange rate for USD?) +
  • +
  • Speak about the Pokemon (eg. Show me the stats of Pikachu)
  • +
+
diff --git a/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.scss b/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.scss index da4227e..48ee8bd 100644 --- a/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.scss +++ b/apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.scss @@ -3,8 +3,23 @@ margin-top: 40px; color: var(--color-grey-600); font-size: 18px; - text-align: center; letter-spacing: 0.03em; + text-align: center; line-height: 1.6; font-weight: 300; } + +.chat-home__examples { + margin-top: 40px; + font-size: 15px; + text-align: left; +} + +.chat-home__list { + text-align: left; + margin-left: 12px; + + > li { + padding-left: 8px; + } +} diff --git a/apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.ts b/apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.ts index 264aa2d..10f276e 100644 --- a/apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.ts +++ b/apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.ts @@ -35,10 +35,11 @@ export class ChatIframeComponent implements OnInit { isRefreshEnabled = environment.isRefreshEnabled; isConfigEnabled = environment.isConfigEnabled; tips = [ - 'Hello there! 👋', - 'Could you please tell me your name?', - 'Hello! How can you help me?', - 'Hello! 👋 How are you?', + 'Hello! 👋 How can you help me?', + 'What’s the weather like in Warsaw?', + 'What is the exchange rate for USD?', + 'Show me list of Pokémon', + 'Show me the stats for Pikachu (Pokémon)?', ]; constructor( diff --git a/libs/openai-assistant/package.json b/libs/openai-assistant/package.json index dc6f052..f3dd38c 100644 --- a/libs/openai-assistant/package.json +++ b/libs/openai-assistant/package.json @@ -1,7 +1,7 @@ { "name": "@boldare/openai-assistant", "description": "NestJS library for building chatbot solutions based on the OpenAI Assistant API", - "version": "0.0.1-dev.1", + "version": "1.0.0", "private": false, "dependencies": { "tslib": "^2.3.0", @@ -13,8 +13,10 @@ "@nestjs/axios": "^3.0.1", "@nestjs/websockets": "^10.3.0", "@nestjs/platform-socket.io": "^10.3.0", + "@nestjs/swagger": "^7.3.0", "socket.io": "^4.7.3", - "multer": "^1.4.4-lts.1" + "multer": "^1.4.4-lts.1", + "class-validator": "^0.14.1" }, "type": "commonjs", "main": "./src/index.js", diff --git a/libs/openai-assistant/src/lib/chat/chat.gateway.spec.ts b/libs/openai-assistant/src/lib/chat/chat.gateway.spec.ts index c74c181..9c6a60f 100644 --- a/libs/openai-assistant/src/lib/chat/chat.gateway.spec.ts +++ b/libs/openai-assistant/src/lib/chat/chat.gateway.spec.ts @@ -1,7 +1,6 @@ import { Test } from '@nestjs/testing'; import { Socket } from 'socket.io'; import { ChatGateway } from './chat.gateway'; -import { AiModule } from './../ai/ai.module'; import { ChatModule } from './chat.module'; import { ChatService } from './chat.service'; @@ -11,11 +10,16 @@ describe('ChatGateway', () => { beforeEach(async () => { const moduleRef = await Test.createTestingModule({ - imports: [AiModule, ChatModule], + imports: [ChatModule], + providers: [ChatGateway], }).compile(); - chatGateway = moduleRef.get(ChatGateway); chatService = moduleRef.get(ChatService); + chatGateway = new ChatGateway(chatService); + + jest + .spyOn(chatService, 'call') + .mockResolvedValue({ threadId: '123', content: 'Hello' }); }); it('should be defined', () => { @@ -24,7 +28,6 @@ describe('ChatGateway', () => { describe('listenForMessages', () => { it('should call chatService.call', async () => { - jest.spyOn(chatService, 'call').mockReturnThis(); const request = { threadId: '123', content: 'Hello' }; await chatGateway.listenForMessages(request, {} as Socket); diff --git a/libs/openai-assistant/src/lib/chat/chat.gateway.ts b/libs/openai-assistant/src/lib/chat/chat.gateway.ts index d56b109..b814488 100644 --- a/libs/openai-assistant/src/lib/chat/chat.gateway.ts +++ b/libs/openai-assistant/src/lib/chat/chat.gateway.ts @@ -4,25 +4,17 @@ import { MessageBody, OnGatewayConnection, SubscribeMessage, - WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatEvents, ChatCallDto } from './chat.model'; import { ChatService } from './chat.service'; -@WebSocketGateway({ - cors: { - origin: '*', - methods: ['GET', 'POST'], - credentials: true, - }, -}) export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server!: Server; private readonly logger: Logger; - constructor(private readonly chatsService: ChatService) { + constructor(protected readonly chatsService: ChatService) { this.logger = new Logger(ChatGateway.name); } diff --git a/libs/openai-assistant/src/lib/chat/chat.module.ts b/libs/openai-assistant/src/lib/chat/chat.module.ts index 9367eb2..fdbcd5b 100644 --- a/libs/openai-assistant/src/lib/chat/chat.module.ts +++ b/libs/openai-assistant/src/lib/chat/chat.module.ts @@ -4,14 +4,13 @@ import { RunModule } from '../run'; import { ChatHelpers } from './chat.helpers'; import { ChatService } from './chat.service'; import { SocketModule } from '@nestjs/websockets/socket-module'; -import { ChatGateway } from './chat.gateway'; import { ChatController } from './chat.controller'; export const sharedServices = [ChatHelpers, ChatService]; @Module({ imports: [SocketModule, AiModule, RunModule], - providers: [ChatGateway, ...sharedServices], + providers: [...sharedServices], controllers: [ChatController], exports: [...sharedServices], }) diff --git a/package.json b/package.json index dadf952..d3c299b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@boldare/source", - "version": "0.0.1", + "version": "1.0.0", "license": "MIT", "scripts": { "start": "node dist/apps/api/main.js",