diff --git a/frontend/src/app/assignment/model/assignment.ts b/frontend/src/app/assignment/model/assignment.ts index 159338b6..329d483f 100644 --- a/frontend/src/app/assignment/model/assignment.ts +++ b/frontend/src/app/assignment/model/assignment.ts @@ -6,13 +6,19 @@ export interface ClassroomInfo { token?: string; webhook?: string; codeSearch?: boolean; - mossId?: number; - mossLanguage?: string; - mossResult?: string; - openaiApiKey?: string; - openaiModel?: string; - openaiConsent?: boolean; - openaiIgnore?: string; +} + +export interface MossConfig { + userId?: number; + language?: string; + result?: string; +} + +export interface OpenAIConfig { + apiKey?: string; + model?: string; + consent?: boolean; + ignore?: string; } export default class Assignment { @@ -29,6 +35,8 @@ export default class Assignment { deadline?: Date | string; classroom?: ClassroomInfo; + moss?: MossConfig; + openAI?: OpenAIConfig; passingPoints?: number; tasks: Task[]; diff --git a/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.html b/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.html index 5caf1121..669bea04 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.html +++ b/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.html @@ -20,7 +20,7 @@ @@ -35,7 +35,7 @@ @for (model of embeddingModels; track model.id) {
- +
+ [(ngModel)]="openAI.consent" (change)="context.saveDraft()">
@@ -71,7 +71,7 @@ OpenAI Ignore + [(ngModel)]="openAI.ignore" (change)="context.saveDraft()">
A gitignore-like list of directories, files and methods to ignore when indexing code.
diff --git a/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.ts b/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.ts index 8cd1c9fa..1d79e2e8 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.ts +++ b/frontend/src/app/assignment/modules/edit-assignment/code-search/code-search.component.ts @@ -1,6 +1,6 @@ import {Component} from '@angular/core'; -import {AssignmentContext} from "../../../services/assignment.context"; -import {ClassroomInfo} from "../../../model/assignment"; +import {AssignmentContext} from '../../../services/assignment.context'; +import {ClassroomInfo, OpenAIConfig} from '../../../model/assignment'; @Component({ selector: 'app-code-search', @@ -9,6 +9,7 @@ import {ClassroomInfo} from "../../../model/assignment"; }) export class CodeSearchComponent { classroom: ClassroomInfo; + openAI: OpenAIConfig; // TODO use a shared constant when frontend and backend are merged embeddingModels = [ @@ -21,7 +22,8 @@ export class CodeSearchComponent { readonly context: AssignmentContext, ) { this.classroom = this.context.assignment.classroom ||= {}; - this.classroom.openaiConsent ??= true; - this.classroom.openaiModel ??= 'text-embedding-ada-002'; + this.openAI = this.context.assignment.openAI ||= {}; + this.openAI.consent ??= true; + this.openAI.model ??= 'text-embedding-ada-002'; } } diff --git a/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.html b/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.html index d9715565..ba30e122 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.html +++ b/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.html @@ -6,7 +6,7 @@
+ [(ngModel)]="moss.userId" (change)="context.saveDraft()">
Your Moss User ID. See "Registering for Moss" in the Moss documentation to obtain it. @@ -17,7 +17,7 @@
- @for (lang of mossLanguages | keyvalue;track lang) { } diff --git a/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.ts b/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.ts index fc4c2285..9545e47f 100644 --- a/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.ts +++ b/frontend/src/app/assignment/modules/edit-assignment/plagiarism-detection/plagiarism-detection.component.ts @@ -1,7 +1,7 @@ import {Component} from '@angular/core'; -import {AssignmentContext} from "../../../services/assignment.context"; -import {ClassroomInfo} from "../../../model/assignment"; -import {ConfigService} from "../../../services/config.service"; +import {AssignmentContext} from '../../../services/assignment.context'; +import {MossConfig} from '../../../model/assignment'; +import {ConfigService} from '../../../services/config.service'; @Component({ selector: 'app-plagiarism-detection', @@ -9,7 +9,7 @@ import {ConfigService} from "../../../services/config.service"; styleUrls: ['./plagiarism-detection.component.scss'] }) export class PlagiarismDetectionComponent { - classroom: ClassroomInfo; + moss: MossConfig; email: string; mossLanguages = { @@ -23,9 +23,9 @@ export class PlagiarismDetectionComponent { constructor( readonly context: AssignmentContext, - private configService: ConfigService, + configService: ConfigService, ) { this.email = configService.get('email'); - this.classroom = context.assignment.classroom ||= {}; + this.moss = context.assignment.moss ||= {}; } } diff --git a/frontend/src/app/assignment/modules/shared/assignment-info/assignment-info.component.html b/frontend/src/app/assignment/modules/shared/assignment-info/assignment-info.component.html index fbdbcee9..dcca39de 100644 --- a/frontend/src/app/assignment/modules/shared/assignment-info/assignment-info.component.html +++ b/frontend/src/app/assignment/modules/shared/assignment-info/assignment-info.component.html @@ -12,9 +12,9 @@

{{ assignment ? assignment.title : 'Loading...' }}

@if (assignment.deadline) { — Deadline: {{ assignment.deadline | date:'medium' }} } - @if (assignment.classroom && assignment.classroom.mossResult) { + @if (assignment.moss?.result; as mossResult) { — - + Moss Results } diff --git a/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts b/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts index eaa325a7..92b745dc 100644 --- a/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts +++ b/frontend/src/app/assignment/modules/solution/evaluation-modal/evaluation-modal.component.ts @@ -92,7 +92,7 @@ export class EvaluationModalComponent implements OnInit, OnDestroy { if (!assignment.classroom?.codeSearch) { this.dto.codeSearch = this.codeSearchEnabled = false; } - if (!assignment.classroom?.openaiApiKey) { + if (!assignment.openAI?.apiKey) { this.viewSimilar = this.similarSolutionsEnabled = false; } })); diff --git a/services/apps/assignments/src/assignment/assignment.dto.ts b/services/apps/assignments/src/assignment/assignment.dto.ts index 6e5958b9..840b634b 100644 --- a/services/apps/assignments/src/assignment/assignment.dto.ts +++ b/services/apps/assignments/src/assignment/assignment.dto.ts @@ -14,7 +14,7 @@ export class ReadTaskDto extends OmitType(Task, ['note', 'children'] as const) { children: ReadTaskDto[]; } -export class ReadAssignmentDto extends OmitType(Assignment, ['token', 'tasks', 'classroom'] as const) { +export class ReadAssignmentDto extends OmitType(Assignment, ['token', 'tasks', 'classroom', 'moss', 'openAI'] as const) { @ApiProperty({type: [ReadTaskDto]}) tasks: ReadTaskDto[]; } diff --git a/services/apps/assignments/src/assignment/assignment.schema.ts b/services/apps/assignments/src/assignment/assignment.schema.ts index 5b1b481b..8af7169c 100644 --- a/services/apps/assignments/src/assignment/assignment.schema.ts +++ b/services/apps/assignments/src/assignment/assignment.schema.ts @@ -18,9 +18,9 @@ import { ValidateNested, } from 'class-validator'; import {Types} from 'mongoose'; -import {MOSS_LANGUAGES} from "../search/search.constants"; -import {Doc} from "@mean-stream/nestx"; -import {EmbeddingModel, EMBEDDING_MODELS} from "../embedding/openai.service"; +import {MOSS_LANGUAGES} from '../search/search.constants'; +import {Doc} from '@mean-stream/nestx'; +import {EMBEDDING_MODELS, EmbeddingModel} from '../embedding/openai.service'; @Schema({id: false, _id: false}) export class Task { @@ -92,49 +92,56 @@ export class ClassroomInfo { @IsOptional() @IsBoolean() codeSearch?: boolean; +} +@Schema({id: false, _id: false}) +export class MossConfig { @Prop() @ApiPropertyOptional() @IsOptional() @IsInt() - mossId?: number; + userId?: number; @Prop({type: String}) @ApiPropertyOptional({enum: Object.keys(MOSS_LANGUAGES)}) @IsOptional() @IsIn(Object.keys(MOSS_LANGUAGES)) - mossLanguage?: keyof typeof MOSS_LANGUAGES; + language?: keyof typeof MOSS_LANGUAGES; @Prop() @ApiPropertyOptional() @IsOptional() @IsUrl() - mossResult?: string; + result?: string; +} + +@Schema({id: false, _id: false}) +export class OpenAIConfig { @Prop({transform: (v?: string) => v && '***'}) @ApiPropertyOptional() @IsOptional() @IsString() @Transform(({value}) => value === '***' ? undefined : value) - openaiApiKey?: string; + apiKey?: string; @Prop({type: String}) @ApiPropertyOptional() @IsOptional() @IsIn(Object.keys(EMBEDDING_MODELS)) - openaiModel?: EmbeddingModel; + model?: EmbeddingModel; @Prop() @ApiPropertyOptional() @IsOptional() @IsBoolean() - openaiConsent?: boolean; + consent?: boolean; @Prop() @ApiPropertyOptional() @IsOptional() @IsString() - openaiIgnore?: string; + ignore?: string; } @Schema() @@ -199,6 +206,20 @@ export class Assignment { @Type(() => ClassroomInfo) classroom?: ClassroomInfo; + @Prop({type: MossConfig}) + @ApiProperty({required: false}) + @IsOptional() + @ValidateNested() + @Type(() => MossConfig) + moss?: MossConfig; + + @Prop({type: OpenAIConfig}) + @ApiProperty({required: false}) + @IsOptional() + @ValidateNested() + @Type(() => OpenAIConfig) + openAI?: OpenAIConfig; + @Prop() @ApiPropertyOptional() @IsOptional() diff --git a/services/apps/assignments/src/assignment/assignment.service.ts b/services/apps/assignments/src/assignment/assignment.service.ts index 36077e79..99eb92cf 100644 --- a/services/apps/assignments/src/assignment/assignment.service.ts +++ b/services/apps/assignments/src/assignment/assignment.service.ts @@ -1,15 +1,15 @@ import {EventRepository, EventService, MongooseRepository} from '@mean-stream/nestx'; import {UserToken} from '@app/keycloak-auth'; -import {Injectable} from '@nestjs/common'; +import {Injectable, Logger, OnModuleInit} from '@nestjs/common'; import {InjectModel} from '@nestjs/mongoose'; import {Model} from 'mongoose'; import {ReadAssignmentDto, ReadTaskDto} from './assignment.dto'; import {Assignment, AssignmentDocument, Task} from './assignment.schema'; -import {MemberService} from "@app/member"; +import {MemberService} from '@app/member'; @Injectable() @EventRepository() -export class AssignmentService extends MongooseRepository { +export class AssignmentService extends MongooseRepository implements OnModuleInit { constructor( @InjectModel(Assignment.name) model: Model, private eventService: EventService, @@ -18,6 +18,31 @@ export class AssignmentService extends MongooseRepository { super(model); } + async onModuleInit() { + const result = await this.model.updateMany({ + $or: [ + {'classroom.mossId': {$exists: true}}, + {'classroom.mossLanguage': {$exists: true}}, + {'classroom.mossResult': {$exists: true}}, + {'classroom.openaiApiKey': {$exists: true}}, + {'classroom.openaiModel': {$exists: true}}, + {'classroom.openaiConsent': {$exists: true}}, + {'classroom.openaiIgnore': {$exists: true}}, + ], + }, { + $rename: { + 'classroom.mossId': 'moss.userId', + 'classroom.mossLanguage': 'moss.language', + 'classroom.mossResult': 'moss.result', + 'classroom.openaiApiKey': 'openAI.apiKey', + 'classroom.openaiModel': 'openAI.model', + 'classroom.openaiConsent': 'openAI.consent', + 'classroom.openaiIgnore': 'openAI.ignore', + }, + }); + result.modifiedCount && new Logger(AssignmentService.name).warn(`Migrated ${result.modifiedCount} assignments`); + } + findTask(tasks: Task[], id: string): Task | undefined { for (const task of tasks) { if (task._id == id) { @@ -39,13 +64,13 @@ export class AssignmentService extends MongooseRepository { * @returns the masked assignment */ mask(assignment: Assignment): ReadAssignmentDto { - const {token: _token, tasks: _tasks, classroom: _classroom, ...rest} = assignment; - const tasks = assignment.deadline && assignment.deadline.valueOf() > Date.now() + const {token, tasks, classroom, moss, openAI, ...rest} = assignment; + const readTasks = assignment.deadline && assignment.deadline.valueOf() > Date.now() ? [] // hide tasks if deadline is in the future : assignment.tasks.map(t => this.maskTask(t)); return { ...rest, - tasks, + tasks: readTasks, }; } diff --git a/services/apps/assignments/src/classroom/classroom.service.ts b/services/apps/assignments/src/classroom/classroom.service.ts index 68224ef2..c8a8fa27 100644 --- a/services/apps/assignments/src/classroom/classroom.service.ts +++ b/services/apps/assignments/src/classroom/classroom.service.ts @@ -50,8 +50,7 @@ export class ClassroomService { }) const solutions = await this.upsertSolutions(assignment, importSolutions); - const {codeSearch, mossId} = assignment.classroom || {}; - if (codeSearch || mossId) { + if (assignment.classroom?.codeSearch || assignment.moss?.userId) { await Promise.all(files.map(async (file, index) => { const stream = createReadStream(file.path); const solution = solutions.upsertedIds[index]; @@ -120,7 +119,7 @@ export class ClassroomService { return []; } - const {org, prefix, token, codeSearch, mossId, openaiApiKey} = assignment.classroom; + const {org, prefix, token, codeSearch} = assignment.classroom; if (!org || !prefix) { return []; } @@ -145,16 +144,18 @@ export class ClassroomService { } }); - (codeSearch || mossId || openaiApiKey) && await Promise.all(repositories.map(async (repo, i) => { - const commit = commits[i]; - const upsertedId = solutions.upsertedIds[i]; - if (commit && upsertedId) { - const zip = await this.getRepoZip(assignment, this.getGithubName(repo, assignment), commit); - if (zip) { - await this.fileService.importZipEntries(zip, assignment.id, upsertedId.toString(), commit); + if (codeSearch || assignment.moss?.userId || assignment.openAI?.apiKey) { + await Promise.all(repositories.map(async (repo, i) => { + const commit = commits[i]; + const upsertedId = solutions.upsertedIds[i]; + if (commit && upsertedId) { + const zip = await this.getRepoZip(assignment, this.getGithubName(repo, assignment), commit); + if (zip) { + await this.fileService.importZipEntries(zip, assignment.id, upsertedId.toString(), commit); + } } - } - })); + })); + } if (reimport) { const otherSolutions = await this.solutionService.findAll({ diff --git a/services/apps/assignments/src/embedding/embedding.handler.ts b/services/apps/assignments/src/embedding/embedding.handler.ts index e9aa64df..51553dee 100644 --- a/services/apps/assignments/src/embedding/embedding.handler.ts +++ b/services/apps/assignments/src/embedding/embedding.handler.ts @@ -15,7 +15,7 @@ export class EmbeddingHandler { @OnEvent('assignments.*.created') @OnEvent('assignments.*.updated') async onAssignment(assignment: AssignmentDocument) { - if (!assignment.classroom?.openaiApiKey) { + if (!assignment.openAI?.apiKey) { return; } @@ -45,7 +45,7 @@ export class EmbeddingHandler { task: task._id, text: prefix + task.description, embedding: [], - }, assignment.classroom!.openaiApiKey!, assignment.classroom!.openaiModel ?? DEFAULT_MODEL); + }, assignment.openAI!.apiKey!, assignment.openAI!.model ?? DEFAULT_MODEL); } @OnEvent('assignments.*.solutions.*.deleted') diff --git a/services/apps/assignments/src/embedding/embedding.service.ts b/services/apps/assignments/src/embedding/embedding.service.ts index a4218df7..2e222d1e 100644 --- a/services/apps/assignments/src/embedding/embedding.service.ts +++ b/services/apps/assignments/src/embedding/embedding.service.ts @@ -68,11 +68,11 @@ export class EmbeddingService implements OnModuleInit { } async createEmbeddings(assignment: Assignment, estimate = false): Promise { - const apiKey = assignment.classroom?.openaiApiKey; + const apiKey = assignment.openAI?.apiKey; if (!apiKey) { throw new ForbiddenException('No OpenAI API key configured for this assignment.'); } - const model = assignment.classroom?.openaiModel || DEFAULT_MODEL; + const model = assignment.openAI?.model || DEFAULT_MODEL; const {solutions, documents, ignoreFn, ignoredFiles} = await this.getDocuments(assignment); const ignoredFunctions = new Set(); @@ -126,13 +126,13 @@ export class EmbeddingService implements OnModuleInit { private async getDocuments(assignment: Assignment) { const filter: FilterQuery = {assignment: assignment._id}; - if (assignment.classroom?.openaiConsent !== false) { + if (assignment.openAI?.consent !== false) { filter['consent.3P'] = true; } const solutionsWithConsent = await this.solutionService.findAll(filter, {projection: {_id: 1}}); const allDocuments = await this.searchService.findAll(assignment._id.toString(), solutionsWithConsent.map(s => s.id)); - const ignoreFn = assignment.classroom?.openaiIgnore ? ignore.compile(assignment.classroom.openaiIgnore) as (path: string) => boolean : undefined; + const ignoreFn = assignment.openAI?.ignore ? ignore.compile(assignment.openAI.ignore) as (path: string) => boolean : undefined; const ignoredFiles = new Set(); const documents = allDocuments.filter(d => { if (!this.openaiService.isSupportedExtension(d.file)) { diff --git a/services/apps/assignments/src/moss/moss.service.ts b/services/apps/assignments/src/moss/moss.service.ts index ce1d9486..23920fa8 100644 --- a/services/apps/assignments/src/moss/moss.service.ts +++ b/services/apps/assignments/src/moss/moss.service.ts @@ -13,13 +13,13 @@ export class MossService { async moss(assignment: AssignmentDocument, files: File[]): Promise { const moss = new MossApi(); - moss.userid = assignment.classroom?.mossId || 0; - const lang = assignment.classroom?.mossLanguage || 'java'; + moss.userid = assignment.moss?.userId || 0; + const lang = assignment.moss?.language || 'java'; const exts = MOSS_LANGUAGES[lang]; moss.language = lang; moss.files = files.filter(file => exts.some(ext => file.name.endsWith(ext))); const result = await moss.send(); - await this.assignmentService.update(assignment._id, {'classroom.mossResult': result}); + await this.assignmentService.update(assignment._id, {'moss.result': result}); return result; } }