From 7e2975e54865f7527d29f5836ce2f2738af16898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Musia=C5=82?= Date: Tue, 2 Jan 2024 10:30:23 +0100 Subject: [PATCH 1/2] feat(assistant): support for assistant files and knowledge retrieval (#5) * feat(assistant): created assistant module with empty services * feat(app): env config and removed unused default NestJS code * feat(assistant): created basic functionality for assistant service * feat(assistant): saved the asssitant ID in the env variables * feat(assistant): update the assistant when instance exist * feat(assistant): implemented full flow without additional agents * feat(assistant): adding files to the assistant * feat(assistant): support for assistant files and knowledge retrieval * feat(agent): added example of agent * feat: vercel configuration & github actions --- .env.dist | 1 + .github/workflows/deploy.yaml | 22 ++++++ package.json | 4 ++ src/assistant/agent.model.ts | 2 - src/assistant/agent.service.ts | 15 ---- src/assistant/agent/agent.base.ts | 19 +++++ src/assistant/agent/agent.model.ts | 7 ++ src/assistant/agent/agent.module.ts | 8 +++ src/assistant/agent/agent.service.ts | 21 ++++++ src/assistant/{ => ai}/ai.service.ts | 0 src/assistant/assistant-files.service.ts | 31 ++++++++ src/assistant/assistant-memory.service.ts | 27 +++++++ src/assistant/assistant.model.ts | 6 ++ src/assistant/assistant.module.ts | 14 ++-- src/assistant/assistant.service.ts | 72 +++++++++++-------- .../{ => chatbot}/chatbot.service.ts | 6 +- src/assistant/{ => run}/run.service.ts | 31 ++++---- src/chat/agents/agents.module.ts | 7 ++ src/chat/agents/pokemon/get-pokemon.agent.ts | 46 ++++++++++++ src/chat/agents/pokemon/get-pokemon.model.ts | 6 ++ src/chat/agents/pokemon/pokemon.module.ts | 12 ++++ src/chat/agents/pokemon/pokemon.service.ts | 26 +++++++ src/chat/chat.config.ts | 9 ++- src/chat/chat.controller.ts | 13 +++- src/chat/chat.module.ts | 8 ++- src/chat/chat.service.ts | 4 +- src/knowledge/about-us.txt | 23 ++++++ vercel.json | 21 ++++++ yarn.lock | 53 ++++++++++++++ 29 files changed, 437 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/deploy.yaml delete mode 100644 src/assistant/agent.model.ts delete mode 100644 src/assistant/agent.service.ts create mode 100644 src/assistant/agent/agent.base.ts create mode 100644 src/assistant/agent/agent.model.ts create mode 100644 src/assistant/agent/agent.module.ts create mode 100644 src/assistant/agent/agent.service.ts rename src/assistant/{ => ai}/ai.service.ts (100%) create mode 100644 src/assistant/assistant-files.service.ts create mode 100644 src/assistant/assistant-memory.service.ts rename src/assistant/{ => chatbot}/chatbot.service.ts (91%) rename src/assistant/{ => run}/run.service.ts (58%) create mode 100644 src/chat/agents/agents.module.ts create mode 100644 src/chat/agents/pokemon/get-pokemon.agent.ts create mode 100644 src/chat/agents/pokemon/get-pokemon.model.ts create mode 100644 src/chat/agents/pokemon/pokemon.module.ts create mode 100644 src/chat/agents/pokemon/pokemon.service.ts create mode 100644 src/knowledge/about-us.txt create mode 100644 vercel.json diff --git a/.env.dist b/.env.dist index 4c5f6b1..913315d 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,3 @@ OPENAI_API_KEY= ASSISTANT_ID= +POKEMON_API_URL= \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..3da0d11 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,22 @@ +name: Vercel Production Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: + - main + - feat/assistant +jobs: + Deploy-Production: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Vercel CLI + run: npm install --global vercel@latest + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/package.json b/package.json index ecb57f8..9b8bc10 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,14 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "axios": "^1.6.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "envfile": "^7.0.0", "openai": "^4.20.0", "reflect-metadata": "^0.1.13", diff --git a/src/assistant/agent.model.ts b/src/assistant/agent.model.ts deleted file mode 100644 index ff34b85..0000000 --- a/src/assistant/agent.model.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type Agent = (a: string) => Promise; -export type Agents = Record; diff --git a/src/assistant/agent.service.ts b/src/assistant/agent.service.ts deleted file mode 100644 index ac474c6..0000000 --- a/src/assistant/agent.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Agent, Agents } from './agent.model'; - -@Injectable() -export class AgentService { - public agents: Agents = {}; - - add(name: string, fn: Agent): void { - this.agents[name] = fn; - } - - get(name: string): Agent { - return this.agents[name]; - } -} diff --git a/src/assistant/agent/agent.base.ts b/src/assistant/agent/agent.base.ts new file mode 100644 index 0000000..ed9e91b --- /dev/null +++ b/src/assistant/agent/agent.base.ts @@ -0,0 +1,19 @@ +import { OnModuleInit } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentService } from './agent.service'; +import { AgentData } from './agent.model'; + +export class AgentBase implements OnModuleInit { + definition: AssistantCreateParams.AssistantToolsFunction; + + onModuleInit(): void { + this.agentService.add(this.definition, this.output.bind(this)); + } + + constructor(protected readonly agentService: AgentService) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async output(data: AgentData): Promise { + return ''; + } +} diff --git a/src/assistant/agent/agent.model.ts b/src/assistant/agent/agent.model.ts new file mode 100644 index 0000000..569888c --- /dev/null +++ b/src/assistant/agent/agent.model.ts @@ -0,0 +1,7 @@ +export type Agent = (data: AgentData) => Promise; +export type Agents = Record; + +export interface AgentData { + threadId: string; + params: string; +} diff --git a/src/assistant/agent/agent.module.ts b/src/assistant/agent/agent.module.ts new file mode 100644 index 0000000..f256644 --- /dev/null +++ b/src/assistant/agent/agent.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { AgentService } from './agent.service'; + +@Module({ + providers: [AgentService], + exports: [AgentService], +}) +export class AgentModule {} diff --git a/src/assistant/agent/agent.service.ts b/src/assistant/agent/agent.service.ts new file mode 100644 index 0000000..efb1b04 --- /dev/null +++ b/src/assistant/agent/agent.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Agent, Agents } from './agent.model'; +import { AssistantCreateParams } from 'openai/resources/beta'; + +@Injectable() +export class AgentService { + public agents: Agents = {}; + public tools: AssistantCreateParams.AssistantToolsFunction[] = []; + + add( + definition: AssistantCreateParams.AssistantToolsFunction, + fn: Agent, + ): void { + this.tools.push(definition); + this.agents[definition.function.name] = fn; + } + + get(name: string): Agent { + return this.agents[name]; + } +} diff --git a/src/assistant/ai.service.ts b/src/assistant/ai/ai.service.ts similarity index 100% rename from src/assistant/ai.service.ts rename to src/assistant/ai/ai.service.ts diff --git a/src/assistant/assistant-files.service.ts b/src/assistant/assistant-files.service.ts new file mode 100644 index 0000000..fb533aa --- /dev/null +++ b/src/assistant/assistant-files.service.ts @@ -0,0 +1,31 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { FileObject } from 'openai/resources'; +import { createReadStream } from 'fs'; +import { AiService } from './ai/ai.service'; +import { AssistantConfig } from './assistant.model'; + +@Injectable() +export class AssistantFilesService { + constructor( + @Inject('config') private config: AssistantConfig, + private readonly aiService: AiService, + ) {} + + async create( + fileNames: string[], + fileDir = this.config.filesDir, + ): Promise { + const files: FileObject[] = []; + + for (const name of fileNames) { + const file = await this.aiService.provider.files.create({ + file: createReadStream(`${fileDir || ''}/${name}`), + purpose: 'assistants', + }); + + files.push(file); + } + + return files.map(({ id }) => id); + } +} diff --git a/src/assistant/assistant-memory.service.ts b/src/assistant/assistant-memory.service.ts new file mode 100644 index 0000000..faae9f9 --- /dev/null +++ b/src/assistant/assistant-memory.service.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { writeFile, readFile } from 'fs/promises'; +import * as envfile from 'envfile'; +import * as process from 'process'; + +@Injectable() +export class AssistantMemoryService { + private readonly logger = new Logger(AssistantMemoryService.name); + + async saveAssistantId(id: string): Promise { + try { + const sourcePath = './.env'; + const envVariables = await readFile(sourcePath); + const parsedVariables = envfile.parse(envVariables.toString()); + const newVariables = { + ...parsedVariables, + ASSISTANT_ID: id, + }; + + process.env.ASSISTANT_ID = id; + + await writeFile(sourcePath, envfile.stringify(newVariables)); + } catch (error) { + this.logger.error(`Can't save variable: ${error}`); + } + } +} diff --git a/src/assistant/assistant.model.ts b/src/assistant/assistant.model.ts index 898e284..ad66fd5 100644 --- a/src/assistant/assistant.model.ts +++ b/src/assistant/assistant.model.ts @@ -5,4 +5,10 @@ export interface AssistantConfig { id: string; params: AssistantCreateParams; options?: RequestOptions; + filesDir?: string; + files?: string[]; +} + +export interface AssistantFiles { + files?: string[]; } diff --git a/src/assistant/assistant.module.ts b/src/assistant/assistant.module.ts index ba27e84..007fdd5 100644 --- a/src/assistant/assistant.module.ts +++ b/src/assistant/assistant.module.ts @@ -1,20 +1,24 @@ import { DynamicModule, Module, OnModuleInit } from '@nestjs/common'; import { AssistantService } from './assistant.service'; -import { ChatbotService } from './chatbot.service'; -import { AiService } from './ai.service'; +import { ChatbotService } from './chatbot/chatbot.service'; +import { AiService } from './ai/ai.service'; +import { RunService } from './run/run.service'; import { AssistantConfig } from './assistant.model'; -import { RunService } from './run.service'; -import { AgentService } from './agent.service'; +import { AssistantFilesService } from './assistant-files.service'; +import { AssistantMemoryService } from './assistant-memory.service'; +import { AgentModule } from './agent/agent.module'; const sharedServices = [ AiService, AssistantService, + AssistantFilesService, + AssistantMemoryService, ChatbotService, RunService, - AgentService, ]; @Module({ + imports: [AgentModule], providers: [...sharedServices], exports: [...sharedServices], }) diff --git a/src/assistant/assistant.service.ts b/src/assistant/assistant.service.ts index eff9387..03d72f5 100644 --- a/src/assistant/assistant.service.ts +++ b/src/assistant/assistant.service.ts @@ -1,9 +1,10 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; -import { Assistant } from 'openai/resources/beta'; -import { writeFile, readFile } from 'fs/promises'; -import * as envfile from 'envfile'; -import { AiService } from './ai.service'; +import { Assistant, AssistantCreateParams } from 'openai/resources/beta'; +import { AiService } from './ai/ai.service'; import { AssistantConfig } from './assistant.model'; +import { AssistantFilesService } from './assistant-files.service'; +import { AssistantMemoryService } from './assistant-memory.service'; +import { AgentService } from './agent/agent.service'; @Injectable() export class AssistantService { @@ -14,47 +15,58 @@ export class AssistantService { constructor( @Inject('config') private config: AssistantConfig, private readonly aiService: AiService, + private readonly assistantFilesService: AssistantFilesService, + private readonly assistantMemoryService: AssistantMemoryService, + private readonly agentService: AgentService, ) {} + getParams(): AssistantCreateParams { + return { + ...this.config.params, + tools: [...(this.config.params.tools || []), ...this.agentService.tools], + }; + } + async init(): Promise { - const { id, params, options } = this.config; + const { id, options } = this.config; if (!id) { - this.assistant = await this.create(); + return await this.create(); } try { - this.assistant = await this.assistants.update(id, params, options); + this.assistant = await this.assistants.update( + id, + this.getParams(), + options, + ); } catch (e) { - this.assistant = await this.create(); + await this.create(); } } - async create(): Promise { - const assistant = await this.assistants.create( - this.config.params, - this.config.options, - ); + async update(params: Partial): Promise { + this.assistant = await this.assistants.update(this.assistant.id, params); + } + + async create(): Promise { + const { options } = this.config; + const params = this.getParams(); + this.assistant = await this.assistants.create(params, options); - this.logger.log(`Created new assistant (${assistant.id})`); - await this.saveAssistantId(assistant.id); + if (this.config.files?.length) { + this.assistant = await this.updateFiles(); + } - return assistant; + this.logger.log(`Created new assistant (${this.assistant.id})`); + await this.assistantMemoryService.saveAssistantId(this.assistant.id); } - async saveAssistantId(id: string): Promise { - try { - const sourcePath = './.env'; - const envVariables = await readFile(sourcePath); - const parsedVariables = envfile.parse(envVariables.toString()); - const newVariables = { - ...parsedVariables, - ASSISTANT_ID: id, - }; - - await writeFile(sourcePath, envfile.stringify(newVariables)); - } catch (error) { - this.logger.error(`Can't save variable: ${error}`); - } + async updateFiles(fileNames?: string[]): Promise { + const names = fileNames || this.config.files || []; + const file_ids = await this.assistantFilesService.create(names); + + await this.update({ file_ids }); + return this.assistant; } } diff --git a/src/assistant/chatbot.service.ts b/src/assistant/chatbot/chatbot.service.ts similarity index 91% rename from src/assistant/chatbot.service.ts rename to src/assistant/chatbot/chatbot.service.ts index ecd2a64..02ec534 100644 --- a/src/assistant/chatbot.service.ts +++ b/src/assistant/chatbot/chatbot.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { AiService } from './ai.service'; -import { AssistantService } from './assistant.service'; +import { AiService } from '../ai/ai.service'; +import { AssistantService } from '../assistant.service'; import { MessageContentText, MessageCreateParams, Run, ThreadMessage, } from 'openai/resources/beta/threads'; -import { RunService } from './run.service'; +import { RunService } from '../run/run.service'; @Injectable() export class ChatbotService { diff --git a/src/assistant/run.service.ts b/src/assistant/run/run.service.ts similarity index 58% rename from src/assistant/run.service.ts rename to src/assistant/run/run.service.ts index 57e1e99..e8727fa 100644 --- a/src/assistant/run.service.ts +++ b/src/assistant/run/run.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Run, RunSubmitToolOutputsParams } from 'openai/resources/beta/threads'; -import { AiService } from './ai.service'; -import { AgentService } from './agent.service'; +import { AiService } from '../ai/ai.service'; +import { AgentService } from '../agent/agent.service'; @Injectable() export class RunService { @@ -13,6 +13,11 @@ export class RunService { private readonly agentsService: AgentService, ) {} + async continueRun(run: Run): Promise { + await new Promise(resolve => setTimeout(resolve, this.timeout)); + return this.threads.runs.retrieve(run.thread_id, run.id); + } + async resolve(run: Run): Promise { while (true) switch (run.status) { @@ -24,10 +29,10 @@ export class RunService { return; case 'requires_action': await this.submitAction(run); + run = await this.continueRun(run); continue; default: - await new Promise(resolve => setTimeout(resolve, this.timeout)); - run = await this.threads.runs.retrieve(run.thread_id, run.id); + run = await this.continueRun(run); } } @@ -37,15 +42,15 @@ export class RunService { } const toolCalls = run.required_action.submit_tool_outputs.tool_calls || []; - const outputs: RunSubmitToolOutputsParams.ToolOutput[] = []; - - for (const toolCall of toolCalls) { - const { name, arguments: arg } = toolCall.function; - const agent = this.agentsService.get(name); - const output = await agent(arg); - - outputs.push({ tool_call_id: toolCall.id, output }); - } + const outputs: RunSubmitToolOutputsParams.ToolOutput[] = await Promise.all( + toolCalls.map(async toolCall => { + const { name, arguments: params } = toolCall.function; + const agent = this.agentsService.get(name); + const output = await agent({ params, threadId: run.thread_id }); + + return { tool_call_id: toolCall.id, output }; + }), + ); await this.threads.runs.submitToolOutputs(run.thread_id, run.id, { tool_outputs: outputs, diff --git a/src/chat/agents/agents.module.ts b/src/chat/agents/agents.module.ts new file mode 100644 index 0000000..5a5a29c --- /dev/null +++ b/src/chat/agents/agents.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { PokemonModule } from './pokemon/pokemon.module'; + +@Module({ + imports: [PokemonModule], +}) +export class AgentsModule {} diff --git a/src/chat/agents/pokemon/get-pokemon.agent.ts b/src/chat/agents/pokemon/get-pokemon.agent.ts new file mode 100644 index 0000000..62388b8 --- /dev/null +++ b/src/chat/agents/pokemon/get-pokemon.agent.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { AssistantCreateParams } from 'openai/resources/beta'; +import { AgentData } from '../../../assistant/agent/agent.model'; +import { GetPokemonParamsDto } from './get-pokemon.model'; +import { AgentService } from '../../../assistant/agent/agent.service'; +import { PokemonService } from './pokemon.service'; +import { AgentBase } from '../../../assistant/agent/agent.base'; + +@Injectable() +export class GetPokemonAgent extends AgentBase { + definition: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { + name: 'getPokemon', + description: 'Get pokemon stats and types', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'The name of the pokemon provided by user', + }, + }, + required: ['name'], + }, + }, + }; + + constructor( + protected readonly agentService: AgentService, + private readonly pokemonService: PokemonService, + ) { + super(agentService); + } + + async output(data: AgentData): Promise { + try { + const parsedData = JSON.parse(data?.params) as GetPokemonParamsDto; + const pokemon = await this.pokemonService.getPokemon(parsedData?.name); + + return JSON.stringify(pokemon); + } catch (errors) { + return `Invalid data: ${JSON.stringify(errors)}`; + } + } +} diff --git a/src/chat/agents/pokemon/get-pokemon.model.ts b/src/chat/agents/pokemon/get-pokemon.model.ts new file mode 100644 index 0000000..05d455e --- /dev/null +++ b/src/chat/agents/pokemon/get-pokemon.model.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class GetPokemonParamsDto { + @IsNotEmpty() + name: string; +} diff --git a/src/chat/agents/pokemon/pokemon.module.ts b/src/chat/agents/pokemon/pokemon.module.ts new file mode 100644 index 0000000..3d6d926 --- /dev/null +++ b/src/chat/agents/pokemon/pokemon.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { PokemonService } from './pokemon.service'; +import { GetPokemonAgent } from './get-pokemon.agent'; +import { AgentModule } from '../../../assistant/agent/agent.module'; + +@Module({ + imports: [ConfigModule, HttpModule, AgentModule], + providers: [PokemonService, GetPokemonAgent], +}) +export class PokemonModule {} diff --git a/src/chat/agents/pokemon/pokemon.service.ts b/src/chat/agents/pokemon/pokemon.service.ts new file mode 100644 index 0000000..e92ff42 --- /dev/null +++ b/src/chat/agents/pokemon/pokemon.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom, map } from 'rxjs'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class PokemonService { + apiUrl = this.configService.get('POKEMON_API_URL'); + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + async getPokemon(name: string): Promise { + return firstValueFrom( + this.httpService.get(`${this.apiUrl}/pokemon/${name.toLowerCase()}`).pipe( + map(res => res.data), + map(res => ({ + name: res?.['name'], + stats: res?.['stats'], + })), + ), + ); + } +} diff --git a/src/chat/chat.config.ts b/src/chat/chat.config.ts index d06f355..0ae23b8 100644 --- a/src/chat/chat.config.ts +++ b/src/chat/chat.config.ts @@ -3,16 +3,19 @@ import { AssistantCreateParams } from 'openai/resources/beta'; import { AssistantConfig } from '../assistant/assistant.model'; export const assistantParams: AssistantCreateParams = { - name: 'AI Template', - instructions: `You are a personal assistant.`, + name: 'Pokemon Assistant', + instructions: `You can ask questions about Pokemon. For example, "What is Pikachu's type?" or "What is Pikachu's base stats?", or "Which pokemon has a bigger chance of winning in a battle between Pikachu and Charmander?".`, tools: [{ type: 'retrieval' }], model: 'gpt-4-1106-preview', + metadata: {}, }; -export default registerAs( +export const chatConfig = registerAs( 'assistant', (): AssistantConfig => ({ id: process.env.ASSISTANT_ID || '', params: assistantParams, + filesDir: './src/knowledge', + files: ['about-us.txt'], }), ); diff --git a/src/chat/chat.controller.ts b/src/chat/chat.controller.ts index d32acd8..9ae35ad 100644 --- a/src/chat/chat.controller.ts +++ b/src/chat/chat.controller.ts @@ -1,13 +1,24 @@ import { Body, Controller, Post } from '@nestjs/common'; import { ChatCall } from './chat.model'; import { ChatService } from './chat.service'; +import { AssistantFiles } from '../assistant/assistant.model'; +import { Assistant } from 'openai/resources/beta'; +import { AssistantService } from '../assistant/assistant.service'; @Controller('chat') export class ChatController { - constructor(public readonly chatService: ChatService) {} + constructor( + public readonly chatService: ChatService, + public readonly assistantService: AssistantService, + ) {} @Post() async call(@Body() payload: ChatCall): Promise { return this.chatService.call(payload); } + + @Post('/files') + async updateFiles(@Body() { files }: AssistantFiles): Promise { + return this.assistantService.updateFiles(files); + } } diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 5095366..181aab5 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -1,16 +1,18 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ChatController } from './chat.controller'; -import chatConfig from './chat.config'; import { AssistantModule } from '../assistant/assistant.module'; import { ChatService } from './chat.service'; +import { chatConfig } from './chat.config'; +import { AgentsModule } from './agents/agents.module'; @Module({ - controllers: [ChatController], - providers: [ChatService], imports: [ ConfigModule.forRoot({ load: [chatConfig] }), AssistantModule.forRoot(chatConfig()), + AgentsModule, ], + controllers: [ChatController], + providers: [ChatService], }) export class ChatModule {} diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts index cc1637f..995371d 100644 --- a/src/chat/chat.service.ts +++ b/src/chat/chat.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { Thread } from 'openai/resources/beta'; -import { AiService } from '../assistant/ai.service'; -import { ChatbotService } from '../assistant/chatbot.service'; +import { AiService } from '../assistant/ai/ai.service'; +import { ChatbotService } from '../assistant/chatbot/chatbot.service'; import { ChatCall } from './chat.model'; @Injectable() diff --git a/src/knowledge/about-us.txt b/src/knowledge/about-us.txt new file mode 100644 index 0000000..881dba3 --- /dev/null +++ b/src/knowledge/about-us.txt @@ -0,0 +1,23 @@ +We are digital product creators & consultants. We exist to help our clients redefine their industries by building with them the products their users want. + +Agile DNA and Lean Startup in our bloodstream allow us to spend our client’s money only on what benefits their business here and now. This means that we deliver web and mobile apps in quick build-measure-learn iterations that help us validate product assumptions with real users so as to learn, tweak or pivot. We pride ourselves on unparalleled speed to market, launching products in as little as 4-6 weeks. + +We recruit for both technical mastery and business-savvy mindset to build agile teams on steroids - cross-functional, self-organizing, creative problem-solvers, taking full ownership. At Boldare, the developer thinks about the product, not just the code; the designer focuses on the users, not just the beautiful UI; and the QA engineer thinks about the return on investment, not just errors. Every product decision we make is business driven, every person is accountable for the client's success. + +Our Service Standards ensure a clear approach to overcoming challenges and transforming complexity into simplicity. They assist us in turning your ideas into reality, your ambitions into tangible outcomes, and your bold concepts into impactful products. Thanks to these standards, we maintain a consistent commitment to excellence, quality, and the client experience, making us a valuable partner in bringing your vision to life. + +This is all possible thanks to our pioneering environment that fuels the way we deliver our services and the way we do business. Self-organization, accountability, radical transparency, openness to change, build-measure-learn, all mixed together! From the start we’ve invested half of our strategic focus to build it and it pays off. Forward-thinking people thrive in this stimulating climate - both our clients and our crew. What you really purchase at Boldare is the environment. + +Boldare company in numbers: +* 7 - Product strategists with over 20 various product-related certificates +* 6 - Interdisciplinary experts in the role of distributed CTO +* 20+ - Technologies in-house +* 1 - Facilitator in every team +* 1 - Service standard for all clients +* 300+ - Released products +* 50% - Of leaders are women +* 17 - Scrum masters, holding 30+ various certifications +* 62 - Frameworks and tools in use, depending on the needs of the product +* 10+ - Different workshop types dedicated to product and business development +* 1 - Scrum master in every product development team +* 10+ - Self-organized chapters - communities focused on problem solving \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..5d6f064 --- /dev/null +++ b/vercel.json @@ -0,0 +1,21 @@ +{ + "version": 2, + "builds": [ + { + "src": "src/main.ts", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "src/main.ts", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE" + ] + } + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5f6f820..6c0ccde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -673,6 +673,11 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@nestjs/axios@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.1.tgz#b006f81dd54a49def92cfaf9a8970434567e75ce" + integrity sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ== + "@nestjs/cli@^10.0.0": version "10.2.1" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.2.1.tgz#a1d32c28e188f0fb4c3f54235c55745de4c6dd7f" @@ -1070,6 +1075,11 @@ dependencies: "@types/superagent" "*" +"@types/validator@^13.7.10": + version "13.11.7" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.7.tgz#99e19760297667ae46b7069ec8b96cbfe0a08b98" + integrity sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -1478,6 +1488,15 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +axios@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.3.tgz#7f50f23b3aa246eff43c54834272346c396613f4" + integrity sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -1784,6 +1803,20 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" + integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== + dependencies: + "@types/validator" "^13.7.10" + libphonenumber-js "^1.10.14" + validator "^13.7.0" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -2585,6 +2618,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" @@ -3635,6 +3673,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.10.14: + version "1.10.53" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz#8dbfe1355ef1a3d8e13b8d92849f7db7ebddc98f" + integrity sha512-sDTnnqlWK4vH4AlDQuswz3n4Hx7bIQWTpIcScJX+Sp7St3LXHmfiax/ZFfyYxHmkdCvydOLSuvtAO/XpXiSySw== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4237,6 +4280,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -5055,6 +5103,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +validator@^13.7.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 3da6680ddb8bbf6ab90739384e99bbb5e2d66e80 Mon Sep 17 00:00:00 2001 From: Olivier Halupczok <33556172+olivierhalupczok@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:21:34 +0100 Subject: [PATCH 2/2] chore: add readme (#6) Signed-off-by: Olivier Halupczok --- README.md | 100 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 8372941..87bef83 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,85 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - - Support us - -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. +# GenAI project template - Typescript + +Project template to make starting project easier. Project uses OpenAI API to create assistants. To demonstrate the usage of the API, project contains the agent answering the questions concerning pokemons. Project is aimed to be used as a starting point for chatbots development. + +Project is built with `nestJS` and `typescript`. + +--- + +## Pre-requisities + +- Node.js v20.0.6 or higher +- npm + +--- ## Installation +Run the following command to install the dependencies: + ```bash -$ yarn install +$ npm install ``` ## Running the app +In order to run the app, you need to run the following command: + ```bash # development -$ yarn run start +$ npm run start # watch mode -$ yarn run start:dev +$ npm run start:dev -# production mode -$ yarn run start:prod +# build +$ npm run build ``` ## Test +Tests are written with `jest` and `supertest`. To run the tests, you need to run the following command: + ```bash # unit tests -$ yarn run test +$ npm run test -# e2e tests -$ yarn run test:e2e +# watch +$ npm run test:watch # test coverage -$ yarn run test:cov +$ npm run test:cov + +# e2e tests +$ npm run test:e2e + +# debug +$ npm run test:debug ``` -## Support +### Formatting + +Following command will run the prettier: -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). -## Stay in touch +```bash +# format - prettier +$ npm run format +``` + +## Secrets + +Project takes advantage of the `envfile` dependency to manage secrets. Assuming so, you need to copy `.env.dist` to `.env` file in the root directory of the project and fill it with the relevant secrets. + +```bash +$ cp .env.dist .env +``` + +--- -- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) +## Architecture -## License +Project is oriented on the OpenAI assistants API. Therefore, it is built with the following architecture: -Nest is [MIT licensed](LICENSE). + - `src/assistant` - contains the logic responsible for creation of the assistant and its usage. Every time you run the program it will check whether an assistant exists. If not, it will create one. If yes, it will use the existing one. It has also the basis for the chatbot development. It also contains the runtime handling. +- `src/chat` - contains the logic responsible for the implementation of the chatbot. It is oriented specifically on the project's use case. It contains agents oriented for the project's need. It uses the `assistant` module to handle user requests and to implement agents defined in this directory. \ No newline at end of file