Skip to content

Commit 0b44d0b

Browse files
committed
feat(agents-microservice): migrate AI request endpoints to agents microservice
- Marked existing AI request endpoints in UserAIRequestsControllerV2 as deprecated, indicating they have moved to the agents microservice. - Introduced new AgentsController to handle AI-related requests, including user token validation, table structure retrieval, and execution of AI queries. - Implemented use cases for validating user tokens, table AI requests, connection edits, and executing raw queries and aggregation pipelines. - Added data structures and DTOs for handling requests and responses in the agents microservice. - Established utility functions for setting up AI connections and asserting user permissions on tables. - Integrated Sentry for error tracking and added streaming capabilities for settings creation.
1 parent 9cb5a1c commit 0b44d0b

20 files changed

Lines changed: 1082 additions & 3 deletions

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { DashboardModule } from './entities/visualizations/dashboard/dashboards.
4343
import { PanelModule } from './entities/visualizations/panel/panel.module.js';
4444
import { PanelPositionModule } from './entities/visualizations/panel-position/panel-position.module.js';
4545
import { TableWidgetModule } from './entities/widget/table-widget.module.js';
46+
import { AgentsModule } from './microservices/agents-microservice/agents.module.js';
4647
import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js';
4748
import { SaasModule } from './microservices/saas-microservice/saas.module.js';
4849
import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js';
@@ -84,6 +85,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
8485
SharedModule,
8586
TableActionModule,
8687
SaasModule,
88+
AgentsModule,
8789
CompanyInfoModule,
8890
SaaSGatewayModule,
8991
TableTriggersModule,

