Files
v3-api/src/ai-v2/ai-v2-admin.controller.ts

251 lines
7.8 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Patch,
Post,
Put,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiOperation,
ApiParam,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { AdminGuard } from 'src/auth/guards/admin.guard';
import { Permissions } from 'src/common/decorators/permission.decorator';
import { Permission } from 'src/common/types/permissions.catalog';
import { MAX_DICTIONARY_BYTES } from 'src/storage/storage.constants';
import { AiV2Client } from './ai-v2.client';
import { AiV2Exception } from './ai-v2.exception';
import { AiV2Service } from './ai-v2.service';
import { CursorPageQueryDto } from './dto/common-query.dto';
import { CreateDomainDto, ListDomainsQueryDto, UpdateDomainDto } from './dto/domains.dto';
import { ListFilesQueryDto } from './dto/files.dto';
import {
CountPointsQueryDto,
CreatePointDto,
GetPointQueryDto,
ListPointsQueryDto,
PatchPointPayloadDto,
ReplacePointDto,
SearchPointsQueryDto,
} from './dto/points.dto';
import { RetrievalQueryDto } from './dto/retrieval.dto';
@ApiTags('v2 AI')
@ApiBearerAuth()
@UseGuards(AdminGuard)
@Permissions(Permission.DictionariesRead)
@Controller('ai/v2')
export class AiV2AdminController {
constructor(
private readonly client: AiV2Client,
private readonly aiV2: AiV2Service,
) {}
@Get('domains')
@ApiOperation({ summary: 'List tenant domains (AI collections)' })
async listDomains(@Query() query: ListDomainsQueryDto) {
return this.aiV2.ok(await this.client.listDomains(query.include_disabled));
}
@Post('domains')
@Permissions(Permission.DictionariesCreate)
@ApiOperation({ summary: 'Create a tenant domain' })
@ApiBody({ type: CreateDomainDto })
async createDomain(@Body() body: CreateDomainDto) {
return this.aiV2.created(await this.client.createDomain(body));
}
@Patch('domains/:domain')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Update a tenant domain display name' })
@ApiParam({ name: 'domain' })
@ApiBody({ type: UpdateDomainDto })
async updateDomain(
@Param('domain') domain: string,
@Body() body: UpdateDomainDto,
) {
return this.aiV2.ok(await this.client.updateDomain(domain, body));
}
@Delete('domains/:domain')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Disable a tenant domain' })
@ApiParam({ name: 'domain' })
async disableDomain(@Param('domain') domain: string) {
return this.aiV2.ok(await this.client.disableDomain(domain));
}
@Post('domains/:domain/enable')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Enable a tenant domain' })
@ApiParam({ name: 'domain' })
async enableDomain(@Param('domain') domain: string) {
return this.aiV2.ok(await this.client.enableDomain(domain));
}
@Get('domains/:domain/points')
@ApiOperation({ summary: 'List points in a domain' })
@ApiParam({ name: 'domain' })
async listDomainPoints(
@Param('domain') domain: string,
@Query() query: CursorPageQueryDto,
) {
return this.aiV2.ok(await this.client.listDomainPoints(domain, query));
}
@Post('files')
@Permissions(Permission.DictionariesCreate)
@ApiOperation({ summary: 'Upload a file into a domain for ingestion' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file', 'domain'],
properties: {
file: { type: 'string', format: 'binary' },
domain: { type: 'string', example: 'faq' },
},
},
})
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: MAX_DICTIONARY_BYTES },
}),
)
async uploadFile(
@UploadedFile() file: Express.Multer.File,
@Body('domain') domain: string,
) {
if (!file) {
throw new AiV2Exception(HttpStatus.BAD_REQUEST, 'file_required');
}
if (!domain?.trim()) {
throw new AiV2Exception(HttpStatus.BAD_REQUEST, 'domain_required');
}
return this.aiV2.created(await this.client.uploadFile(file, domain.trim()));
}
@Get('files')
@ApiOperation({ summary: 'List uploaded files' })
async listFiles(@Query() query: ListFilesQueryDto) {
return this.aiV2.ok(await this.client.listFiles(query));
}
@Get('files/:fileId')
@ApiOperation({ summary: 'Get file status / ingestion progress' })
@ApiParam({ name: 'fileId' })
async getFile(@Param('fileId') fileId: string) {
return this.aiV2.ok(await this.client.getFile(fileId));
}
@Delete('files/:fileId')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Delete a file (soft-deletes its points)' })
@ApiParam({ name: 'fileId' })
async deleteFile(@Param('fileId') fileId: string) {
return this.aiV2.ok(await this.client.deleteFile(fileId));
}
@Get('files/:fileId/points')
@ApiOperation({ summary: 'List points for a file' })
@ApiParam({ name: 'fileId' })
async listFilePoints(
@Param('fileId') fileId: string,
@Query() query: CursorPageQueryDto,
) {
return this.aiV2.ok(await this.client.listFilePoints(fileId, query));
}
@Get('points/count')
@ApiOperation({ summary: 'Count points, optionally filtered by domain and/or file' })
async countPoints(@Query() query: CountPointsQueryDto) {
return this.aiV2.ok(await this.client.countPoints(query));
}
@Get('points/search')
@ApiOperation({ summary: 'Keyword search points (not semantic retrieval)' })
async searchPoints(@Query() query: SearchPointsQueryDto) {
return this.aiV2.ok(await this.client.searchPoints(query));
}
@Get('points')
@ApiOperation({ summary: 'List points for a file_id' })
@ApiQuery({ name: 'file_id', required: true })
async listPoints(@Query() query: ListPointsQueryDto) {
return this.aiV2.ok(await this.client.listPoints(query));
}
@Post('points')
@Permissions(Permission.DictionariesCreate)
@ApiOperation({ summary: 'Create a point in a file' })
@ApiBody({ type: CreatePointDto })
async createPoint(@Body() body: CreatePointDto) {
return this.aiV2.created(await this.client.createPoint(body));
}
@Get('points/:pointId')
@ApiOperation({ summary: 'Get one point' })
@ApiParam({ name: 'pointId' })
async getPoint(
@Param('pointId') pointId: string,
@Query() query: GetPointQueryDto,
) {
return this.aiV2.ok(await this.client.getPoint(pointId, query.with_vectors));
}
@Delete('points/:pointId')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Delete a point' })
@ApiParam({ name: 'pointId' })
async deletePoint(@Param('pointId') pointId: string) {
return this.aiV2.ok(await this.client.deletePoint(pointId));
}
@Put('points/:pointId')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Replace point content (version-guarded)' })
@ApiParam({ name: 'pointId' })
@ApiBody({ type: ReplacePointDto })
async replacePoint(
@Param('pointId') pointId: string,
@Body() body: ReplacePointDto,
) {
return this.aiV2.ok(await this.client.replacePoint(pointId, body));
}
@Patch('points/:pointId/payload')
@Permissions(Permission.DictionariesWrite)
@ApiOperation({ summary: 'Patch point payload (version-guarded)' })
@ApiParam({ name: 'pointId' })
@ApiBody({ type: PatchPointPayloadDto })
async patchPointPayload(
@Param('pointId') pointId: string,
@Body() body: PatchPointPayloadDto,
) {
return this.aiV2.ok(await this.client.patchPointPayload(pointId, body));
}
@Post('retrieval/query')
@ApiOperation({ summary: 'Hybrid semantic retrieval (not the ask API)' })
@ApiBody({ type: RetrievalQueryDto })
async queryRetrieval(@Body() body: RetrievalQueryDto) {
return this.aiV2.ok(await this.client.queryRetrieval(body));
}
}