Skip to content

Commit

Permalink
refactor: Restructure Assignment OpenAI and Moss Config (#430)
Browse files Browse the repository at this point in the history
* refactor(assignments-service): Move OpenAI and Moss config to separate subdocuments

* fix(assignments-service): Migrate OpenAI and Moss config

* fix(frontend): Adapt new OpenAI and Moss config

* fix(assignments-service): Write Moss result in the correct path
  • Loading branch information
Clashsoft authored Oct 2, 2024
1 parent 127d25a commit bc43236
Show file tree
Hide file tree
Showing 14 changed files with 121 additions and 64 deletions.
22 changes: 15 additions & 7 deletions frontend/src/app/assignment/model/assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +35,8 @@ export default class Assignment {
deadline?: Date | string;

classroom?: ClassroomInfo;
moss?: MossConfig;
openAI?: OpenAIConfig;

passingPoints?: number;
tasks: Task[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<app-masked-input
id="openaiApiKeyInput"
type="password"
[(value)]="classroom.openaiApiKey"
[(value)]="openAI.apiKey"
(change)="context.saveDraft()"
placeholder="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
></app-masked-input>
Expand All @@ -35,7 +35,7 @@
</label>
@for (model of embeddingModels; track model.id) {
<div class="form-check">
<input class="form-check-input" type="radio" name="openaiModel" id="openaiModel-{{model.id}}" [value]="model.id" [ngModel]="classroom.openaiModel">
<input class="form-check-input" type="radio" name="openaiModel" id="openaiModel-{{model.id}}" [value]="model.id" [ngModel]="openAI.model">
<label class="form-check-label" for="openaiModel-{{model.id}}">
{{ model.id }}
<span class="badge bg-{{ model.labelBg }}">
Expand All @@ -58,7 +58,7 @@
</label>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="openaiConsentCheck"
[(ngModel)]="classroom.openaiConsent" (change)="context.saveDraft()">
[(ngModel)]="openAI.consent" (change)="context.saveDraft()">
<label class="form-check-label" for="openaiConsentCheck">Require Third-Party Consent</label>
</div>
<div class="form-text">
Expand All @@ -71,7 +71,7 @@
OpenAI Ignore
</label>
<textarea class="form-control" id="openaiIgnore" rows="10"
[(ngModel)]="classroom.openaiIgnore" (change)="context.saveDraft()"></textarea>
[(ngModel)]="openAI.ignore" (change)="context.saveDraft()"></textarea>
<div class="form-text">
A gitignore-like list of directories, files and methods to ignore when indexing code.
<details>
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 = [
Expand All @@ -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';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="mb-3">
<label class="form-label" for="mossIdInput">Moss User ID</label>
<input type="number" class="form-control" id="mossIdInput" placeholder="123456789"
[(ngModel)]="classroom.mossId" (change)="context.saveDraft()">
[(ngModel)]="moss.userId" (change)="context.saveDraft()">
<div class="form-text">
Your Moss User ID.
See "Registering for Moss" in the Moss documentation to obtain it.
Expand All @@ -17,7 +17,7 @@
</div>
<div class="mb-3">
<label class="form-label" for="mossLanguageInput">Moss Language</label>
<select class="form-control" id="mossLanguageInput" [(ngModel)]="classroom.mossLanguage" (change)="context.saveDraft()">
<select class="form-control" id="mossLanguageInput" [(ngModel)]="moss.language" (change)="context.saveDraft()">
@for (lang of mossLanguages | keyvalue;track lang) {
<option [value]="lang.key">{{ lang.value }}</option>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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',
templateUrl: './plagiarism-detection.component.html',
styleUrls: ['./plagiarism-detection.component.scss']
})
export class PlagiarismDetectionComponent {
classroom: ClassroomInfo;
moss: MossConfig;
email: string;

mossLanguages = {
Expand All @@ -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 ||= {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ <h1>{{ assignment ? assignment.title : 'Loading...' }}</h1>
@if (assignment.deadline) {
&mdash; Deadline: {{ assignment.deadline | date:'medium' }}
}
@if (assignment.classroom && assignment.classroom.mossResult) {
@if (assignment.moss?.result; as mossResult) {
&mdash;
<a class="bi-incognito" [href]="assignment.classroom.mossResult" target="_blank">
<a class="bi-incognito" [href]="mossResult" target="_blank">
Moss Results
</a>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}));
Expand Down
2 changes: 1 addition & 1 deletion services/apps/assignments/src/assignment/assignment.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand Down
41 changes: 31 additions & 10 deletions services/apps/assignments/src/assignment/assignment.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
37 changes: 31 additions & 6 deletions services/apps/assignments/src/assignment/assignment.service.ts
Original file line number Diff line number Diff line change
@@ -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<Assignment> {
export class AssignmentService extends MongooseRepository<Assignment> implements OnModuleInit {
constructor(
@InjectModel(Assignment.name) model: Model<Assignment>,
private eventService: EventService,
Expand All @@ -18,6 +18,31 @@ export class AssignmentService extends MongooseRepository<Assignment> {
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) {
Expand All @@ -39,13 +64,13 @@ export class AssignmentService extends MongooseRepository<Assignment> {
* @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,
};
}

Expand Down
25 changes: 13 additions & 12 deletions services/apps/assignments/src/classroom/classroom.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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 [];
}
Expand All @@ -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({
Expand Down
Loading

0 comments on commit bc43236

Please sign in to comment.