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
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { DashboardModule } from './entities/visualizations/dashboard/dashboards.
import { PanelModule } from './entities/visualizations/panel/panel.module.js';
import { PanelPositionModule } from './entities/visualizations/panel-position/panel-position.module.js';
import { TableWidgetModule } from './entities/widget/table-widget.module.js';
import { AgentsModule } from './microservices/agents-microservice/agents.module.js';
import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js';
import { SaasModule } from './microservices/saas-microservice/saas.module.js';
import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js';
Expand Down Expand Up @@ -84,6 +85,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
SharedModule,
TableActionModule,
SaasModule,
AgentsModule,
CompanyInfoModule,
SaaSGatewayModule,
TableTriggersModule,
Expand Down
9 changes: 9 additions & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ export enum UseCaseType {
FIND_USER_AI_CHAT_BY_ID = 'FIND_USER_AI_CHAT_BY_ID',
DELETE_USER_AI_CHAT = 'DELETE_USER_AI_CHAT',

AGENTS_VALIDATE_USER_TOKEN = 'AGENTS_VALIDATE_USER_TOKEN',
AGENTS_VALIDATE_TABLE_AI_REQUEST = 'AGENTS_VALIDATE_TABLE_AI_REQUEST',
AGENTS_VALIDATE_CONNECTION_EDIT = 'AGENTS_VALIDATE_CONNECTION_EDIT',
AGENTS_GET_AI_CONNECTION_CONTEXT = 'AGENTS_GET_AI_CONNECTION_CONTEXT',
AGENTS_GET_AI_TABLE_STRUCTURE = 'AGENTS_GET_AI_TABLE_STRUCTURE',
AGENTS_EXECUTE_AI_RAW_QUERY = 'AGENTS_EXECUTE_AI_RAW_QUERY',
AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE = 'AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE',
AGENTS_SCAN_AND_CREATE_SETTINGS = 'AGENTS_SCAN_AND_CREATE_SETTINGS',

CREATE_TABLE_FILTERS = 'CREATE_TABLE_FILTERS',
FIND_TABLE_FILTERS = 'FIND_TABLE_FILTERS',
DELETE_TABLE_FILTERS = 'DELETE_TABLE_FILTERS',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export class UserAiChatController {
private readonly deleteUserAiChatUseCase: IDeleteUserAiChat,
) {}

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

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

@ApiOperation({ summary: 'Delete AI chat by ID' })
@ApiOperation({
summary: 'Delete AI chat by ID',
deprecated: true,
description:
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
})
@ApiResponse({
status: 200,
description: 'AI chat deleted successfully.',
Expand Down
6 changes: 6 additions & 0 deletions backend/src/entities/ai/user-ai-requests-v2.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export class UserAIRequestsControllerV2 {

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

@ApiOperation({
summary: 'Request AI settings and widgets creation for connection',
deprecated: true,
description:
'Deprecated: this endpoint moved to the agents microservice (same path and contract). It remains here only until traffic is switched over.',
})
@ApiResponse({
status: 200,
Expand Down
187 changes: 187 additions & 0 deletions backend/src/microservices/agents-microservice/agents.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Body, Controller, Inject, Injectable, Post, Res, UseInterceptors } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { SkipThrottle } from '@nestjs/throttler';
import { Response } from 'express';
import { UseCaseType } from '../../common/data-injection.tokens.js';
import { SlugUuid } from '../../decorators/slug-uuid.decorator.js';
import { Timeout, TimeoutDefaults } from '../../decorators/timeout.decorator.js';
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
import { isTest } from '../../helpers/app/is-test.js';
import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
import {
AiConnectionContextRO,
AiQueryResultRO,
PermissionAllowedRO,
ValidatedUserTokenRO,
} from './data-structures/agents-responses.ds.js';
import {
AiDataRequestBaseDto,
ExecuteAiAggregationPipelineDto,
ExecuteAiRawQueryDto,
GetAiTableStructureDto,
} from './dto/agents-ai-data.dtos.js';
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
import {
IExecuteAiAggregationPipeline,
IExecuteAiRawQuery,
IGetAiConnectionContext,
IGetAiTableStructure,
IScanAndCreateSettings,
IValidateConnectionEdit,
IValidateTableAiRequest,
IValidateUserToken,
} from './use-cases/agents-use-cases.interface.js';

@UseInterceptors(SentryInterceptor)
@SkipThrottle()
@Timeout()
@ApiTags('agents microservice')
@Controller('internal/agents')
@Injectable()
export class AgentsController {
constructor(
@Inject(UseCaseType.AGENTS_VALIDATE_USER_TOKEN)
private readonly validateUserTokenUseCase: IValidateUserToken,
@Inject(UseCaseType.AGENTS_VALIDATE_TABLE_AI_REQUEST)
private readonly validateTableAiRequestUseCase: IValidateTableAiRequest,
@Inject(UseCaseType.AGENTS_VALIDATE_CONNECTION_EDIT)
private readonly validateConnectionEditUseCase: IValidateConnectionEdit,
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT)
private readonly getAiConnectionContextUseCase: IGetAiConnectionContext,
@Inject(UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE)
private readonly getAiTableStructureUseCase: IGetAiTableStructure,
@Inject(UseCaseType.AGENTS_EXECUTE_AI_RAW_QUERY)
private readonly executeAiRawQueryUseCase: IExecuteAiRawQuery,
@Inject(UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE)
private readonly executeAiAggregationPipelineUseCase: IExecuteAiAggregationPipeline,
@Inject(UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS)
private readonly scanAndCreateSettingsUseCase: IScanAndCreateSettings,
) {}

@ApiOperation({ summary: 'Validate an end-user JWT on behalf of the agents microservice' })
@ApiResponse({ status: 201, type: ValidatedUserTokenRO })
@ApiBody({ type: ValidateUserTokenDto })
@Post('/auth/validate-user-token')
public async validateUserToken(@Body() body: ValidateUserTokenDto): Promise<ValidatedUserTokenRO> {
return await this.validateUserTokenUseCase.execute(body.token, InTransactionEnum.OFF);
}

@ApiOperation({ summary: 'Check Cedar permission for an AI request on a table' })
@ApiResponse({ status: 201, type: PermissionAllowedRO })
@ApiBody({ type: ValidateTableAiRequestDto })
@Post('/auth/validate-table-ai-request')
public async validateTableAiRequest(@Body() body: ValidateTableAiRequestDto): Promise<PermissionAllowedRO> {
return await this.validateTableAiRequestUseCase.execute(
{ userId: body.userId, connectionId: body.connectionId, tableName: body.tableName },
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Check Cedar permission for editing a connection' })
@ApiResponse({ status: 201, type: PermissionAllowedRO })
@ApiBody({ type: ValidateConnectionEditDto })
@Post('/auth/validate-connection-edit')
public async validateConnectionEdit(@Body() body: ValidateConnectionEditDto): Promise<PermissionAllowedRO> {
return await this.validateConnectionEditUseCase.execute(
{ userId: body.userId, connectionId: body.connectionId },
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Get AI-relevant connection context (type, schema, MongoDB flag)' })
@ApiResponse({ status: 201, type: AiConnectionContextRO })
@ApiBody({ type: AiDataRequestBaseDto })
@Post('/ai/data/:connectionId/context')
public async getAiConnectionContext(
@SlugUuid('connectionId') connectionId: string,
@Body() body: AiDataRequestBaseDto,
): Promise<AiConnectionContextRO> {
return await this.getAiConnectionContextUseCase.execute(
{ connectionId, userId: body.userId, masterPassword: body.masterPassword ?? null },
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Get permission-aware table structure for the AI tool loop' })
@ApiResponse({ status: 201, description: 'Table structure with related tables.' })
@ApiBody({ type: GetAiTableStructureDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/table-structure')
public async getAiTableStructure(
@SlugUuid('connectionId') connectionId: string,
@Body() body: GetAiTableStructureDto,
): Promise<Record<string, unknown>> {
return await this.getAiTableStructureUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Validate and execute a read-only SQL query for the AI tool loop' })
@ApiResponse({ status: 201, type: AiQueryResultRO })
@ApiBody({ type: ExecuteAiRawQueryDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/raw-query')
public async executeAiRawQuery(
@SlugUuid('connectionId') connectionId: string,
@Body() body: ExecuteAiRawQueryDto,
): Promise<AiQueryResultRO> {
return await this.executeAiRawQueryUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
query: body.query,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Validate and execute a read-only MongoDB aggregation pipeline for the AI tool loop' })
@ApiResponse({ status: 201, type: AiQueryResultRO })
@ApiBody({ type: ExecuteAiAggregationPipelineDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/aggregation-pipeline')
public async executeAiAggregationPipeline(
@SlugUuid('connectionId') connectionId: string,
@Body() body: ExecuteAiAggregationPipelineDto,
): Promise<AiQueryResultRO> {
return await this.executeAiAggregationPipelineUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
pipeline: body.pipeline,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Run the AI settings/widgets scan, streaming progress chunks' })
@ApiResponse({ status: 201, description: 'Streams progress as newline-delimited JSON chunks.' })
@ApiBody({ type: AiDataRequestBaseDto })
@Timeout(!isTest() ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST)
@Post('/ai/data/:connectionId/settings-scan')
public async scanAndCreateSettings(
@SlugUuid('connectionId') connectionId: string,
@Body() body: AiDataRequestBaseDto,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
return await this.scanAndCreateSettingsUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
response,
},
InTransactionEnum.OFF,
);
}
}
62 changes: 62 additions & 0 deletions backend/src/microservices/agents-microservice/agents.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SaaSAuthMiddleware } from '../../authorization/saas-auth.middleware.js';
import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js';
import { AgentsController } from './agents.controller.js';
import { ExecuteAiAggregationPipelineUseCase } from './use-cases/execute-ai-aggregation-pipeline.use.case.js';
import { ExecuteAiRawQueryUseCase } from './use-cases/execute-ai-raw-query.use.case.js';
import { GetAiConnectionContextUseCase } from './use-cases/get-ai-connection-context.use.case.js';
import { GetAiTableStructureUseCase } from './use-cases/get-ai-table-structure.use.case.js';
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
import { ValidateConnectionEditUseCase } from './use-cases/validate-connection-edit.use.case.js';
import { ValidateTableAiRequestUseCase } from './use-cases/validate-table-ai-request.use.case.js';
import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.case.js';

@Module({
imports: [TypeOrmModule.forFeature([])],
providers: [
{
provide: BaseType.GLOBAL_DB_CONTEXT,
useClass: GlobalDatabaseContext,
},
{
provide: UseCaseType.AGENTS_VALIDATE_USER_TOKEN,
useClass: ValidateUserTokenUseCase,
},
{
provide: UseCaseType.AGENTS_VALIDATE_TABLE_AI_REQUEST,
useClass: ValidateTableAiRequestUseCase,
},
{
provide: UseCaseType.AGENTS_VALIDATE_CONNECTION_EDIT,
useClass: ValidateConnectionEditUseCase,
},
{
provide: UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT,
useClass: GetAiConnectionContextUseCase,
},
{
provide: UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE,
useClass: GetAiTableStructureUseCase,
},
{
provide: UseCaseType.AGENTS_EXECUTE_AI_RAW_QUERY,
useClass: ExecuteAiRawQueryUseCase,
},
{
provide: UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE,
useClass: ExecuteAiAggregationPipelineUseCase,
},
{
provide: UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS,
useClass: ScanAndCreateSettingsUseCase,
},
],
controllers: [AgentsController],
})
export class AgentsModule {
public configure(consumer: MiddlewareConsumer): void {
consumer.apply(SaaSAuthMiddleware).forRoutes(AgentsController);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class ValidatedUserTokenRO {
@ApiProperty()
sub: string;

@ApiPropertyOptional()
email: string | null;

@ApiPropertyOptional()
exp: number | null;

@ApiPropertyOptional()
iat: number | null;
}

export class PermissionAllowedRO {
@ApiProperty()
allowed: boolean;
}

export class AiConnectionContextRO {
@ApiProperty()
connectionId: string;

@ApiProperty()
type: string;

@ApiPropertyOptional()
schema: string | null;

@ApiProperty()
isMongoDb: boolean;

@ApiProperty()
userEmail: string;
}

export class AiQueryResultRO {
@ApiProperty()
result: unknown;
}
Loading
Loading