backend/src/common/data-injection.tokens.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,15 @@ export enum UseCaseType {
177177
FIND_USER_AI_CHAT_BY_ID = 'FIND_USER_AI_CHAT_BY_ID',
178178
DELETE_USER_AI_CHAT = 'DELETE_USER_AI_CHAT',
179179

180+
AGENTS_VALIDATE_USER_TOKEN = 'AGENTS_VALIDATE_USER_TOKEN',
181+
AGENTS_VALIDATE_TABLE_AI_REQUEST = 'AGENTS_VALIDATE_TABLE_AI_REQUEST',
182+
AGENTS_VALIDATE_CONNECTION_EDIT = 'AGENTS_VALIDATE_CONNECTION_EDIT',
183+
AGENTS_GET_AI_CONNECTION_CONTEXT = 'AGENTS_GET_AI_CONNECTION_CONTEXT',
184+
AGENTS_GET_AI_TABLE_STRUCTURE = 'AGENTS_GET_AI_TABLE_STRUCTURE',
185+
AGENTS_EXECUTE_AI_RAW_QUERY = 'AGENTS_EXECUTE_AI_RAW_QUERY',
186+
AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE = 'AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE',
187+
AGENTS_SCAN_AND_CREATE_SETTINGS = 'AGENTS_SCAN_AND_CREATE_SETTINGS',
188+
180189
CREATE_TABLE_FILTERS = 'CREATE_TABLE_FILTERS',
181190
FIND_TABLE_FILTERS = 'FIND_TABLE_FILTERS',
182191
DELETE_TABLE_FILTERS = 'DELETE_TABLE_FILTERS',

backend/src/entities/ai/ai-conversation-history/user-ai-chat.controller.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ export class UserAiChatController {
3535
private readonly deleteUserAiChatUseCase: IDeleteUserAiChat,
3636
) {}
3737

38-
@ApiOperation({ summary: 'Get all AI chats for current user' })
38+
@ApiOperation({
39+
summary: 'Get all AI chats for current user',
40+
deprecated: true,
41+
description:
42+
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
43+
})
3944
@ApiResponse({
4045
status: 200,
4146
description: 'Returns list of AI chats.',
@@ -47,7 +52,12 @@ export class UserAiChatController {
4752
return await this.findUserAiChatsUseCase.execute(inputData, InTransactionEnum.OFF);
4853
}
4954

50-
@ApiOperation({ summary: 'Get AI chat by ID with all messages' })
55+
@ApiOperation({
56+
summary: 'Get AI chat by ID with all messages',
57+
deprecated: true,
58+
description:
59+
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
60+
})
5161
@ApiResponse({
5262
status: 200,
5363
description: 'Returns AI chat with messages.',
@@ -60,7 +70,12 @@ export class UserAiChatController {
6070
return await this.findUserAiChatByIdUseCase.execute(inputData, InTransactionEnum.OFF);
6171
}
6272

63-
@ApiOperation({ summary: 'Delete AI chat by ID' })
73+
@ApiOperation({
74+
summary: 'Delete AI chat by ID',
75+
deprecated: true,
76+
description:
77+
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
78+
})
6479
@ApiResponse({
6580
status: 200,
6681
description: 'AI chat deleted successfully.',

backend/src/entities/ai/user-ai-requests-v2.controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export class UserAIRequestsControllerV2 {
4444

4545
@ApiOperation({
4646
summary: 'Request info from table in connection with AI with conversation history (Version 4)',
47+
deprecated: true,
48+
description:
49+
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
4750
})
4851
@ApiResponse({
4952
status: 201,
@@ -86,6 +89,9 @@ export class UserAIRequestsControllerV2 {
8689

8790
@ApiOperation({
8891
summary: 'Request AI settings and widgets creation for connection',
92+
deprecated: true,
93+
description:
94+
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
8995
})
9096
@ApiResponse({
9197
status: 200,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { Body, Controller, Inject, Injectable, Post, Res, UseInterceptors } from '@nestjs/common';
2+
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
3+
import { SkipThrottle } from '@nestjs/throttler';
4+
import { Response } from 'express';
5+
import { UseCaseType } from '../../common/data-injection.tokens.js';
6+
import { SlugUuid } from '../../decorators/slug-uuid.decorator.js';
7+
import { Timeout, TimeoutDefaults } from '../../decorators/timeout.decorator.js';
8+
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
9+
import { isTest } from '../../helpers/app/is-test.js';
10+
import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
11+
import {
12+
AiConnectionContextRO,
13+
AiQueryResultRO,
14+
PermissionAllowedRO,
15+
ValidatedUserTokenRO,
16+
} from './data-structures/agents-responses.ds.js';
17+
import {
18+
AiDataRequestBaseDto,
19+
ExecuteAiAggregationPipelineDto,
20+
ExecuteAiRawQueryDto,
21+
GetAiTableStructureDto,
22+
} from './dto/agents-ai-data.dtos.js';
23+
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
24+
import {
25+
IExecuteAiAggregationPipeline,
26+
IExecuteAiRawQuery,
27+
IGetAiConnectionContext,
28+
IGetAiTableStructure,
29+
IScanAndCreateSettings,
30+
IValidateConnectionEdit,
31+
IValidateTableAiRequest,
32+
IValidateUserToken,
33+
} from './use-cases/agents-use-cases.interface.js';
34+
35+
@UseInterceptors(SentryInterceptor)
36+
@SkipThrottle()
37+
@Timeout()
38+
@ApiTags('agents microservice')
39+
@Controller('internal/agents')
40+
@Injectable()
41+
export class AgentsController {
42+
constructor(
43+
@Inject(UseCaseType.AGENTS_VALIDATE_USER_TOKEN)
44+
private readonly validateUserTokenUseCase: IValidateUserToken,
45+
@Inject(UseCaseType.AGENTS_VALIDATE_TABLE_AI_REQUEST)
46+
private readonly validateTableAiRequestUseCase: IValidateTableAiRequest,
47+
@Inject(UseCaseType.AGENTS_VALIDATE_CONNECTION_EDIT)
48+
private readonly validateConnectionEditUseCase: IValidateConnectionEdit,
49+
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT)
50+
private readonly getAiConnectionContextUseCase: IGetAiConnectionContext,
51+
@Inject(UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE)
52+
private readonly getAiTableStructureUseCase: IGetAiTableStructure,
53+
@Inject(UseCaseType.AGENTS_EXECUTE_AI_RAW_QUERY)
54+
private readonly executeAiRawQueryUseCase: IExecuteAiRawQuery,
55+
@Inject(UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE)
56+
private readonly executeAiAggregationPipelineUseCase: IExecuteAiAggregationPipeline,
57+
@Inject(UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS)
58+
private readonly scanAndCreateSettingsUseCase: IScanAndCreateSettings,
59+
) {}
60+
61+
@ApiOperation({ summary: 'Validate an end-user JWT on behalf of the agents microservice' })
62+
@ApiResponse({ status: 201, type: ValidatedUserTokenRO })
63+
@ApiBody({ type: ValidateUserTokenDto })
64+
@Post('/auth/validate-user-token')
65+
public async validateUserToken(@Body() body: ValidateUserTokenDto): Promise<ValidatedUserTokenRO> {
66+
return await this.validateUserTokenUseCase.execute(body.token, InTransactionEnum.OFF);
67+
}
68+
69+
@ApiOperation({ summary: 'Check Cedar permission for an AI request on a table' })
70+
@ApiResponse({ status: 201, type: PermissionAllowedRO })
71+
@ApiBody({ type: ValidateTableAiRequestDto })
72+
@Post('/auth/validate-table-ai-request')
73+
public async validateTableAiRequest(@Body() body: ValidateTableAiRequestDto): Promise<PermissionAllowedRO> {
74+
return await this.validateTableAiRequestUseCase.execute(
75+
{ userId: body.userId, connectionId: body.connectionId, tableName: body.tableName },
76+
InTransactionEnum.OFF,
77+
);
78+
}
79+
80+
@ApiOperation({ summary: 'Check Cedar permission for editing a connection' })
81+
@ApiResponse({ status: 201, type: PermissionAllowedRO })
82+
@ApiBody({ type: ValidateConnectionEditDto })
83+
@Post('/auth/validate-connection-edit')
84+
public async validateConnectionEdit(@Body() body: ValidateConnectionEditDto): Promise<PermissionAllowedRO> {
85+
return await this.validateConnectionEditUseCase.execute(
86+
{ userId: body.userId, connectionId: body.connectionId },
87+
InTransactionEnum.OFF,
88+
);
89+
}
90+
91+
@ApiOperation({ summary: 'Get AI-relevant connection context (type, schema, MongoDB flag)' })
92+
@ApiResponse({ status: 201, type: AiConnectionContextRO })
93+
@ApiBody({ type: AiDataRequestBaseDto })
94+
@Post('/ai/data/:connectionId/context')
95+
public async getAiConnectionContext(
96+
@SlugUuid('connectionId') connectionId: string,
97+
@Body() body: AiDataRequestBaseDto,
98+
): Promise<AiConnectionContextRO> {
99+
return await this.getAiConnectionContextUseCase.execute(
100+
{ connectionId, userId: body.userId, masterPassword: body.masterPassword ?? null },
101+
InTransactionEnum.OFF,
102+
);
103+
}
104+
105+
@ApiOperation({ summary: 'Get permission-aware table structure for the AI tool loop' })
106+
@ApiResponse({ status: 201, description: 'Table structure with related tables.' })
107+
@ApiBody({ type: GetAiTableStructureDto })
108+
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
109+
@Post('/ai/data/:connectionId/table-structure')
110+
public async getAiTableStructure(
111+
@SlugUuid('connectionId') connectionId: string,
112+
@Body() body: GetAiTableStructureDto,
113+
): Promise<Record<string, unknown>> {
114+
return await this.getAiTableStructureUseCase.execute(
115+
{
116+
connectionId,
117+
userId: body.userId,
118+
masterPassword: body.masterPassword ?? null,
119+
tableName: body.tableName,
120+
},
121+
InTransactionEnum.OFF,
122+
);
123+
}
124+
125+
@ApiOperation({ summary: 'Validate and execute a read-only SQL query for the AI tool loop' })
126+
@ApiResponse({ status: 201, type: AiQueryResultRO })
127+
@ApiBody({ type: ExecuteAiRawQueryDto })
128+
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
129+
@Post('/ai/data/:connectionId/raw-query')
130+
public async executeAiRawQuery(
131+
@SlugUuid('connectionId') connectionId: string,
132+
@Body() body: ExecuteAiRawQueryDto,
133+
): Promise<AiQueryResultRO> {
134+
return await this.executeAiRawQueryUseCase.execute(
135+
{
136+
connectionId,
137+
userId: body.userId,
138+
masterPassword: body.masterPassword ?? null,
139+
tableName: body.tableName,
140+
query: body.query,
141+
},
142+
InTransactionEnum.OFF,
143+
);
144+
}
145+
146+
@ApiOperation({ summary: 'Validate and execute a read-only MongoDB aggregation pipeline for the AI tool loop' })
147+
@ApiResponse({ status: 201, type: AiQueryResultRO })
148+
@ApiBody({ type: ExecuteAiAggregationPipelineDto })
149+
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
150+
@Post('/ai/data/:connectionId/aggregation-pipeline')
151+
public async executeAiAggregationPipeline(
152+
@SlugUuid('connectionId') connectionId: string,
153+
@Body() body: ExecuteAiAggregationPipelineDto,
154+
): Promise<AiQueryResultRO> {
155+
return await this.executeAiAggregationPipelineUseCase.execute(
156+
{
157+
connectionId,
158+
userId: body.userId,
159+
masterPassword: body.masterPassword ?? null,
160+
tableName: body.tableName,
161+
pipeline: body.pipeline,
162+
},
163+
InTransactionEnum.OFF,
164+
);
165+
}
166+
167+
@ApiOperation({ summary: 'Run the AI settings/widgets scan, streaming progress chunks' })
168+
@ApiResponse({ status: 201, description: 'Streams progress as newline-delimited JSON chunks.' })
169+
@ApiBody({ type: AiDataRequestBaseDto })
170+
@Timeout(!isTest() ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST)
171+
@Post('/ai/data/:connectionId/settings-scan')
172+
public async scanAndCreateSettings(
173+
@SlugUuid('connectionId') connectionId: string,
174+
@Body() body: AiDataRequestBaseDto,
175+
@Res({ passthrough: true }) response: Response,
176+
): Promise<void> {
177+
return await this.scanAndCreateSettingsUseCase.execute(
178+
{
179+
connectionId,
180+
userId: body.userId,
181+
masterPassword: body.masterPassword ?? null,
182+
response,
183+
},
184+
InTransactionEnum.OFF,
185+
);
186+
}
187+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { SaaSAuthMiddleware } from '../../authorization/saas-auth.middleware.js';
4+
import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
5+
import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js';
6+
import { AgentsController } from './agents.controller.js';
7+
import { ExecuteAiAggregationPipelineUseCase } from './use-cases/execute-ai-aggregation-pipeline.use.case.js';
8+
import { ExecuteAiRawQueryUseCase } from './use-cases/execute-ai-raw-query.use.case.js';
9+
import { GetAiConnectionContextUseCase } from './use-cases/get-ai-connection-context.use.case.js';
10+
import { GetAiTableStructureUseCase } from './use-cases/get-ai-table-structure.use.case.js';
11+
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
12+
import { ValidateConnectionEditUseCase } from './use-cases/validate-connection-edit.use.case.js';
13+
import { ValidateTableAiRequestUseCase } from './use-cases/validate-table-ai-request.use.case.js';
14+
import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.case.js';
15+
16+
@Module({
17+
imports: [TypeOrmModule.forFeature([])],
18+
providers: [
19+
{
20+
provide: BaseType.GLOBAL_DB_CONTEXT,
21+
useClass: GlobalDatabaseContext,
22+
},
23+
{
24+
provide: UseCaseType.AGENTS_VALIDATE_USER_TOKEN,
25+
useClass: ValidateUserTokenUseCase,
26+
},
27+
{
28+
provide: UseCaseType.AGENTS_VALIDATE_TABLE_AI_REQUEST,
29+
useClass: ValidateTableAiRequestUseCase,
30+
},
31+
{
32+
provide: UseCaseType.AGENTS_VALIDATE_CONNECTION_EDIT,
33+
useClass: ValidateConnectionEditUseCase,
34+
},
35+
{
36+
provide: UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT,
37+
useClass: GetAiConnectionContextUseCase,
38+
},
39+
{
40+
provide: UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE,
41+
useClass: GetAiTableStructureUseCase,
42+
},
43+
{
44+
provide: UseCaseType.AGENTS_EXECUTE_AI_RAW_QUERY,
45+
useClass: ExecuteAiRawQueryUseCase,
46+
},
47+
{
48+
provide: UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE,
49+
useClass: ExecuteAiAggregationPipelineUseCase,
50+
},
51+
{
52+
provide: UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS,
53+
useClass: ScanAndCreateSettingsUseCase,
54+
},
55+
],
56+
controllers: [AgentsController],
57+
})
58+
export class AgentsModule {
59+
public configure(consumer: MiddlewareConsumer): void {
60+
consumer.apply(SaaSAuthMiddleware).forRoutes(AgentsController);
61+
}
62+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
3+
export class ValidatedUserTokenRO {
4+
@ApiProperty()
5+
sub: string;
6+
7+
@ApiPropertyOptional()
8+
email: string | null;
9+
10+
@ApiPropertyOptional()
11+
exp: number | null;
12+
13+
@ApiPropertyOptional()
14+
iat: number | null;
15+
}
16+
17+
export class PermissionAllowedRO {
18+
@ApiProperty()
19+
allowed: boolean;
20+
}
21+
22+
export class AiConnectionContextRO {
23+
@ApiProperty()
24+
connectionId: string;
25+
26+
@ApiProperty()
27+
type: string;
28+
29+
@ApiPropertyOptional()
30+
schema: string | null;
31+
32+
@ApiProperty()
33+
isMongoDb: boolean;
34+
35+
@ApiProperty()
36+
userEmail: string;
37+
}
38+
39+
export class AiQueryResultRO {
40+
@ApiProperty()
41+
result: unknown;
42+
}

0 commit comments

Comments
 (0)