Skip to content

Commit 7e2975e

Browse files
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
1 parent ba6813c commit 7e2975e

29 files changed

+437
-77
lines changed

.env.dist

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
OPENAI_API_KEY=
22
ASSISTANT_ID=
3+
POKEMON_API_URL=

.github/workflows/deploy.yaml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Vercel Production Deployment
2+
env:
3+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
4+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
5+
on:
6+
push:
7+
branches:
8+
- main
9+
- feat/assistant
10+
jobs:
11+
Deploy-Production:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v2
15+
- name: Install Vercel CLI
16+
run: npm install --global vercel@latest
17+
- name: Pull Vercel Environment Information
18+
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
19+
- name: Build Project Artifacts
20+
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
21+
- name: Deploy Project Artifacts to Vercel
22+
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
"test:e2e": "jest --config ./test/jest-e2e.json"
2121
},
2222
"dependencies": {
23+
"@nestjs/axios": "^3.0.1",
2324
"@nestjs/common": "^10.0.0",
2425
"@nestjs/config": "^3.1.1",
2526
"@nestjs/core": "^10.0.0",
2627
"@nestjs/platform-express": "^10.0.0",
28+
"axios": "^1.6.3",
29+
"class-transformer": "^0.5.1",
30+
"class-validator": "^0.14.0",
2731
"envfile": "^7.0.0",
2832
"openai": "^4.20.0",
2933
"reflect-metadata": "^0.1.13",

src/assistant/agent.model.ts

-2
This file was deleted.

src/assistant/agent.service.ts

-15
This file was deleted.

src/assistant/agent/agent.base.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { OnModuleInit } from '@nestjs/common';
2+
import { AssistantCreateParams } from 'openai/resources/beta';
3+
import { AgentService } from './agent.service';
4+
import { AgentData } from './agent.model';
5+
6+
export class AgentBase implements OnModuleInit {
7+
definition: AssistantCreateParams.AssistantToolsFunction;
8+
9+
onModuleInit(): void {
10+
this.agentService.add(this.definition, this.output.bind(this));
11+
}
12+
13+
constructor(protected readonly agentService: AgentService) {}
14+
15+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
16+
async output(data: AgentData): Promise<string> {
17+
return '';
18+
}
19+
}

src/assistant/agent/agent.model.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type Agent = (data: AgentData) => Promise<string>;
2+
export type Agents = Record<string, Agent>;
3+
4+
export interface AgentData {
5+
threadId: string;
6+
params: string;
7+
}

src/assistant/agent/agent.module.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { AgentService } from './agent.service';
3+
4+
@Module({
5+
providers: [AgentService],
6+
exports: [AgentService],
7+
})
8+
export class AgentModule {}

src/assistant/agent/agent.service.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { Agent, Agents } from './agent.model';
3+
import { AssistantCreateParams } from 'openai/resources/beta';
4+
5+
@Injectable()
6+
export class AgentService {
7+
public agents: Agents = {};
8+
public tools: AssistantCreateParams.AssistantToolsFunction[] = [];
9+
10+
add(
11+
definition: AssistantCreateParams.AssistantToolsFunction,
12+
fn: Agent,
13+
): void {
14+
this.tools.push(definition);
15+
this.agents[definition.function.name] = fn;
16+
}
17+
18+
get(name: string): Agent {
19+
return this.agents[name];
20+
}
21+
}
File renamed without changes.
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { FileObject } from 'openai/resources';
3+
import { createReadStream } from 'fs';
4+
import { AiService } from './ai/ai.service';
5+
import { AssistantConfig } from './assistant.model';
6+
7+
@Injectable()
8+
export class AssistantFilesService {
9+
constructor(
10+
@Inject('config') private config: AssistantConfig,
11+
private readonly aiService: AiService,
12+
) {}
13+
14+
async create(
15+
fileNames: string[],
16+
fileDir = this.config.filesDir,
17+
): Promise<string[]> {
18+
const files: FileObject[] = [];
19+
20+
for (const name of fileNames) {
21+
const file = await this.aiService.provider.files.create({
22+
file: createReadStream(`${fileDir || ''}/${name}`),
23+
purpose: 'assistants',
24+
});
25+
26+
files.push(file);
27+
}
28+
29+
return files.map(({ id }) => id);
30+
}
31+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { writeFile, readFile } from 'fs/promises';
3+
import * as envfile from 'envfile';
4+
import * as process from 'process';
5+
6+
@Injectable()
7+
export class AssistantMemoryService {
8+
private readonly logger = new Logger(AssistantMemoryService.name);
9+
10+
async saveAssistantId(id: string): Promise<void> {
11+
try {
12+
const sourcePath = './.env';
13+
const envVariables = await readFile(sourcePath);
14+
const parsedVariables = envfile.parse(envVariables.toString());
15+
const newVariables = {
16+
...parsedVariables,
17+
ASSISTANT_ID: id,
18+
};
19+
20+
process.env.ASSISTANT_ID = id;
21+
22+
await writeFile(sourcePath, envfile.stringify(newVariables));
23+
} catch (error) {
24+
this.logger.error(`Can't save variable: ${error}`);
25+
}
26+
}
27+
}

src/assistant/assistant.model.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@ export interface AssistantConfig {
55
id: string;
66
params: AssistantCreateParams;
77
options?: RequestOptions;
8+
filesDir?: string;
9+
files?: string[];
10+
}
11+
12+
export interface AssistantFiles {
13+
files?: string[];
814
}

src/assistant/assistant.module.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { DynamicModule, Module, OnModuleInit } from '@nestjs/common';
22
import { AssistantService } from './assistant.service';
3-
import { ChatbotService } from './chatbot.service';
4-
import { AiService } from './ai.service';
3+
import { ChatbotService } from './chatbot/chatbot.service';
4+
import { AiService } from './ai/ai.service';
5+
import { RunService } from './run/run.service';
56
import { AssistantConfig } from './assistant.model';
6-
import { RunService } from './run.service';
7-
import { AgentService } from './agent.service';
7+
import { AssistantFilesService } from './assistant-files.service';
8+
import { AssistantMemoryService } from './assistant-memory.service';
9+
import { AgentModule } from './agent/agent.module';
810

911
const sharedServices = [
1012
AiService,
1113
AssistantService,
14+
AssistantFilesService,
15+
AssistantMemoryService,
1216
ChatbotService,
1317
RunService,
14-
AgentService,
1518
];
1619

1720
@Module({
21+
imports: [AgentModule],
1822
providers: [...sharedServices],
1923
exports: [...sharedServices],
2024
})

src/assistant/assistant.service.ts

+42-30
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Inject, Injectable, Logger } from '@nestjs/common';
2-
import { Assistant } from 'openai/resources/beta';
3-
import { writeFile, readFile } from 'fs/promises';
4-
import * as envfile from 'envfile';
5-
import { AiService } from './ai.service';
2+
import { Assistant, AssistantCreateParams } from 'openai/resources/beta';
3+
import { AiService } from './ai/ai.service';
64
import { AssistantConfig } from './assistant.model';
5+
import { AssistantFilesService } from './assistant-files.service';
6+
import { AssistantMemoryService } from './assistant-memory.service';
7+
import { AgentService } from './agent/agent.service';
78

89
@Injectable()
910
export class AssistantService {
@@ -14,47 +15,58 @@ export class AssistantService {
1415
constructor(
1516
@Inject('config') private config: AssistantConfig,
1617
private readonly aiService: AiService,
18+
private readonly assistantFilesService: AssistantFilesService,
19+
private readonly assistantMemoryService: AssistantMemoryService,
20+
private readonly agentService: AgentService,
1721
) {}
1822

23+
getParams(): AssistantCreateParams {
24+
return {
25+
...this.config.params,
26+
tools: [...(this.config.params.tools || []), ...this.agentService.tools],
27+
};
28+
}
29+
1930
async init(): Promise<void> {
20-
const { id, params, options } = this.config;
31+
const { id, options } = this.config;
2132

2233
if (!id) {
23-
this.assistant = await this.create();
34+
return await this.create();
2435
}
2536

2637
try {
27-
this.assistant = await this.assistants.update(id, params, options);
38+
this.assistant = await this.assistants.update(
39+
id,
40+
this.getParams(),
41+
options,
42+
);
2843
} catch (e) {
29-
this.assistant = await this.create();
44+
await this.create();
3045
}
3146
}
3247

33-
async create(): Promise<Assistant> {
34-
const assistant = await this.assistants.create(
35-
this.config.params,
36-
this.config.options,
37-
);
48+
async update(params: Partial<AssistantCreateParams>): Promise<void> {
49+
this.assistant = await this.assistants.update(this.assistant.id, params);
50+
}
51+
52+
async create(): Promise<void> {
53+
const { options } = this.config;
54+
const params = this.getParams();
55+
this.assistant = await this.assistants.create(params, options);
3856

39-
this.logger.log(`Created new assistant (${assistant.id})`);
40-
await this.saveAssistantId(assistant.id);
57+
if (this.config.files?.length) {
58+
this.assistant = await this.updateFiles();
59+
}
4160

42-
return assistant;
61+
this.logger.log(`Created new assistant (${this.assistant.id})`);
62+
await this.assistantMemoryService.saveAssistantId(this.assistant.id);
4363
}
4464

45-
async saveAssistantId(id: string): Promise<void> {
46-
try {
47-
const sourcePath = './.env';
48-
const envVariables = await readFile(sourcePath);
49-
const parsedVariables = envfile.parse(envVariables.toString());
50-
const newVariables = {
51-
...parsedVariables,
52-
ASSISTANT_ID: id,
53-
};
54-
55-
await writeFile(sourcePath, envfile.stringify(newVariables));
56-
} catch (error) {
57-
this.logger.error(`Can't save variable: ${error}`);
58-
}
65+
async updateFiles(fileNames?: string[]): Promise<Assistant> {
66+
const names = fileNames || this.config.files || [];
67+
const file_ids = await this.assistantFilesService.create(names);
68+
69+
await this.update({ file_ids });
70+
return this.assistant;
5971
}
6072
}

src/assistant/chatbot.service.ts src/assistant/chatbot/chatbot.service.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Injectable } from '@nestjs/common';
2-
import { AiService } from './ai.service';
3-
import { AssistantService } from './assistant.service';
2+
import { AiService } from '../ai/ai.service';
3+
import { AssistantService } from '../assistant.service';
44
import {
55
MessageContentText,
66
MessageCreateParams,
77
Run,
88
ThreadMessage,
99
} from 'openai/resources/beta/threads';
10-
import { RunService } from './run.service';
10+
import { RunService } from '../run/run.service';
1111

1212
@Injectable()
1313
export class ChatbotService {

src/assistant/run.service.ts src/assistant/run/run.service.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { Run, RunSubmitToolOutputsParams } from 'openai/resources/beta/threads';
3-
import { AiService } from './ai.service';
4-
import { AgentService } from './agent.service';
3+
import { AiService } from '../ai/ai.service';
4+
import { AgentService } from '../agent/agent.service';
55

66
@Injectable()
77
export class RunService {
@@ -13,6 +13,11 @@ export class RunService {
1313
private readonly agentsService: AgentService,
1414
) {}
1515

16+
async continueRun(run: Run): Promise<Run> {
17+
await new Promise(resolve => setTimeout(resolve, this.timeout));
18+
return this.threads.runs.retrieve(run.thread_id, run.id);
19+
}
20+
1621
async resolve(run: Run): Promise<void> {
1722
while (true)
1823
switch (run.status) {
@@ -24,10 +29,10 @@ export class RunService {
2429
return;
2530
case 'requires_action':
2631
await this.submitAction(run);
32+
run = await this.continueRun(run);
2733
continue;
2834
default:
29-
await new Promise(resolve => setTimeout(resolve, this.timeout));
30-
run = await this.threads.runs.retrieve(run.thread_id, run.id);
35+
run = await this.continueRun(run);
3136
}
3237
}
3338

@@ -37,15 +42,15 @@ export class RunService {
3742
}
3843

3944
const toolCalls = run.required_action.submit_tool_outputs.tool_calls || [];
40-
const outputs: RunSubmitToolOutputsParams.ToolOutput[] = [];
41-
42-
for (const toolCall of toolCalls) {
43-
const { name, arguments: arg } = toolCall.function;
44-
const agent = this.agentsService.get(name);
45-
const output = await agent(arg);
46-
47-
outputs.push({ tool_call_id: toolCall.id, output });
48-
}
45+
const outputs: RunSubmitToolOutputsParams.ToolOutput[] = await Promise.all(
46+
toolCalls.map(async toolCall => {
47+
const { name, arguments: params } = toolCall.function;
48+
const agent = this.agentsService.get(name);
49+
const output = await agent({ params, threadId: run.thread_id });
50+
51+
return { tool_call_id: toolCall.id, output };
52+
}),
53+
);
4954

5055
await this.threads.runs.submitToolOutputs(run.thread_id, run.id, {
5156
tool_outputs: outputs,

0 commit comments

Comments
 (0)