Skip to content
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
3 changes: 3 additions & 0 deletions configs/webpack.config.main.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export default merge(baseConfig, {
RI_CLOUD_API_URL: 'RI_CLOUD_API_URL' in process.env ? process.env.RI_CLOUD_API_URL: '',
RI_CLOUD_CAPI_URL: 'RI_CLOUD_CAPI_URL' in process.env ? process.env.RI_CLOUD_CAPI_URL: '',
RI_CLOUD_API_TOKEN: 'RI_CLOUD_API_TOKEN' in process.env ? process.env.RI_CLOUD_API_TOKEN: '',
RI_AI_CONVAI_TOKEN: 'RI_AI_CONVAI_TOKEN' in process.env ? process.env.RI_AI_CONVAI_TOKEN: '',
RI_AI_QUERY_USER: 'RI_AI_QUERY_USER' in process.env ? process.env.RI_AI_QUERY_USER: '',
RI_AI_QUERY_PASS: 'RI_AI_QUERY_PASS' in process.env ? process.env.RI_AI_QUERY_PASS: '',
}),

new webpack.DefinePlugin({
Expand Down
3 changes: 3 additions & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,5 +239,8 @@ export default {
ai: {
convAiApiUrl: process.env.RI_AI_CONVAI_API_URL || 'https://staging.learn.redis.com/convai/api',
convAiToken: process.env.RI_AI_CONVAI_TOKEN,
queryApiUrl: process.env.RI_AI_QUERY_URL || 'https://rsgpt.ostability.com/api/v1',
queryApiUser: process.env.RI_AI_QUERY_USER,
queryApiPass: process.env.RI_AI_QUERY_PASS,
},
};
2 changes: 2 additions & 0 deletions redisinsight/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-aut
import { CustomTutorialModule } from 'src/modules/custom-tutorial/custom-tutorial.module';
import { CloudModule } from 'src/modules/cloud/cloud.module';
import { AiChatModule } from 'src/modules/ai/chat/ai-chat.module';
import { AiQueryModule } from 'src/modules/ai/query/ai-query.module';
import { BrowserModule } from './modules/browser/browser.module';
import { RedisEnterpriseModule } from './modules/redis-enterprise/redis-enterprise.module';
import { RedisSentinelModule } from './modules/redis-sentinel/redis-sentinel.module';
Expand Down Expand Up @@ -62,6 +63,7 @@ const PATH_CONFIG = config.get('dir_path') as Config['dir_path'];
TriggeredFunctionsModule,
CloudModule.register(),
AiChatModule,
AiQueryModule,
...(SERVER_CONFIG.staticContent
? [
ServeStaticModule.forRoot({
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/common/models/client-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ClientContext {
CLI = 'CLI',
Workbench = 'Workbench',
Profiler = 'Profiler',
AI = 'AI',
}

export class ClientMetadata {
Expand Down
6 changes: 6 additions & 0 deletions redisinsight/api/src/constants/custom-error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ export enum CustomErrorCodes {
ConvAiForbidden = 11_302,
ConvAiBadRequest = 11_303,
ConvAiNotFound = 11_304,

QueryAiInternalServerError = 11_351,
QueryAiUnauthorized = 11_351,
QueryAiForbidden = 11_352,
QueryAiBadRequest = 11_353,
QueryAiNotFound = 11_354,
}
2 changes: 1 addition & 1 deletion redisinsight/api/src/modules/ai/chat/ai-chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Response } from 'express';

@ApiTags('AI')
@UseInterceptors(ClassSerializerInterceptor)
@Controller('ai/chats')
@Controller('ai/assistant/chats')
@UsePipes(new ValidationPipe({ transform: true }))
export class AiChatController {
constructor(
Expand Down
38 changes: 38 additions & 0 deletions redisinsight/api/src/modules/ai/query/ai-query.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Body,
ClassSerializerInterceptor,
Controller,
Post,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
import { ApiTags } from '@nestjs/swagger';
import { RequestSessionMetadata } from 'src/common/decorators';
import { SessionMetadata } from 'src/common/models';
import { AiQueryService } from 'src/modules/ai/query/ai-query.service';
import { SendAiQueryMessageDto } from 'src/modules/ai/query/dto/send.ai-query.message.dto';

@ApiTags('AI')
@UseInterceptors(ClassSerializerInterceptor)
@Controller('ai/expert/queries')
@UsePipes(new ValidationPipe({ transform: true }))
export class AiQueryController {
constructor(
private readonly service: AiQueryService,
) {}

@Post('/')
@ApiEndpoint({
description: 'Generate new query',
statusCode: 200,
responses: [{ type: String }],
})
async generateQuery(
@RequestSessionMetadata() sessionMetadata: SessionMetadata,
@Body() dto: SendAiQueryMessageDto,
) {
return this.service.generateQuery(sessionMetadata, dto);
}
}
13 changes: 13 additions & 0 deletions redisinsight/api/src/modules/ai/query/ai-query.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AiQueryController } from 'src/modules/ai/query/ai-query.controller';
import { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';
import { AiQueryService } from 'src/modules/ai/query/ai-query.service';

@Module({
controllers: [AiQueryController],
providers: [
AiQueryProvider,
AiQueryService,
],
})
export class AiQueryModule {}
44 changes: 44 additions & 0 deletions redisinsight/api/src/modules/ai/query/ai-query.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { HttpException, Injectable } from '@nestjs/common';
import { ClientContext, SessionMetadata } from 'src/common/models';
import { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';
import { SendAiQueryMessageDto } from 'src/modules/ai/query/dto/send.ai-query.message.dto';
import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';
import { getFullDbContext } from 'src/modules/ai/query/utils/context.util';
import { AiQueryInternalServerErrorException } from 'src/modules/ai/query/exceptions';

@Injectable()
export class AiQueryService {
constructor(
private readonly aiQueryProvider: AiQueryProvider,
private readonly databaseClientFactory: DatabaseClientFactory,
) {
}

async getContext(sessionMetadata: SessionMetadata, dto: SendAiQueryMessageDto) {
try {
const client = await this.databaseClientFactory.getOrCreateClient({
sessionMetadata,
databaseId: dto.databaseId,
context: ClientContext.AI,
});

return await getFullDbContext(client);
} catch (e) {
return {};
}
}

async generateQuery(sessionMetadata: SessionMetadata, dto: SendAiQueryMessageDto) {
try {
const context = await this.getContext(sessionMetadata, dto);

return await this.aiQueryProvider.generateQuery(sessionMetadata, dto.content, context);
} catch (e) {
if (e instanceof HttpException) {
throw e;
}

throw new AiQueryInternalServerErrorException(e.message);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class SendAiQueryMessageDto {
@ApiProperty({
description: 'Database id',
type: String,
})
@IsString()
@IsNotEmpty()
databaseId: string;

@ApiProperty({
description: 'Message content',
type: String,
})
@IsString()
@IsNotEmpty()
content: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import { CustomErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';

export class AiQueryBadRequestException extends HttpException {
constructor(message = ERROR_MESSAGES.BAD_REQUEST, options?: HttpExceptionOptions) {
const response = {
message,
statusCode: HttpStatus.BAD_REQUEST,
error: 'AiQueryBadRequest',
errorCode: CustomErrorCodes.QueryAiInternalServerError,
};

super(response, response.statusCode, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AxiosError } from 'axios';
import { HttpException } from '@nestjs/common';
import {
AiQueryUnauthorizedException,
AiQueryForbiddenException,
AiQueryBadRequestException,
AiQueryNotFoundException,
AiQueryInternalServerErrorException,
} from 'src/modules/ai/query/exceptions';

export const wrapAiQueryError = (error: AxiosError, message?: string): HttpException => {
if (error instanceof HttpException) {
return error;
}

const { response } = error;

if (response) {
const errorOptions = { cause: new Error(response?.data as string) };
switch (response?.status) {
case 401:
return new AiQueryUnauthorizedException(message, errorOptions);
case 403:
return new AiQueryForbiddenException(message, errorOptions);
case 400:
return new AiQueryBadRequestException(message, errorOptions);
case 404:
return new AiQueryNotFoundException(message, errorOptions);
default:
return new AiQueryInternalServerErrorException(message, errorOptions);
}
}

return new AiQueryInternalServerErrorException(message);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import { CustomErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';

export class AiQueryForbiddenException extends HttpException {
constructor(message = ERROR_MESSAGES.FORBIDDEN, options?: HttpExceptionOptions) {
const response = {
message,
statusCode: HttpStatus.FORBIDDEN,
error: 'AiQueryForbidden',
errorCode: CustomErrorCodes.QueryAiForbidden,
};

super(response, response.statusCode, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import { CustomErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';

export class AiQueryInternalServerErrorException extends HttpException {
constructor(message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR, options?: HttpExceptionOptions) {
const response = {
message,
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'AiQueryInternalServerError',
errorCode: CustomErrorCodes.QueryAiInternalServerError,
};

super(response, response.statusCode, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import { CustomErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';

export class AiQueryNotFoundException extends HttpException {
constructor(message = ERROR_MESSAGES.NOT_FOUND, options?: HttpExceptionOptions) {
const response = {
message,
statusCode: HttpStatus.NOT_FOUND,
error: 'AiQueryNotFound',
errorCode: CustomErrorCodes.QueryAiNotFound,
};

super(response, response.statusCode, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
import { CustomErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';

export class AiQueryUnauthorizedException extends HttpException {
constructor(message = ERROR_MESSAGES.UNAUTHORIZED, options?: HttpExceptionOptions) {
const response = {
message,
statusCode: HttpStatus.UNAUTHORIZED,
error: 'AiQueryUnauthorized',
errorCode: CustomErrorCodes.QueryAiUnauthorized,
};

super(response, response.statusCode, options);
}
}
6 changes: 6 additions & 0 deletions redisinsight/api/src/modules/ai/query/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './ai-query.bad-request.exception';
export * from './ai-query.error.handler';
export * from './ai-query.forbidden.exception';
export * from './ai-query.internal-server-error.exception';
export * from './ai-query.not-found.exception';
export * from './ai-query.unauthorized.exception';
21 changes: 21 additions & 0 deletions redisinsight/api/src/modules/ai/query/models/ai-query.message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';

export enum AiQueryMessageType {
HumanMessage = 'HumanMessage',
AiMessage = 'AIMessage',
}

export class AiQueryMessage {
@ApiProperty({
enum: AiQueryMessageType,
})
@Expose()
type: AiQueryMessageType;

@ApiProperty({
type: String,
})
@Expose()
content: string;
}
1 change: 1 addition & 0 deletions redisinsight/api/src/modules/ai/query/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ai-query.message';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SessionMetadata } from 'src/common/models';
import config, { Config } from 'src/utils/config';
import axios from 'axios';
import { wrapAiQueryError } from 'src/modules/ai/query/exceptions';

const aiConfig = config.get('ai') as Config['ai'];

export class AiQueryProvider {
protected api = axios.create({
baseURL: aiConfig.queryApiUrl,
});

async generateQuery(sessionMetadata: SessionMetadata, question: string, context: object): Promise<object> {
try {
const { data } = await this.api.post(
'/generate_query',
{
question,
context,
},
{
auth: {
username: aiConfig.queryApiUser,
password: aiConfig.queryApiPass,
},
},
);

return data;
} catch (e) {
console.log('___ e', e.response.data)
throw wrapAiQueryError(e);
}
}
}
Loading