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
32 changes: 28 additions & 4 deletions src/api/ai-workflow/ai-workflow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CreateAiWorkflowDto,
CreateAiWorkflowRunDto,
UpdateAiWorkflowDto,
CreateAiWorkflowRunItemsDto,
UpdateAiWorkflowRunDto,
} from '../../dto/aiWorkflow.dto';
import { Scopes } from 'src/shared/decorators/scopes.decorator';
Expand Down Expand Up @@ -50,6 +51,29 @@ export class AiWorkflowController {
return this.aiWorkflowService.createWithValidation(createAiWorkflowDto);
}

@Post('/:workflowId/runs/:runId/items')
@Scopes(Scope.CreateWorkflowRun)
@ApiOperation({ summary: 'Create AIWorkflowRunItems in batch' })
@ApiResponse({
status: 201,
description: 'AIWorkflowRunItems created successfully.',
})
@ApiResponse({ status: 400, description: 'Bad Request.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Workflow or Run not found.' })
async createRunItems(
@Param('workflowId') workflowId: string,
@Param('runId') runId: string,
@Body(new ValidationPipe({ whitelist: true, transform: true }))
createItemsDto: CreateAiWorkflowRunItemsDto,
) {
return this.aiWorkflowService.createRunItemsBatch(
workflowId,
runId,
createItemsDto.items,
);
}

@Get(':id')
@Roles(UserRole.Admin, UserRole.User, UserRole.Copilot, UserRole.Reviewer)
@Scopes(Scope.ReadWorkflow)
Expand Down Expand Up @@ -85,7 +109,7 @@ export class AiWorkflowController {

@Post('/:workflowId/runs')
@Roles(UserRole.Admin)
@Scopes(Scope.CreateWorkflowRuns)
@Scopes(Scope.CreateWorkflowRun)
@ApiOperation({ summary: 'Create a new run for an AI workflow' })
@ApiResponse({
status: 201,
Expand All @@ -108,7 +132,7 @@ export class AiWorkflowController {
UserRole.Reviewer,
UserRole.Submitter,
)
@Scopes(Scope.ReadWorkflowRuns)
@Scopes(Scope.ReadWorkflowRun)
@ApiOperation({
summary: 'Get all the AI workflow runs for a given submission ID',
})
Expand Down Expand Up @@ -140,7 +164,7 @@ export class AiWorkflowController {
UserRole.Reviewer,
UserRole.Submitter,
)
@Scopes(Scope.ReadWorkflowRuns)
@Scopes(Scope.ReadWorkflowRun)
@ApiOperation({
summary: 'Get an AI workflow run by its ID',
})
Expand Down Expand Up @@ -171,7 +195,7 @@ export class AiWorkflowController {
}

@Patch('/:workflowId/runs/:runId')
@Scopes(Scope.UpdateWorkflowRuns)
@Scopes(Scope.UpdateWorkflowRun)
@ApiOperation({ summary: 'Update a run for an AI workflow' })
@ApiResponse({
status: 200,
Expand Down
63 changes: 63 additions & 0 deletions src/api/ai-workflow/ai-workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,68 @@ export class AiWorkflowService {
});
}

async createRunItemsBatch(workflowId: string, runId: string, items: any[]) {
const workflow = await this.prisma.aiWorkflow.findUnique({
where: { id: workflowId },
});
if (!workflow) {
this.logger.error(`Workflow with id ${workflowId} not found.`);
throw new NotFoundException(`Workflow with id ${workflowId} not found.`);
}

const run = await this.prisma.aiWorkflowRun.findUnique({
where: { id: runId },
include: { workflow: true },
});
if (!run || run.workflowId !== workflowId) {
this.logger.error(
`Run with id ${runId} not found or does not belong to workflow ${workflowId}.`,
);
throw new NotFoundException(
`Run with id ${runId} not found or does not belong to workflow ${workflowId}.`,
);
}

for (const item of items) {
if (!item.scorecardQuestionId || !item.content) {
this.logger.error(
`Invalid item: scorecardQuestionId and content are required.`,
);
throw new BadRequestException(
`Each item must have scorecardQuestionId and content.`,
);
}
const questionExists = await this.prisma.scorecardQuestion.findUnique({
where: { id: item.scorecardQuestionId },
});
if (!questionExists) {
this.logger.error(
`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`,
);
throw new BadRequestException(
`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`,
);
}
}

const createdItems = await this.prisma.aiWorkflowRunItem.createMany({
data: items.map((item) => ({
workflowRunId: runId,
scorecardQuestionId: item.scorecardQuestionId,
content: item.content,
upVotes: item.upVotes ?? 0,
downVotes: item.downVotes ?? 0,
questionScore: item.questionScore ?? null,
createdAt: new Date(),
// TODO: Remove this once prisma middleware implementation is done
createdBy: '',
})),
});

return { createdCount: createdItems.count };

}

async createWorkflowRun(workflowId: string, runData: CreateAiWorkflowRunDto) {
try {
return await this.prisma.aiWorkflowRun.create({
Expand Down Expand Up @@ -171,6 +233,7 @@ export class AiWorkflowService {
}
}


async getWorkflowRuns(
workflowId: string,
user: JwtUser,
Expand Down
42 changes: 41 additions & 1 deletion src/dto/aiWorkflow.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsNumber,
IsArray,
ValidateNested,
IsOptional,
IsInt,
IsDate,
Min,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';

export class CreateAiWorkflowDto {
@ApiProperty()
Expand Down Expand Up @@ -86,3 +90,39 @@ export class UpdateAiWorkflowRunDto extends OmitType(
PartialType(CreateAiWorkflowRunDto),
['submissionId'],
) {}

export class CreateAiWorkflowRunItemDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
scorecardQuestionId: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
content: string;

@ApiProperty({ required: false })
@IsOptional()
@IsInt()
@Min(0)
upVotes?: number;

@ApiProperty({ required: false })
@IsOptional()
@IsInt()
@Min(0)
downVotes?: number;

@ApiProperty({ required: false })
@IsOptional()
questionScore?: number;
}

export class CreateAiWorkflowRunItemsDto {
@ApiProperty({ type: [CreateAiWorkflowRunItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateAiWorkflowRunItemDto)
items: CreateAiWorkflowRunItemDto[];
}
7 changes: 3 additions & 4 deletions src/shared/enums/scopes.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,9 @@ export enum Scope {
CreateWorkflow = 'create:workflow',
ReadWorkflow = 'read:workflow',
UpdateWorkflow = 'update:workflow',

CreateWorkflowRuns = 'create:workflow-run',
ReadWorkflowRuns = 'read:workflow-run',
UpdateWorkflowRuns = 'update:workflow-run',
CreateWorkflowRun = 'create:workflow-run',
ReadWorkflowRun = 'read:workflow-run',
UpdateWorkflowRun = 'update:workflow-run',
}

/**
Expand Down