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/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 index 42c57f9..fb533aa 100644 --- a/src/assistant/assistant-files.service.ts +++ b/src/assistant/assistant-files.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { FileObject } from 'openai/resources'; import { createReadStream } from 'fs'; -import { AiService } from './ai.service'; +import { AiService } from './ai/ai.service'; import { AssistantConfig } from './assistant.model'; @Injectable() diff --git a/src/assistant/assistant-memory.service.ts b/src/assistant/assistant-memory.service.ts index ae3c7a5..faae9f9 100644 --- a/src/assistant/assistant-memory.service.ts +++ b/src/assistant/assistant-memory.service.ts @@ -1,6 +1,7 @@ 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 { @@ -16,6 +17,8 @@ export class AssistantMemoryService { 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.module.ts b/src/assistant/assistant.module.ts index d16bfc3..007fdd5 100644 --- a/src/assistant/assistant.module.ts +++ b/src/assistant/assistant.module.ts @@ -1,12 +1,12 @@ import { DynamicModule, Module, OnModuleInit } from '@nestjs/common'; import { AssistantService } from './assistant.service'; -import { ChatbotService } from './chatbot.service'; -import { AiService } from './ai.service'; -import { RunService } from './run.service'; -import { AgentService } from './agent.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 { AssistantFilesService } from './assistant-files.service'; import { AssistantMemoryService } from './assistant-memory.service'; +import { AgentModule } from './agent/agent.module'; const sharedServices = [ AiService, @@ -15,10 +15,10 @@ const sharedServices = [ 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 d217c66..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, AssistantCreateParams } from 'openai/resources/beta'; -import { AiService } from './ai.service'; +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 { @@ -16,30 +17,41 @@ export class AssistantService { 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) { - 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) { await this.create(); } } async update(params: Partial): Promise { - this.assistant = await this.assistants.update(this.assistant.id, { - ...params, - }); + this.assistant = await this.assistants.update(this.assistant.id, params); } async create(): Promise { - const { params, options } = this.config; + const { options } = this.config; + const params = this.getParams(); this.assistant = await this.assistants.create(params, options); if (this.config.files?.length) { 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..dfe1ae9 --- /dev/null +++ b/src/chat/agents/pokemon/pokemon.service.ts @@ -0,0 +1,22 @@ +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)), + ); + } +} diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 0885804..181aab5 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -4,11 +4,13 @@ import { ChatController } from './chat.controller'; import { AssistantModule } from '../assistant/assistant.module'; import { ChatService } from './chat.service'; import { chatConfig } from './chat.config'; +import { AgentsModule } from './agents/agents.module'; @Module({ imports: [ ConfigModule.forRoot({ load: [chatConfig] }), AssistantModule.forRoot(chatConfig()), + AgentsModule, ], controllers: [ChatController], providers: [ChatService], 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/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"