Skip to content

Commit 6b19e23

Browse files
authored
Merge pull request #3380 from RedisInsight/be/feature/RI-5724-enhanse-sm-session
fix retries
2 parents f0db5e5 + 8c53dfd commit 6b19e23

36 files changed

+594
-319
lines changed

redisinsight/api/config/features-config.json

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": 2.4602,
2+
"version": 2.4603,
33
"features": {
44
"insightsRecommendations": {
55
"flag": true,
@@ -47,28 +47,6 @@
4747
}
4848
}
4949
},
50-
"documentationChat": {
51-
"flag": true,
52-
"perc": [[0,100]],
53-
"filters": [
54-
{
55-
"name": "config.server.buildType",
56-
"value": "ELECTRON",
57-
"cond": "eq"
58-
}
59-
]
60-
},
61-
"databaseChat": {
62-
"flag": true,
63-
"perc": [[0,100]],
64-
"filters": [
65-
{
66-
"name": "config.server.buildType",
67-
"value": "ELECTRON",
68-
"cond": "eq"
69-
}
70-
]
71-
},
7250
"cloudSsoRecommendedSettings": {
7351
"flag": true,
7452
"perc": [[0, 100]],

redisinsight/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"reflect-metadata": "^0.1.13",
8383
"rxjs": "^7.5.6",
8484
"socket.io": "^4.6.2",
85+
"socket.io-client": "^4.7.5",
8586
"source-map-support": "^0.5.19",
8687
"sqlite3": "5.1.6",
8788
"swagger-ui-express": "^4.1.4",
@@ -125,7 +126,6 @@
125126
"nyc": "^15.1.0",
126127
"object-diff": "^0.0.4",
127128
"rimraf": "^3.0.2",
128-
"socket.io-client": "^4.4.1",
129129
"socket.io-mock": "^1.3.2",
130130
"supertest": "^4.0.2",
131131
"ts-jest": "^26.1.0",

redisinsight/api/src/__mocks__/cloud-capi-key.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,6 @@ export const mockCloudCapiKeyService = jest.fn(() => ({
6969
export const mockCloudCapiKeyAnalytics = jest.fn(() => ({
7070
sendCloudAccountKeyGenerated: jest.fn(),
7171
sendCloudAccountKeyGenerationFailed: jest.fn(),
72+
sendCloudAccountSecretGenerated: jest.fn(),
73+
sendCloudAccountSecretGenerationFailed: jest.fn(),
7274
}));
Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
1+
import { HttpStatus } from '@nestjs/common';
2+
import ERROR_MESSAGES from 'src/constants/error-messages';
3+
import { CustomErrorCodes } from 'src/constants';
4+
15
export const mockCapiUnauthorizedError = {
26
message: 'Request failed with status code 401',
37
response: {
48
status: 401,
59
},
610
};
711

8-
export const mockApiInternalServerError = {
12+
export const mockSmApiUnauthorizedError = mockCapiUnauthorizedError;
13+
14+
export const mockSmApiInternalServerError = {
915
message: 'Something wrong',
1016
response: {
1117
status: 500,
1218
},
1319
};
1420

21+
export const mockSmApiBadRequestError = {
22+
message: 'Bad Request',
23+
response: {
24+
status: 400,
25+
},
26+
};
27+
1528
export const mockUtm = {
1629
source: 'redisinsight',
1730
medium: 'sso',
1831
campaign: 'workbench',
1932
};
33+
34+
export const mockCloudApiUnauthorizedExceptionResponse = {
35+
error: 'CloudApiUnauthorized',
36+
errorCode: CustomErrorCodes.CloudApiUnauthorized,
37+
message: ERROR_MESSAGES.UNAUTHORIZED,
38+
statusCode: HttpStatus.UNAUTHORIZED,
39+
};
40+
41+
export const mockCloudApiBadRequestExceptionResponse = {
42+
error: 'CloudApiBadRequest',
43+
errorCode: CustomErrorCodes.CloudApiBadRequest,
44+
message: ERROR_MESSAGES.BAD_REQUEST,
45+
statusCode: HttpStatus.BAD_REQUEST,
46+
};

redisinsight/api/src/__mocks__/cloud-user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const mockCloudUserRepository = jest.fn(() => ({
136136
export const mockCloudUserApiService = jest.fn(() => ({
137137
getCapiKeys: jest.fn().mockResolvedValue(mockCloudCapiAuthDto),
138138
me: jest.fn().mockResolvedValue(mockCloudUser),
139+
getCloudUser: jest.fn().mockResolvedValue(mockCloudUser),
139140
setCurrentAccount: jest.fn(),
140141
updateUser: jest.fn(),
141142
}));

redisinsight/api/src/constants/telemetry-events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export enum TelemetryEvents {
3939
// Event for cloud CAPI keys
4040
CloudAccountKeyGenerated = 'CLOUD_ACCOUNT_KEY_GENERATED',
4141
CloudAccountKeyGenerationFailed = 'CLOUD_ACCOUNT_KEY_GENERATION_FAILED',
42+
CloudAccountSecretGenerated = 'CLOUD_ACCOUNT_SECRET_GENERATED',
43+
CloudAccountSecretGenerationFailed = 'CLOUD_ACCOUNT_SECRET_GENERATION_FAILED',
4244

4345
// Events for cli tool
4446
CliClientCreated = 'CLI_CLIENT_CREATED',

redisinsight/api/src/modules/ai/query/ai-query.service.ts

Lines changed: 110 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -83,133 +83,139 @@ export class AiQueryService {
8383
dto: SendAiQueryMessageDto,
8484
res: Response,
8585
) {
86-
let socket: Socket;
86+
return this.aiQueryAuthProvider.callWithAuthRetry(sessionMetadata, async () => {
87+
let socket: Socket;
8788

88-
try {
89-
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
90-
const history = await this.aiQueryMessageRepository.list(sessionMetadata, databaseId, auth.accountId);
89+
try {
90+
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
91+
const history = await this.aiQueryMessageRepository.list(sessionMetadata, databaseId, auth.accountId);
9192

92-
const client = await this.databaseClientFactory.getOrCreateClient({
93-
sessionMetadata,
94-
databaseId,
95-
context: ClientContext.AI,
96-
});
97-
98-
let context = await this.aiQueryContextRepository.getFullDbContext(sessionMetadata, databaseId, auth.accountId);
99-
100-
if (!context) {
101-
context = await this.aiQueryContextRepository.setFullDbContext(
93+
const client = await this.databaseClientFactory.getOrCreateClient({
10294
sessionMetadata,
10395
databaseId,
104-
auth.accountId,
105-
await getFullDbContext(client),
106-
);
107-
}
96+
context: ClientContext.AI,
97+
});
10898

109-
const question = classToClass(AiQueryMessage, {
110-
type: AiQueryMessageType.HumanMessage,
111-
content: dto.content,
112-
databaseId,
113-
accountId: auth.accountId,
114-
createdAt: new Date(),
115-
});
116-
117-
const answer = classToClass(AiQueryMessage, {
118-
type: AiQueryMessageType.AiMessage,
119-
content: '',
120-
databaseId,
121-
accountId: auth.accountId,
122-
});
123-
124-
socket = await this.aiQueryProvider.getSocket(auth);
125-
126-
socket.on(AiQueryWsEvents.REPLY_CHUNK, (chunk) => {
127-
answer.content += chunk;
128-
res.write(chunk);
129-
});
130-
131-
socket.on(AiQueryWsEvents.GET_INDEX, async (index, cb) => {
132-
try {
133-
const indexContext = await this.aiQueryContextRepository.getIndexContext(
99+
let context = await this.aiQueryContextRepository.getFullDbContext(sessionMetadata, databaseId, auth.accountId);
100+
101+
if (!context) {
102+
context = await this.aiQueryContextRepository.setFullDbContext(
134103
sessionMetadata,
135104
databaseId,
136105
auth.accountId,
137-
index,
106+
await getFullDbContext(client),
138107
);
108+
}
109+
110+
const question = classToClass(AiQueryMessage, {
111+
type: AiQueryMessageType.HumanMessage,
112+
content: dto.content,
113+
databaseId,
114+
accountId: auth.accountId,
115+
createdAt: new Date(),
116+
});
117+
118+
const answer = classToClass(AiQueryMessage, {
119+
type: AiQueryMessageType.AiMessage,
120+
content: '',
121+
databaseId,
122+
accountId: auth.accountId,
123+
});
124+
125+
socket = await this.aiQueryProvider.getSocket(sessionMetadata, auth);
126+
127+
socket.on(AiQueryWsEvents.REPLY_CHUNK, (chunk) => {
128+
answer.content += chunk;
129+
res.write(chunk);
130+
});
139131

140-
if (!context) {
141-
return cb(await this.aiQueryContextRepository.setIndexContext(
132+
socket.on(AiQueryWsEvents.GET_INDEX, async (index, cb) => {
133+
try {
134+
const indexContext = await this.aiQueryContextRepository.getIndexContext(
142135
sessionMetadata,
143136
databaseId,
144137
auth.accountId,
145138
index,
146-
await getIndexContext(client, index),
147-
));
139+
);
140+
141+
if (!indexContext) {
142+
return cb(await this.aiQueryContextRepository.setIndexContext(
143+
sessionMetadata,
144+
databaseId,
145+
auth.accountId,
146+
index,
147+
await getIndexContext(client, index),
148+
));
149+
}
150+
151+
return cb(indexContext);
152+
} catch (e) {
153+
this.logger.warn('Unable to create index content', e);
154+
return cb(e.message);
148155
}
149-
150-
return cb(indexContext);
151-
} catch (e) {
152-
this.logger.warn('Unable to create index content', e);
153-
return cb(e.message);
154-
}
155-
});
156-
157-
socket.on(AiQueryWsEvents.RUN_QUERY, async (data, cb) => {
158-
try {
159-
if (!COMMANDS_WHITELIST[(data?.[0] || '').toLowerCase()]) {
160-
return cb('-ERR: This command is not allowed');
156+
});
157+
158+
socket.on(AiQueryWsEvents.RUN_QUERY, async (data, cb) => {
159+
try {
160+
if (!COMMANDS_WHITELIST[(data?.[0] || '').toLowerCase()]) {
161+
return cb('-ERR: This command is not allowed');
162+
}
163+
164+
return cb(await client.sendCommand(data, { replyEncoding: 'utf8' }));
165+
} catch (e) {
166+
this.logger.warn('Query execution error', e);
167+
return cb(e.message);
161168
}
162-
163-
return cb(await client.sendCommand(data, { replyEncoding: 'utf8' }));
164-
} catch (e) {
165-
this.logger.warn('Query execution error', e);
166-
return cb(e.message);
167-
}
168-
});
169-
170-
socket.on(AiQueryWsEvents.TOOL_CALL, async (data) => {
171-
answer.steps.push(plainToClass(AiQueryIntermediateStep, {
172-
type: AiQueryIntermediateStepType.TOOL_CALL,
173-
data,
174-
}));
175-
});
176-
177-
socket.on(AiQueryWsEvents.TOOL_REPLY, async (data) => {
178-
answer.steps.push(plainToClass(AiQueryIntermediateStep, {
179-
type: AiQueryIntermediateStepType.TOOL,
180-
data,
181-
}));
182-
});
183-
184-
await socket.emitWithAck('stream', dto.content, context, AiQueryService.prepareHistory(history));
185-
socket.close();
186-
await this.aiQueryMessageRepository.createMany(sessionMetadata, [question, answer]);
187-
188-
return res.end();
189-
} catch (e) {
190-
socket?.close?.();
191-
throw wrapAiQueryError(e, 'Unable to send the question');
192-
}
169+
});
170+
171+
socket.on(AiQueryWsEvents.TOOL_CALL, async (data) => {
172+
answer.steps.push(plainToClass(AiQueryIntermediateStep, {
173+
type: AiQueryIntermediateStepType.TOOL_CALL,
174+
data,
175+
}));
176+
});
177+
178+
socket.on(AiQueryWsEvents.TOOL_REPLY, async (data) => {
179+
answer.steps.push(plainToClass(AiQueryIntermediateStep, {
180+
type: AiQueryIntermediateStepType.TOOL,
181+
data,
182+
}));
183+
});
184+
185+
await socket.emitWithAck('stream', dto.content, context, AiQueryService.prepareHistory(history));
186+
socket.close();
187+
await this.aiQueryMessageRepository.createMany(sessionMetadata, [question, answer]);
188+
189+
return res.end();
190+
} catch (e) {
191+
socket?.close?.();
192+
throw wrapAiQueryError(e, 'Unable to send the question');
193+
}
194+
});
193195
}
194196

195197
async getHistory(sessionMetadata: SessionMetadata, databaseId: string): Promise<AiQueryMessage[]> {
196-
try {
197-
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
198-
return await this.aiQueryMessageRepository.list(sessionMetadata, databaseId, auth.accountId);
199-
} catch (e) {
200-
throw wrapAiQueryError(e, 'Unable to get history');
201-
}
198+
return this.aiQueryAuthProvider.callWithAuthRetry(sessionMetadata, async () => {
199+
try {
200+
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
201+
return await this.aiQueryMessageRepository.list(sessionMetadata, databaseId, auth.accountId);
202+
} catch (e) {
203+
throw wrapAiQueryError(e, 'Unable to get history');
204+
}
205+
});
202206
}
203207

204208
async clearHistory(sessionMetadata: SessionMetadata, databaseId: string): Promise<void> {
205-
try {
206-
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
209+
return this.aiQueryAuthProvider.callWithAuthRetry(sessionMetadata, async () => {
210+
try {
211+
const auth = await this.aiQueryAuthProvider.getAuthData(sessionMetadata);
207212

208-
await this.aiQueryContextRepository.reset(sessionMetadata, databaseId, auth.accountId);
213+
await this.aiQueryContextRepository.reset(sessionMetadata, databaseId, auth.accountId);
209214

210-
return this.aiQueryMessageRepository.clearHistory(sessionMetadata, databaseId, auth.accountId);
211-
} catch (e) {
212-
throw wrapAiQueryError(e, 'Unable to clear history');
213-
}
215+
return this.aiQueryMessageRepository.clearHistory(sessionMetadata, databaseId, auth.accountId);
216+
} catch (e) {
217+
throw wrapAiQueryError(e, 'Unable to clear history');
218+
}
219+
});
214220
}
215221
}

redisinsight/api/src/modules/ai/query/exceptions/ai-query.bad-request.exception.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class AiQueryBadRequestException extends HttpException {
88
message,
99
statusCode: HttpStatus.BAD_REQUEST,
1010
error: 'AiQueryBadRequest',
11-
errorCode: CustomErrorCodes.QueryAiInternalServerError,
11+
errorCode: CustomErrorCodes.QueryAiBadRequest,
1212
};
1313

1414
super(response, response.statusCode, options);

redisinsight/api/src/modules/ai/query/exceptions/ai-query.error.handler.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AxiosError } from 'axios';
2+
import { get } from 'lodash';
23
import { HttpException } from '@nestjs/common';
34
import {
45
AiQueryUnauthorizedException,
@@ -13,11 +14,12 @@ export const wrapAiQueryError = (error: AxiosError, message?: string): HttpExcep
1314
return error;
1415
}
1516

16-
const { response } = error;
17+
// TransportError or Axios error
18+
const response = get(error, ['description', 'target', '_req', 'res'], error.response);
1719

1820
if (response) {
1921
const errorOptions = { cause: new Error(response?.data as string) };
20-
switch (response?.status) {
22+
switch (response?.status || response?.statusCode) {
2123
case 401:
2224
return new AiQueryUnauthorizedException(message, errorOptions);
2325
case 403:

0 commit comments

Comments
 (0)