Skip to content
Closed
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
108 changes: 108 additions & 0 deletions grading-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"name": "grade",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "a short summary of the provided content. Must not be longer than 200 characters."
},
"categories": {
"type": "object",
"properties": {
"bibliography": {
"type": "object",
"description": "Are at least 3 sources of primary or secondary literature cited and used in the work?",
"properties": {
"grade": {
"type": "number",
"minimum": 1,
"maximum": 6,
"description": "The swiss school grade for this category, which ranges from 1-6 where grade 1 is the worst/failing mark, grade 4 is the minimum passing grade (sufficient performance), and grade 6 is the highest/excellent grade."
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"feedback": {
"type": "object",
"properties": {
"good": {
"type": "string"
},
"bad": {
"type": "string"
}
},
"required": [
"good",
"bad"
],
"additionalProperties": false
}
},
"required": [
"grade",
"sources",
"feedback"
],
"additionalProperties": false
},
"language": {
"type": "object",
"description": "Is the language precise and to the point, without unnecessary embellishments and easily understandable?",
"properties": {
"grade": {
"type": "number",
"minimum": 1,
"maximum": 6,
"description": "The swiss school grade for this category, which ranges from 1-6 where grade 1 is the worst/failing mark, grade 4 is the minimum passing grade (sufficient performance), and grade 6 is the highest/excellent grade."
},
"feedback": {
"type": "object",
"properties": {
"good": {
"type": "string"
},
"bad": {
"type": "string"
}
},
"required": [
"good",
"bad"
],
"additionalProperties": false
}
},
"required": [
"grade",
"feedback"
],
"additionalProperties": false
}
},
"required": [
"bibliography",
"language"
],
"additionalProperties": false
},
"final_grade": {
"type": "number",
"minimum": 1,
"maximum": 6,
"description": "The overall grade (swiss school) for the provided document, which ranges from 1-6 where grade 1 is the worst/failing mark, grade 4 is the minimum passing grade (sufficient performance), and grade 6 is the highest/excellent grade. This should be the summary of all evaluated categories"
}
},
"additionalProperties": false,
"required": [
"summary",
"categories",
"final_grade"
]
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"global": "^4.4.0",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"openai": "^5.0.1",
"passport": "^0.7.0",
"passport-azure-ad": "^4.3.5",
"socket.io": "^4.8.1",
Expand Down Expand Up @@ -62,4 +63,4 @@
"engines": {
"node": "^22.11.0"
}
}
}
39 changes: 39 additions & 0 deletions prisma/migrations/20250608170858_add_ai_models/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- CreateTable
CREATE TABLE "ai_templates" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"author_id" UUID NOT NULL,
"rate_limit" INTEGER NOT NULL DEFAULT 0,
"rate_limit_period_ms" BIGINT NOT NULL DEFAULT 3600000,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"api_key" TEXT NOT NULL,
"api_url" TEXT NOT NULL,
"config" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ai_templates_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "ai_requests" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"ai_template_id" UUID NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"status_code" INTEGER,
"request" TEXT NOT NULL,
"response" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ai_requests_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "ai_templates" ADD CONSTRAINT "ai_templates_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ai_requests" ADD CONSTRAINT "ai_requests_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ai_requests" ADD CONSTRAINT "ai_requests_ai_template_id_fkey" FOREIGN KEY ("ai_template_id") REFERENCES "ai_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
44 changes: 44 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,54 @@ model User {
view_AllDocumentUserPermissions view_AllDocumentUserPermissions[]
cmsSettings CmsSettings?
studentGroups UserStudentGroup[] @relation("user_student_groups")
aiRequests AiRequest[]
aiTemplates AiTemplate[] @relation("ai_templates")

@@map("users")
}

model AiTemplate {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid

author User @relation("ai_templates", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @map("author_id") @db.Uuid
rateLimit Int @default(0) @map("rate_limit")
rateLimitPeriodMs BigInt @default(3600000) @map("rate_limit_period_ms") // Default to 1 hour
isActive Boolean @default(true) @map("is_active")

apiKey String @map("api_key")
apiUrl String @map("api_url")

config Json @map("config") // Configuration for the AI template, e.g., model, parameters

aiRequests AiRequest[]

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@map("ai_templates")
}

model AiRequest {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @map("user_id") @db.Uuid

aiTemplate AiTemplate @relation(fields: [aiTemplateId], references: [id], onDelete: Cascade)
aiTemplateId String @map("ai_template_id") @db.Uuid

status String @default("pending") // pending, completed, failed
statusCode Int? @map("status_code")

request String
response Json

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@map("ai_requests")
}

model CmsSettings {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
Expand Down
2 changes: 1 addition & 1 deletion prisma/seed-files/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const users: Prisma.UserCreateInput[] = [
if (USER_EMAIL && USER_ID) {
const name = USER_EMAIL.split('@')[0];
users.push({
email: USER_EMAIL,
email: USER_EMAIL.toLowerCase(),
id: USER_ID,
firstName: name.split('.')[0],
lastName: name.split('.')[1] || name,
Expand Down
38 changes: 38 additions & 0 deletions src/controllers/aiRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RequestHandler } from 'express';
import AiRequest from '../models/AiRequest';
import { AiRequest as DbAiRequest } from '@prisma/client';
import { IoRoom } from '../routes/socketEvents';
import { IoEvent, RecordType } from '../routes/socketEventTypes';

export const all: RequestHandler<{ aiTemplateId: string }> = async (req, res, next) => {
try {
const requests = await AiRequest.all(req.user!, req.params.aiTemplateId);
res.json(requests);
} catch (error) {
next(error);
}
};

export const find: RequestHandler<{ id: string; requestId: string }> = async (req, res, next) => {
try {
const request = await AiRequest.findModel(req.user!, req.params.id, req.params.requestId);
res.json(request);
} catch (error) {
next(error);
}
};

export const create: RequestHandler<{ id: string }, any, { request: string }> = async (req, res, next) => {
try {
const onResponse = (aiRequest: DbAiRequest) => {
req.io?.to([req.user!.id, IoRoom.ADMIN]).emit(IoEvent.CHANGED_RECORD, {
type: RecordType.AiRequest,
record: aiRequest
});
};
const request = await AiRequest.createModel(req.user!, req.params.id, req.body.request, onResponse);
res.status(201).json(request);
} catch (error) {
next(error);
}
};
54 changes: 54 additions & 0 deletions src/controllers/aiTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RequestHandler } from 'express';
import AiTemplate from '../models/AiTemplate';
(BigInt.prototype as any).toJSON = function () {
return this.toString(); // Convert to string for serialization
};
export const all: RequestHandler = async (req, res, next) => {
try {
const templates = await AiTemplate.all(req.user!);
res.json(templates);
} catch (error) {
next(error);
}
};

export const find: RequestHandler<{ id: string }> = async (req, res, next) => {
try {
const template = await AiTemplate.findModel(req.user!, req.params.id);
res.json(template);
} catch (error) {
next(error);
}
};
export const clone: RequestHandler<{ id: string }, any, any> = async (req, res, next) => {
try {
const template = await AiTemplate.cloneModel(req.user!, req.params.id);
res.status(201).json(template);
} catch (error) {
next(error);
}
};
export const create: RequestHandler<any, any, any> = async (req, res, next) => {
try {
const template = await AiTemplate.createModel(req.user!, req.body);
res.status(201).json(template);
} catch (error) {
next(error);
}
};
export const update: RequestHandler<{ id: string }, any, any> = async (req, res, next) => {
try {
const template = await AiTemplate.updateModel(req.user!, req.params.id, req.body);
res.status(200).json(template);
} catch (error) {
next(error);
}
};
export const destroy: RequestHandler<{ id: string }> = async (req, res, next) => {
try {
const template = await AiTemplate.deleteModel(req.user!, req.params.id);
res.status(200).json(template);
} catch (error) {
next(error);
}
};
Loading