diff --git a/apps/api/src/app/chat/chat.config.ts b/apps/api/src/app/chat/chat.config.ts index ee0d1ff..28223b5 100644 --- a/apps/api/src/app/chat/chat.config.ts +++ b/apps/api/src/app/chat/chat.config.ts @@ -11,7 +11,7 @@ export const assistantParams: AssistantCreateParams = { }; export const assistantConfig: AssistantConfigParams = { - id: process.env.ASSISTANT_ID || '', + id: process.env['ASSISTANT_ID'] || '', params: assistantParams, filesDir: './apps/api/src/app/knowledge', files: [], diff --git a/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html b/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html index 659a0e6..d3e56c9 100644 --- a/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html +++ b/apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html @@ -1,5 +1,5 @@ -@if (isAudioEnabled) { - +@if (isAudioEnabled && message) { + @if(!isStarted) { play_circle } @else { diff --git a/apps/spa/src/app/modules/+chat/shared/chat.model.ts b/apps/spa/src/app/modules/+chat/shared/chat.model.ts index 37e1471..2fed212 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat.model.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat.model.ts @@ -1,5 +1,3 @@ -import { ThreadCreateParams } from 'openai/resources/beta'; - export interface AudioResponse { content: string; } @@ -11,19 +9,16 @@ export enum ChatRole { } export interface Message { + metadata?: Record; content: string; role: ChatRole; } -export interface ThreadResponse { - id: string; -} - -export interface ThreadConfig { - messages?: ThreadCreateParams.Message[]; -} - export enum ChatEvents { SendMessage = 'send_message', MessageReceived = 'message_received', } + +export enum MessageStatus { + Invisible = 'invisible', +} diff --git a/apps/spa/src/app/modules/+chat/shared/chat.service.ts b/apps/spa/src/app/modules/+chat/shared/chat.service.ts index b685807..e09811f 100644 --- a/apps/spa/src/app/modules/+chat/shared/chat.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/chat.service.ts @@ -1,12 +1,22 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Subscription, take } from 'rxjs'; -import { ChatRole, Message } from './chat.model'; +import { + BehaviorSubject, + distinctUntilChanged, + map, + mergeMap, + Subscription, + take, +} from 'rxjs'; +import { ChatRole, Message, MessageStatus } from './chat.model'; import { ChatGatewayService } from './chat-gateway.service'; import { ChatClientService } from './chat-client.service'; import { ThreadService } from './thread.service'; import { ChatFilesService } from './chat-files.service'; import { environment } from '../../../../environments/environment'; -import { OpenAiFile } from '@boldare/ai-assistant'; +import { OpenAiFile, ThreadResponse } from '@boldare/ai-assistant'; +import { Threads } from 'openai/resources/beta'; +import MessageContentText = Threads.MessageContentText; +import { ThreadMessage } from 'openai/resources/beta/threads'; @Injectable({ providedIn: 'root' }) export class ChatService { @@ -22,10 +32,43 @@ export class ChatService { ) { document.body.classList.add('ai-chat'); + this.setInitialValues(); this.watchMessages(); this.watchVisibility(); } + isMessageInvisible(message: ThreadMessage): boolean { + const metadata = message.metadata as Record; + return metadata?.['status'] === MessageStatus.Invisible; + } + + isTextMessage(message: ThreadMessage): boolean { + return message.content[0].type === 'text'; + } + + parseMessages(thread: ThreadResponse): Message[] { + return thread.messages + .reverse() + .filter( + message => + this.isTextMessage(message) && !this.isMessageInvisible(message), + ) + .map(message => ({ + content: (message.content[0] as MessageContentText).text.value, + role: message.role as ChatRole, + })); + } + + setInitialValues(): void { + this.threadService.threadId$ + .pipe( + distinctUntilChanged(), + mergeMap(threadId => this.threadService.getThread(threadId)), + map((response: ThreadResponse) => this.parseMessages(response)), + ) + .subscribe(data => this.messages$.next(data)); + } + toggle(): void { this.isVisible$.next(!this.isVisible$.value); } diff --git a/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts b/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts index 6d69bf9..a4bf148 100644 --- a/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/thread-client.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { ThreadConfig, ThreadResponse } from '@boldare/ai-assistant'; import { Observable } from 'rxjs'; import { environment } from '../../../../environments/environment'; -import { ThreadConfig, ThreadResponse } from './chat.model'; @Injectable({ providedIn: 'root' }) export class ThreadClientService { @@ -14,4 +14,10 @@ export class ThreadClientService { payload, ); } + + getThread(id: string): Observable { + return this.httpClient.get( + `${environment.apiUrl}/assistant/threads/${id}`, + ); + } } diff --git a/apps/spa/src/app/modules/+chat/shared/thread.service.ts b/apps/spa/src/app/modules/+chat/shared/thread.service.ts index 9dd7e85..4be7d60 100644 --- a/apps/spa/src/app/modules/+chat/shared/thread.service.ts +++ b/apps/spa/src/app/modules/+chat/shared/thread.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject, take, tap } from 'rxjs'; import { environment } from '../../../../environments/environment'; import { ThreadClientService } from './thread-client.service'; -import { ThreadResponse } from './chat.model'; import { ConfigurationFormService } from '../../+configuration/shared/configuration-form.service'; +import { ThreadResponse } from '@boldare/ai-assistant'; @Injectable({ providedIn: 'root' }) export class ThreadService { @@ -41,4 +41,8 @@ export class ThreadService { localStorage.removeItem(this.key); } + + getThread(id: string): Observable { + return this.threadClientService.getThread(id).pipe(take(1)); + } } diff --git a/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts b/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts index b5698f3..41313a1 100644 --- a/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts +++ b/apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { SpeechVoice } from '@boldare/ai-assistant'; +import { SpeechVoice, ThreadConfig } from '@boldare/ai-assistant'; import { ConfigurationForm } from './configuration.model'; -import { ThreadConfig } from '../../+chat/shared/chat.model'; +import { MessageStatus } from '../../+chat/shared/chat.model'; @Injectable({ providedIn: 'root' }) export class ConfigurationFormService { @@ -19,6 +19,9 @@ export class ConfigurationFormService { content: `Below you can find my details: * first name: ${this.form.controls.firstName.value || '-'} `, + metadata: { + status: MessageStatus.Invisible, + }, }, ], }; diff --git a/libs/ai-assistant/src/index.ts b/libs/ai-assistant/src/index.ts index 977633d..c7b0f65 100644 --- a/libs/ai-assistant/src/index.ts +++ b/libs/ai-assistant/src/index.ts @@ -4,3 +4,4 @@ export * from './lib/ai'; export * from './lib/chat'; export * from './lib/run'; export * from './lib/files'; +export * from './lib/threads'; diff --git a/libs/ai-assistant/src/lib/agent/agent.service.spec.ts b/libs/ai-assistant/src/lib/agent/agent.service.spec.ts new file mode 100644 index 0000000..b7ac3c0 --- /dev/null +++ b/libs/ai-assistant/src/lib/agent/agent.service.spec.ts @@ -0,0 +1,35 @@ +import { AgentService } from './agent.service'; +import { AssistantCreateParams } from 'openai/resources/beta'; + +describe('AgentService', () => { + let agentService: AgentService; + const agentNameMock = 'agent-name'; + const agentMock = async () => 'agent-result'; + const definitionMock: AssistantCreateParams.AssistantToolsFunction = { + type: 'function', + function: { name: agentNameMock }, + }; + + beforeEach(() => { + agentService = new AgentService(); + }); + + describe('add', () => { + it('should add new tool', async () => { + agentService.add(definitionMock, agentMock); + expect(agentService.tools).toContain(definitionMock); + }); + + it('should add new agent', async () => { + agentService.add(definitionMock, agentMock); + expect(agentService.agents[agentNameMock]).toEqual(agentMock); + }); + }); + + describe('get', () => { + it('should return agent', async () => { + agentService.add(definitionMock, agentMock); + expect(agentService.get(agentNameMock)).toEqual(agentMock); + }); + }); +}); diff --git a/libs/ai-assistant/src/lib/chat/chat.model.ts b/libs/ai-assistant/src/lib/chat/chat.model.ts index f39b1b0..2b4150b 100644 --- a/libs/ai-assistant/src/lib/chat/chat.model.ts +++ b/libs/ai-assistant/src/lib/chat/chat.model.ts @@ -2,6 +2,7 @@ export interface ChatCall { content: string; threadId: string; file_ids?: string[]; + metadata?: Record; } export interface ChatAudio { @@ -25,3 +26,7 @@ export enum ChatEvents { SendAudio = 'send_audio', AudioReceived = 'audio_received', } + +export enum MessageStatus { + Invisible = 'invisible', +} diff --git a/libs/ai-assistant/src/lib/chat/chat.service.ts b/libs/ai-assistant/src/lib/chat/chat.service.ts index ad5f4f1..ab3bf3d 100644 --- a/libs/ai-assistant/src/lib/chat/chat.service.ts +++ b/libs/ai-assistant/src/lib/chat/chat.service.ts @@ -17,11 +17,12 @@ export class ChatService { ) {} async call(payload: ChatCall): Promise { - const { threadId, content, file_ids } = payload; + const { threadId, content, file_ids, metadata } = payload; const message: MessageCreateParams = { role: 'user', content, file_ids, + metadata, }; await this.threads.messages.create(threadId, message); diff --git a/libs/ai-assistant/src/lib/threads/threads.controller.ts b/libs/ai-assistant/src/lib/threads/threads.controller.ts index 864b3c2..3b03904 100644 --- a/libs/ai-assistant/src/lib/threads/threads.controller.ts +++ b/libs/ai-assistant/src/lib/threads/threads.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { Thread } from 'openai/resources/beta'; -import { GetThreadParams, ThreadConfig } from './threads.model'; +import { GetThreadParams, ThreadConfig, ThreadResponse } from './threads.model'; import { ThreadsService } from './threads.service'; @Controller('assistant/threads') @@ -8,7 +8,7 @@ export class ThreadsController { constructor(private readonly threadsService: ThreadsService) {} @Get(':id') - async getThread(@Param() params: GetThreadParams): Promise { + async getThread(@Param() params: GetThreadParams): Promise { return await this.threadsService.getThread(params.id); } diff --git a/libs/ai-assistant/src/lib/threads/threads.model.ts b/libs/ai-assistant/src/lib/threads/threads.model.ts index 9e784e6..28a7d9a 100644 --- a/libs/ai-assistant/src/lib/threads/threads.model.ts +++ b/libs/ai-assistant/src/lib/threads/threads.model.ts @@ -1,4 +1,5 @@ import { ThreadCreateParams } from 'openai/resources/beta'; +import { ThreadMessage } from 'openai/resources/beta/threads'; export interface GetThreadParams { id: string; @@ -7,3 +8,8 @@ export interface GetThreadParams { export interface ThreadConfig { messages?: ThreadCreateParams.Message[]; } + +export interface ThreadResponse { + id: string; + messages: ThreadMessage[]; +} diff --git a/libs/ai-assistant/src/lib/threads/threads.service.ts b/libs/ai-assistant/src/lib/threads/threads.service.ts index 947acd6..6e7899b 100644 --- a/libs/ai-assistant/src/lib/threads/threads.service.ts +++ b/libs/ai-assistant/src/lib/threads/threads.service.ts @@ -1,14 +1,20 @@ import { Injectable } from '@nestjs/common'; import { Thread } from 'openai/resources/beta'; import { AiService } from '../ai'; -import { ThreadConfig } from './threads.model'; +import { ThreadConfig, ThreadResponse } from './threads.model'; @Injectable() export class ThreadsService { constructor(private readonly aiService: AiService) {} - async getThread(id: string): Promise { - return this.aiService.provider.beta.threads.retrieve(id); + async getThread(id: string): Promise { + const messages = await this.aiService.provider.beta.threads.messages.list( + id, + ); + return { + id, + messages: messages.data || [], + }; } async createThread({ messages }: ThreadConfig = {}): Promise {