Skip to content

Commit

Permalink
feat(assistant): support for assistant files and knowledge retrieval (#5
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
sebastianmusial committed Jan 2, 2024
1 parent ba6813c commit 7e2975e
Show file tree
Hide file tree
Showing 29 changed files with 437 additions and 77 deletions.
1 change: 1 addition & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
OPENAI_API_KEY=
ASSISTANT_ID=
POKEMON_API_URL=
22 changes: 22 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions src/assistant/agent.model.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/assistant/agent.service.ts

This file was deleted.

19 changes: 19 additions & 0 deletions src/assistant/agent/agent.base.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return '';
}
}
7 changes: 7 additions & 0 deletions src/assistant/agent/agent.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type Agent = (data: AgentData) => Promise<string>;
export type Agents = Record<string, Agent>;

export interface AgentData {
threadId: string;
params: string;
}
8 changes: 8 additions & 0 deletions src/assistant/agent/agent.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { AgentService } from './agent.service';

@Module({
providers: [AgentService],
exports: [AgentService],
})
export class AgentModule {}
21 changes: 21 additions & 0 deletions src/assistant/agent/agent.service.ts
Original file line number Diff line number Diff line change
@@ -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];
}
}
File renamed without changes.
31 changes: 31 additions & 0 deletions src/assistant/assistant-files.service.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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);
}
}
27 changes: 27 additions & 0 deletions src/assistant/assistant-memory.service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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}`);
}
}
}
6 changes: 6 additions & 0 deletions src/assistant/assistant.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ export interface AssistantConfig {
id: string;
params: AssistantCreateParams;
options?: RequestOptions;
filesDir?: string;
files?: string[];
}

export interface AssistantFiles {
files?: string[];
}
14 changes: 9 additions & 5 deletions src/assistant/assistant.module.ts
Original file line number Diff line number Diff line change
@@ -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],
})
Expand Down
72 changes: 42 additions & 30 deletions src/assistant/assistant.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<void> {
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<Assistant> {
const assistant = await this.assistants.create(
this.config.params,
this.config.options,
);
async update(params: Partial<AssistantCreateParams>): Promise<void> {
this.assistant = await this.assistants.update(this.assistant.id, params);
}

async create(): Promise<void> {
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<void> {
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<Assistant> {
const names = fileNames || this.config.files || [];
const file_ids = await this.assistantFilesService.create(names);

await this.update({ file_ids });
return this.assistant;
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
31 changes: 18 additions & 13 deletions src/assistant/run.service.ts → src/assistant/run/run.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,6 +13,11 @@ export class RunService {
private readonly agentsService: AgentService,
) {}

async continueRun(run: Run): Promise<Run> {
await new Promise(resolve => setTimeout(resolve, this.timeout));
return this.threads.runs.retrieve(run.thread_id, run.id);
}

async resolve(run: Run): Promise<void> {
while (true)
switch (run.status) {
Expand All @@ -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);
}
}

Expand All @@ -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,
Expand Down
Loading

0 comments on commit 7e2975e

Please sign in to comment.