var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var CopilotController_1; import { Controller, Get, Logger, Param, Query, Req, Res, Sse, } from '@nestjs/common'; import { catchError, concatMap, connect, EMPTY, finalize, from, interval, map, merge, mergeMap, Subject, switchMap, takeUntil, toArray, } from 'rxjs'; import { Public } from '../../core/auth'; import { CurrentUser } from '../../core/auth/current-user'; import { BlobNotFound, Config, CopilotFailedToGenerateText, CopilotSessionNotFound, mapSseError, NoCopilotProviderAvailable, UnsplashIsNotConfigured, } from '../../fundamentals'; import { CopilotProviderService } from './providers'; import { ChatSessionService } from './session'; import { CopilotStorage } from './storage'; import { CopilotCapability } from './types'; import { CopilotWorkflowService, GraphExecutorState } from './workflow'; const PING_INTERVAL = 5000; let CopilotController = CopilotController_1 = class CopilotController { constructor(config, chatSession, provider, workflow, storage) { this.config = config; this.chatSession = chatSession; this.provider = provider; this.workflow = workflow; this.storage = storage; this.logger = new Logger(CopilotController_1.name); } async checkRequest(userId, sessionId, messageId) { await this.chatSession.checkQuota(userId); const session = await this.chatSession.get(sessionId); if (!session || session.config.userId !== userId) { throw new CopilotSessionNotFound(); } const ret = { model: session.model }; if (messageId && typeof messageId === 'string') { const message = await session.getMessageById(messageId); ret.hasAttachment = Array.isArray(message.attachments) && !!message.attachments.length; } return ret; } async chooseTextProvider(userId, sessionId, messageId) { const { hasAttachment, model } = await this.checkRequest(userId, sessionId, messageId); let provider = await this.provider.getProviderByCapability(CopilotCapability.TextToText, model); // fallback to image to text if text to text is not available if (!provider && hasAttachment) { provider = await this.provider.getProviderByCapability(CopilotCapability.ImageToText, model); } if (!provider) { throw new NoCopilotProviderAvailable(); } return provider; } async appendSessionMessage(sessionId, messageId) { const session = await this.chatSession.get(sessionId); if (!session) { throw new CopilotSessionNotFound(); } if (messageId) { await session.pushByMessageId(messageId); } else { // revert the latest message generated by the assistant // if messageId is not provided, then we can retry the action await this.chatSession.revertLatestMessage(sessionId); session.revertLatestMessage(); } return session; } prepareParams(params) { const messageId = Array.isArray(params.messageId) ? params.messageId[0] : params.messageId; delete params.messageId; return { messageId, params }; } getSignal(req) { const controller = new AbortController(); req.on('close', () => controller.abort()); return controller.signal; } parseNumber(value) { if (!value) { return undefined; } const num = Number.parseInt(Array.isArray(value) ? value[0] : value, 10); if (Number.isNaN(num)) { return undefined; } return num; } mergePingStream(messageId, source$) { const subject$ = new Subject(); const ping$ = interval(PING_INTERVAL).pipe(map(() => ({ type: 'ping', id: messageId, data: '' })), takeUntil(subject$)); return merge(source$.pipe(finalize(() => subject$.next(null))), ping$); } async chat(user, req, sessionId, params) { const { messageId } = this.prepareParams(params); const provider = await this.chooseTextProvider(user.id, sessionId, messageId); const session = await this.appendSessionMessage(sessionId, messageId); try { const content = await provider.generateText(session.finish(params), session.model, { ...session.config.promptConfig, signal: this.getSignal(req), user: user.id, }); session.push({ role: 'assistant', content, createdAt: new Date(), }); await session.save(); return content; } catch (e) { throw new CopilotFailedToGenerateText(e.message); } } async chatStream(user, req, sessionId, params) { try { const { messageId } = this.prepareParams(params); const provider = await this.chooseTextProvider(user.id, sessionId, messageId); const session = await this.appendSessionMessage(sessionId, messageId); const source$ = from(provider.generateTextStream(session.finish(params), session.model, { ...session.config.promptConfig, signal: this.getSignal(req), user: user.id, })).pipe(connect(shared$ => merge( // actual chat event stream shared$.pipe(map(data => ({ type: 'message', id: messageId, data }))), // save the generated text to the session shared$.pipe(toArray(), concatMap(values => { session.push({ role: 'assistant', content: values.join(''), createdAt: new Date(), }); return from(session.save()); }), switchMap(() => EMPTY)))), catchError(mapSseError)); return this.mergePingStream(messageId, source$); } catch (err) { return mapSseError(err); } } async chatWorkflow(user, req, sessionId, params) { try { const { messageId } = this.prepareParams(params); const session = await this.appendSessionMessage(sessionId, messageId); const latestMessage = session.stashMessages.findLast(m => m.role === 'user'); if (latestMessage) { params = Object.assign({}, params, latestMessage.params, { content: latestMessage.content, attachments: latestMessage.attachments, }); } const source$ = from(this.workflow.runGraph(params, session.model, { ...session.config.promptConfig, signal: this.getSignal(req), user: user.id, })).pipe(connect(shared$ => merge( // actual chat event stream shared$.pipe(map(data => { switch (data.status) { case GraphExecutorState.EmitContent: return { type: 'message', id: messageId, data: data.content, }; case GraphExecutorState.EmitAttachment: return { type: 'attachment', id: messageId, data: data.attachment, }; default: return { type: 'event', id: messageId, data: { status: data.status, id: data.node.id, type: data.node.config.nodeType, }, }; } })), // save the generated text to the session shared$.pipe(toArray(), concatMap(values => { session.push({ role: 'assistant', content: values.join(''), createdAt: new Date(), }); return from(session.save()); }), switchMap(() => EMPTY)))), catchError(mapSseError)); return this.mergePingStream(messageId, source$); } catch (err) { return mapSseError(err); } } async chatImagesStream(user, req, sessionId, params) { try { const { messageId } = this.prepareParams(params); const { model, hasAttachment } = await this.checkRequest(user.id, sessionId, messageId); const provider = await this.provider.getProviderByCapability(hasAttachment ? CopilotCapability.ImageToImage : CopilotCapability.TextToImage, model); if (!provider) { throw new NoCopilotProviderAvailable(); } const session = await this.appendSessionMessage(sessionId, messageId); const handleRemoteLink = this.storage.handleRemoteLink.bind(this.storage, user.id, sessionId); const source$ = from(provider.generateImagesStream(session.finish(params), session.model, { ...session.config.promptConfig, seed: this.parseNumber(params.seed), signal: this.getSignal(req), user: user.id, })).pipe(mergeMap(handleRemoteLink), connect(shared$ => merge( // actual chat event stream shared$.pipe(map(attachment => ({ type: 'attachment', id: messageId, data: attachment, }))), // save the generated text to the session shared$.pipe(toArray(), concatMap(attachments => { session.push({ role: 'assistant', content: '', attachments: attachments, createdAt: new Date(), }); return from(session.save()); }), switchMap(() => EMPTY)))), catchError(mapSseError)); return this.mergePingStream(messageId, source$); } catch (err) { return mapSseError(err); } } async unsplashPhotos(req, res, params) { const { unsplashKey } = this.config.plugins.copilot || {}; if (!unsplashKey) { throw new UnsplashIsNotConfigured(); } const query = new URLSearchParams(params); const response = await fetch(`https://api.unsplash.com/search/photos?${query}`, { headers: { Authorization: `Client-ID ${unsplashKey}` }, signal: this.getSignal(req), }); res.set({ 'Content-Type': response.headers.get('Content-Type'), 'Content-Length': response.headers.get('Content-Length'), 'X-Ratelimit-Limit': response.headers.get('X-Ratelimit-Limit'), 'X-Ratelimit-Remaining': response.headers.get('X-Ratelimit-Remaining'), }); res.status(response.status).send(await response.json()); } async getBlob(res, userId, workspaceId, key) { const { body, metadata } = await this.storage.get(userId, workspaceId, key); if (!body) { throw new BlobNotFound({ workspaceId, blobId: key, }); } // metadata should always exists if body is not null if (metadata) { res.setHeader('content-type', metadata.contentType); res.setHeader('last-modified', metadata.lastModified.toUTCString()); res.setHeader('content-length', metadata.contentLength); } else { this.logger.warn(`Blob ${workspaceId}/${key} has no metadata`); } res.setHeader('cache-control', 'public, max-age=2592000, immutable'); body.pipe(res); } }; __decorate([ Get('/chat/:sessionId'), __param(0, CurrentUser()), __param(1, Req()), __param(2, Param('sessionId')), __param(3, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, String, Object]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "chat", null); __decorate([ Sse('/chat/:sessionId/stream'), __param(0, CurrentUser()), __param(1, Req()), __param(2, Param('sessionId')), __param(3, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, String, Object]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "chatStream", null); __decorate([ Sse('/chat/:sessionId/workflow'), __param(0, CurrentUser()), __param(1, Req()), __param(2, Param('sessionId')), __param(3, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, String, Object]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "chatWorkflow", null); __decorate([ Sse('/chat/:sessionId/images'), __param(0, CurrentUser()), __param(1, Req()), __param(2, Param('sessionId')), __param(3, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, String, Object]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "chatImagesStream", null); __decorate([ Get('/unsplash/photos'), __param(0, Req()), __param(1, Res()), __param(2, Query()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "unsplashPhotos", null); __decorate([ Public(), Get('/blob/:userId/:workspaceId/:key'), __param(0, Res()), __param(1, Param('userId')), __param(2, Param('workspaceId')), __param(3, Param('key')), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, String, String, String]), __metadata("design:returntype", Promise) ], CopilotController.prototype, "getBlob", null); CopilotController = CopilotController_1 = __decorate([ Controller('/api/copilot'), __metadata("design:paramtypes", [Config, ChatSessionService, CopilotProviderService, CopilotWorkflowService, CopilotStorage]) ], CopilotController); export { CopilotController }; //# sourceMappingURL=controller.js.map