Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assistant): support for assistant files and knowledge retrieval #5

Merged
merged 13 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading