Skip to content

Commit aabd2f4

Browse files
committed
Merge branch 'main' of https://github.com/UTSC-CSCC01-Software-Engineering-I/term-group-project-c01w25-project-course-matrix into scrum-57-child-133-Update-timetable-endpoints-for-favorites
2 parents 7821a3a + 2699f2a commit aabd2f4

File tree

14 files changed

+81
-188
lines changed

14 files changed

+81
-188
lines changed

course-matrix/backend/jest.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import type { Config } from "jest";
22

33
const config: Config = {
44
preset: "ts-jest",
5-
moduleNameMapper: { "\\.(css|scss)$": "identity-obj-proxy" },
5+
moduleNameMapper: {
6+
"\\.(css|scss)$": "identity-obj-proxy",
7+
"^.+\\.svg": "<rootDir>/tests/mocks/svgMock.tsx",
8+
},
69
// to obtain access to the matchers.
10+
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
711
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
812
modulePaths: ["<rootDir>"],
913
testEnvironment: "jsdom",

course-matrix/backend/src/constants/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const yearToCode = (year: number) => {
3939

4040
// Set minimum results wanted for a similarity search on the associated namespace.
4141
export const namespaceToMinResults = new Map();
42-
namespaceToMinResults.set("courses_v3", 10);
42+
namespaceToMinResults.set("courses_v2", 10);
4343
namespaceToMinResults.set("offerings", 16); // Typically, more offering info is wanted.
4444
namespaceToMinResults.set("prerequisites", 5);
4545
namespaceToMinResults.set("corequisites", 5);

course-matrix/backend/src/constants/promptKeywords.ts

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Keywords related to each namespace
22
export const NAMESPACE_KEYWORDS = {
3-
courses_v3: [
3+
courses_v2: [
44
"course",
55
"class",
66
"description",
@@ -61,41 +61,6 @@ export const NAMESPACE_KEYWORDS = {
6161
programs: ["program", "major", "minor", "specialist", "degree", "stream"],
6262
};
6363

64-
export const BREADTH_REQUIREMENT_KEYWORDS = {
65-
ART_LIT_LANG: [
66-
"ART_LIT_LANG",
67-
"art literature",
68-
"arts literature",
69-
"art language",
70-
"arts language",
71-
"literature language",
72-
"art literature language",
73-
"arts literature language",
74-
],
75-
HIS_PHIL_CUL: [
76-
"HIS_PHIL_CUL",
77-
"history philosophy culture",
78-
"history, philosophy, culture",
79-
"history, philosophy, and culture",
80-
"history, philosophy",
81-
"history philosophy",
82-
"philosophy culture",
83-
"philosophy, culture",
84-
"history culture",
85-
"History, Philosophy and Cultural Studies",
86-
],
87-
SOCIAL_SCI: ["SOCIAL_SCI", "social science", "social sciences"],
88-
NAT_SCI: ["NAT_SCI", "natural science", "natural sciences"],
89-
QUANT: ["QUANT", "quantitative reasoning"],
90-
};
91-
92-
export const YEAR_LEVEL_KEYWORDS = {
93-
first_year: ["first year", "first-year", "A-level", "A level", "1st year"],
94-
second_year: ["second year", "second-year", "B-level", "B level", "2nd year"],
95-
third_year: ["third year", "third-year", "C-level", "C level", "3rd year"],
96-
fourth_year: ["fourth year", "fourth-year", "D-level", "D level", "4th year"],
97-
};
98-
9964
// General academic terms that might indicate a search is needed
10065
export const GENERAL_ACADEMIC_TERMS = ["credit", "enroll", "drop"];
10166

course-matrix/backend/src/controllers/aiController.ts

Lines changed: 19 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,10 @@ import {
1212
DEPARTMENT_CODES,
1313
ASSISTANT_TERMS,
1414
USEFUL_INFO,
15-
BREADTH_REQUIREMENT_KEYWORDS,
16-
YEAR_LEVEL_KEYWORDS,
1715
} from "../constants/promptKeywords";
1816
import { CHATBOT_MEMORY_THRESHOLD, codeToYear } from "../constants/constants";
1917
import { namespaceToMinResults } from "../constants/constants";
2018
import OpenAI from "openai";
21-
import { convertBreadthRequirement } from "../utils/convert-breadth-requirement";
22-
import { convertYearLevel } from "../utils/convert-year-level";
2319

2420
const openai = createOpenAI({
2521
baseURL: process.env.OPENAI_BASE_URL,
@@ -35,7 +31,7 @@ const pinecone = new Pinecone({
3531
});
3632

3733
const index: Index<RecordMetadata> = pinecone.Index(
38-
process.env.PINECONE_INDEX_NAME!,
34+
process.env.PINECONE_INDEX_NAME!
3935
);
4036

4137
console.log("Connected to OpenAI API");
@@ -62,8 +58,8 @@ function analyzeQuery(query: string): {
6258

6359
// If a course code is detected, add tehse namespaces
6460
if (containsCourseCode) {
65-
if (!relevantNamespaces.includes("courses_v3"))
66-
relevantNamespaces.push("courses_v3");
61+
if (!relevantNamespaces.includes("courses_v2"))
62+
relevantNamespaces.push("courses_v2");
6763
if (!relevantNamespaces.includes("offerings"))
6864
relevantNamespaces.push("offerings");
6965
if (!relevantNamespaces.includes("prerequisites"))
@@ -74,8 +70,8 @@ function analyzeQuery(query: string): {
7470
if (DEPARTMENT_CODES.some((code) => lowerQuery.includes(code))) {
7571
if (!relevantNamespaces.includes("departments"))
7672
relevantNamespaces.push("departments");
77-
if (!relevantNamespaces.includes("courses_v3"))
78-
relevantNamespaces.push("courses_v3");
73+
if (!relevantNamespaces.includes("courses_v2"))
74+
relevantNamespaces.push("courses_v2");
7975
}
8076

8177
// If search is required at all
@@ -87,12 +83,12 @@ function analyzeQuery(query: string): {
8783
// If no specific namespaces identified & search required, then search all
8884
if (requiresSearch && relevantNamespaces.length === 0) {
8985
relevantNamespaces.push(
90-
"courses_v3",
86+
"courses_v2",
9187
"offerings",
9288
"prerequisites",
9389
"corequisites",
9490
"departments",
95-
"programs",
91+
"programs"
9692
);
9793
}
9894

@@ -109,8 +105,7 @@ function analyzeQuery(query: string): {
109105
async function searchSelectedNamespaces(
110106
query: string,
111107
k: number,
112-
namespaces: string[],
113-
filters?: Object,
108+
namespaces: string[]
114109
): Promise<Document[]> {
115110
let allResults: Document[] = [];
116111

@@ -131,8 +126,7 @@ async function searchSelectedNamespaces(
131126
// Search results count given by the min result count for a given namespace (or k if k is greater)
132127
const results = await namespaceStore.similaritySearch(
133128
query,
134-
Math.max(k, namespaceToMinResults.get(namespace)),
135-
namespace === "courses_v3" ? filters : undefined,
129+
Math.max(k, namespaceToMinResults.get(namespace))
136130
);
137131
console.log(`Found ${results.length} results in namespace: ${namespace}`);
138132
allResults = [...allResults, ...results];
@@ -153,7 +147,7 @@ async function searchSelectedNamespaces(
153147
// Reformulate user query to make more concise query to database, taking into consideration context
154148
async function reformulateQuery(
155149
latestQuery: string,
156-
conversationHistory: any[],
150+
conversationHistory: any[]
157151
): Promise<string> {
158152
try {
159153
const openai = new OpenAI({
@@ -233,69 +227,6 @@ async function reformulateQuery(
233227
}
234228
}
235229

236-
// Determines whether to apply metadata filtering based on user query.
237-
function includeFilters(query: string) {
238-
const lowerQuery = query.toLocaleLowerCase();
239-
const relaventBreadthRequirements: string[] = [];
240-
const relaventYearLevels: string[] = [];
241-
242-
Object.entries(BREADTH_REQUIREMENT_KEYWORDS).forEach(
243-
([namespace, keywords]) => {
244-
if (keywords.some((keyword) => lowerQuery.includes(keyword))) {
245-
relaventBreadthRequirements.push(convertBreadthRequirement(namespace));
246-
}
247-
},
248-
);
249-
250-
Object.entries(YEAR_LEVEL_KEYWORDS).forEach(([namespace, keywords]) => {
251-
if (keywords.some((keyword) => lowerQuery.includes(keyword))) {
252-
relaventYearLevels.push(convertYearLevel(namespace));
253-
}
254-
});
255-
256-
let filter = {};
257-
if (relaventBreadthRequirements.length > 0 && relaventYearLevels.length > 0) {
258-
filter = {
259-
$and: [
260-
{
261-
$or: relaventBreadthRequirements.map((req) => ({
262-
breadth_requirement: { $eq: req },
263-
})),
264-
},
265-
{
266-
$or: relaventYearLevels.map((yl) => ({ year_level: { $eq: yl } })),
267-
},
268-
],
269-
};
270-
} else if (relaventBreadthRequirements.length > 0) {
271-
filter = {
272-
$or: relaventBreadthRequirements.map((req) => ({
273-
breadth_requirement: { $eq: req },
274-
})),
275-
};
276-
} else if (relaventYearLevels.length > 0) {
277-
filter = {
278-
$or: relaventYearLevels.map((yl) => ({ year_level: { $eq: yl } })),
279-
};
280-
}
281-
return filter;
282-
}
283-
284-
/**
285-
* @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
286-
*
287-
* @param {Request} req - The Express request object, containing:
288-
* @param {Object[]} req.body.messages - Array of message objects representing the conversation history.
289-
* @param {string} req.body.messages[].role - The role of the message sender (e.g., "user", "assistant").
290-
* @param {Object[]} req.body.messages[].content - An array containing message content objects.
291-
* @param {string} req.body.messages[].content[].text - The actual text of the message.
292-
*
293-
* @param {Response} res - The Express response object used to stream the generated response.
294-
*
295-
* @returns {void} Responds with a streamed text response of the AI output
296-
*
297-
* @throws {Error} If query reformulation or knowledge retrieval fails.
298-
*/
299230
export const chat = asyncHandler(async (req: Request, res: Response) => {
300231
const { messages } = req.body;
301232
const latestMessage = messages[messages.length - 1].content[0].text;
@@ -309,7 +240,7 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
309240
// Use GPT-4o to reformulate the query based on conversation history
310241
const reformulatedQuery = await reformulateQuery(
311242
latestMessage,
312-
conversationHistory.slice(-CHATBOT_MEMORY_THRESHOLD), // last K messages
243+
conversationHistory.slice(-CHATBOT_MEMORY_THRESHOLD) // last K messages
313244
);
314245
console.log(">>>> Original query:", latestMessage);
315246
console.log(">>>> Reformulated query:", reformulatedQuery);
@@ -323,19 +254,15 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
323254
if (requiresSearch) {
324255
console.log(
325256
`Query requires knowledge retrieval, searching namespaces: ${relevantNamespaces.join(
326-
", ",
327-
)}`,
257+
", "
258+
)}`
328259
);
329260

330-
const filters = includeFilters(reformulatedQuery);
331-
// console.log("Filters: ", JSON.stringify(filters))
332-
333261
// Search only relevant namespaces
334262
const searchResults = await searchSelectedNamespaces(
335263
reformulatedQuery,
336264
3,
337-
relevantNamespaces,
338-
Object.keys(filters).length === 0 ? undefined : filters,
265+
relevantNamespaces
339266
);
340267
// console.log("Search Results: ", searchResults);
341268

@@ -403,15 +330,15 @@ export const testSimilaritySearch = asyncHandler(
403330
if (requiresSearch) {
404331
console.log(
405332
`Query requires knowledge retrieval, searching namespaces: ${relevantNamespaces.join(
406-
", ",
407-
)}`,
333+
", "
334+
)}`
408335
);
409336

410337
// Search only the relevant namespaces
411338
const searchResults = await searchSelectedNamespaces(
412339
message,
413340
3,
414-
relevantNamespaces,
341+
relevantNamespaces
415342
);
416343
console.log("Search Results: ", searchResults);
417344

@@ -421,11 +348,11 @@ export const testSimilaritySearch = asyncHandler(
421348
}
422349
} else {
423350
console.log(
424-
"Query does not require knowledge retrieval, skipping search",
351+
"Query does not require knowledge retrieval, skipping search"
425352
);
426353
}
427354

428355
console.log("CONTEXT: ", context);
429356
res.status(200).send(context);
430-
},
357+
}
431358
);

course-matrix/backend/src/routes/aiRouter.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,5 @@ import { authRouter } from "./authRouter";
44

55
export const aiRouter = express.Router();
66

7-
/**
8-
* @route POST /api/ai/chat
9-
* @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
10-
*/
117
aiRouter.post("/chat", authRouter, chat);
12-
/**
13-
* @route POST /api/ai/test-similarity-search
14-
* @description Test vector database similarity search feature
15-
*/
168
aiRouter.post("/test-similarity-search", testSimilaritySearch);

course-matrix/backend/src/utils/embeddings.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { PineconeStore } from "@langchain/pinecone";
55
import { Pinecone } from "@pinecone-database/pinecone";
66
import config from "../config/config";
77
import path from "path";
8-
import { convertBreadthRequirement } from "./convert-breadth-requirement";
98

109
console.log("Running embeddings process...");
1110

@@ -38,35 +37,6 @@ async function processCSV(filePath: string, namespace: string) {
3837
});
3938
}
4039

41-
// Generate embeddings for courses.csv
42-
async function processCoursesCSV(filePath: string, namespace: string) {
43-
const fileName = path.basename(filePath);
44-
const loader = new CSVLoader(filePath);
45-
let docs = await loader.load();
46-
47-
docs = docs.map((doc, index) => ({
48-
...doc,
49-
metadata: {
50-
...doc.metadata,
51-
source: fileName,
52-
row: index + 1,
53-
breadth_requirement: convertBreadthRequirement(
54-
doc.pageContent.split("\n")[1].split(": ")[1],
55-
),
56-
year_level: doc.pageContent.split("\n")[10].split(": ")[1],
57-
},
58-
}));
59-
console.log("Sample doc: ", docs[0]);
60-
61-
const index = pinecone.Index(process.env.PINECONE_INDEX_NAME!);
62-
63-
// Store each row as an individual embedding
64-
await PineconeStore.fromDocuments(docs, embeddings, {
65-
pineconeIndex: index as any,
66-
namespace: namespace,
67-
});
68-
}
69-
7040
// Generate embeddings for pdfs
7141
async function processPDF(filePath: string, namespace: string) {
7242
const fileName = path.basename(filePath);
@@ -101,7 +71,7 @@ async function processPDF(filePath: string, namespace: string) {
10171
// console.log("Sample split docs: ", splitDocs.slice(0, 6))
10272

10373
console.log(
104-
`Split into ${splitDocs.length} sections by "Calendar Section:" delimiter`,
74+
`Split into ${splitDocs.length} sections by "Calendar Section:" delimiter`
10575
);
10676

10777
// Store the split documents as embeddings
@@ -128,7 +98,6 @@ async function processPDF(filePath: string, namespace: string) {
12898
// processCSV("../data/tables/offerings_winter_2026.csv", "offerings")
12999
// processCSV("../data/tables/departments.csv", "departments")
130100
// processCSV("../data/tables/courses_with_year.csv", "courses_v2")
131-
// processCoursesCSV("../data/tables/courses_with_year.csv", "courses_v3");
132101

133102
console.log("embeddings done.");
134103

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export default "SvgrURL";
2+
export const ReactComponent = "div";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import "@testing-library/jest-dom";
1+
import "@testing-library/jest-dom";

0 commit comments

Comments
 (0)