Skip to content

Commit 8434b7a

Browse files
Merge branch 'develop' into mt/scrum-57-child-133-Update-timetable-endpoints-for-favorites
2 parents b0a4a6b + 8479c79 commit 8434b7a

File tree

13 files changed

+295
-25
lines changed

13 files changed

+295
-25
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ The `DATABASE_URL` variable should contain your Supabase project url and the `DA
6161

6262
```
6363
VITE_SERVER_URL="http://localhost:8081"
64+
VITE_PUBLIC_ASSISTANT_BASE_URL=[Insert vite public assistant bas URL]
65+
VITE_ASSISTANT_UI_KEY=[Insert vite assistant UI key]
6466
```
6567

6668
### Running the Application

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_v2", 10);
42+
namespaceToMinResults.set("courses_v3", 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: 36 additions & 1 deletion
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_v2: [
3+
courses_v3: [
44
"course",
55
"class",
66
"description",
@@ -61,6 +61,41 @@ 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+
6499
// General academic terms that might indicate a search is needed
65100
export const GENERAL_ACADEMIC_TERMS = ["credit", "enroll", "drop"];
66101

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

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import {
1212
DEPARTMENT_CODES,
1313
ASSISTANT_TERMS,
1414
USEFUL_INFO,
15+
BREADTH_REQUIREMENT_KEYWORDS,
16+
YEAR_LEVEL_KEYWORDS,
1517
} from "../constants/promptKeywords";
1618
import { CHATBOT_MEMORY_THRESHOLD, codeToYear } from "../constants/constants";
1719
import { namespaceToMinResults } from "../constants/constants";
1820
import OpenAI from "openai";
21+
import { convertBreadthRequirement } from "../utils/convert-breadth-requirement";
22+
import { convertYearLevel } from "../utils/convert-year-level";
1923

2024
const openai = createOpenAI({
2125
baseURL: process.env.OPENAI_BASE_URL,
@@ -58,8 +62,8 @@ function analyzeQuery(query: string): {
5862

5963
// If a course code is detected, add tehse namespaces
6064
if (containsCourseCode) {
61-
if (!relevantNamespaces.includes("courses_v2"))
62-
relevantNamespaces.push("courses_v2");
65+
if (!relevantNamespaces.includes("courses_v3"))
66+
relevantNamespaces.push("courses_v3");
6367
if (!relevantNamespaces.includes("offerings"))
6468
relevantNamespaces.push("offerings");
6569
if (!relevantNamespaces.includes("prerequisites"))
@@ -70,8 +74,8 @@ function analyzeQuery(query: string): {
7074
if (DEPARTMENT_CODES.some((code) => lowerQuery.includes(code))) {
7175
if (!relevantNamespaces.includes("departments"))
7276
relevantNamespaces.push("departments");
73-
if (!relevantNamespaces.includes("courses_v2"))
74-
relevantNamespaces.push("courses_v2");
77+
if (!relevantNamespaces.includes("courses_v3"))
78+
relevantNamespaces.push("courses_v3");
7579
}
7680

7781
// If search is required at all
@@ -83,7 +87,7 @@ function analyzeQuery(query: string): {
8387
// If no specific namespaces identified & search required, then search all
8488
if (requiresSearch && relevantNamespaces.length === 0) {
8589
relevantNamespaces.push(
86-
"courses_v2",
90+
"courses_v3",
8791
"offerings",
8892
"prerequisites",
8993
"corequisites",
@@ -106,6 +110,7 @@ async function searchSelectedNamespaces(
106110
query: string,
107111
k: number,
108112
namespaces: string[],
113+
filters?: Object,
109114
): Promise<Document[]> {
110115
let allResults: Document[] = [];
111116

@@ -127,6 +132,7 @@ async function searchSelectedNamespaces(
127132
const results = await namespaceStore.similaritySearch(
128133
query,
129134
Math.max(k, namespaceToMinResults.get(namespace)),
135+
namespace === "courses_v3" ? filters : undefined,
130136
);
131137
console.log(`Found ${results.length} results in namespace: ${namespace}`);
132138
allResults = [...allResults, ...results];
@@ -172,16 +178,18 @@ async function reformulateQuery(
172178
- DO replace pronouns and references with specific names and identifiers
173179
- DO include course codes, names and specific details for academic entities
174180
- If the query is not about university courses & offerings, return exactly a copy of the user's query.
181+
- Append "code: " before course codes For example: "CSCC01" -> "code: CSCC01"
182+
- If a course year level is written as "first year", "second year", etc. Then replace "first" with "1st" and "second" with "2nd" etc.
175183
176184
Examples:
177185
User: "When is it offered?"
178-
Output: "When is CSCA48 Introduction to Computer Science offered in the 2024-2025 academic year?"
186+
Output: "When is CSCA48 offered in the 2024-2025 academic year?"
179187
180188
User: "Tell me more about that"
181-
Output: "What are the details, descriptions, and requirements for MATA31 Calculus I?"
189+
Output: "What are the details, descriptions, and requirements for MATA31?"
182190
183191
User: "Who teaches it?"
184-
Output: "Who are the instructors for MGEA02 Introduction to Microeconomics at UTSC?"
192+
Output: "Who are the instructors for MGEA02 at UTSC?"
185193
186194
User: "What are the course names of those codes?"
187195
Output: "What are the course names of course codes: MGTA01, CSCA08, MATA31, MATA35?"
@@ -192,8 +200,13 @@ async function reformulateQuery(
192200
User: "Give 2nd year math courses."
193201
Output: "What are some 2nd year math courses?"
194202
195-
User: "Give first year math courses."
196-
Output: "What are some 1st year math courses?"`,
203+
User: "Give third year math courses."
204+
Output: "What are some 3rd year math courses?"
205+
206+
User: "What breadth requirement does CSCC01 satisfy?"
207+
Output: "What breadth requirement does code: CSCC01 satisfy?"
208+
209+
`,
197210
},
198211
];
199212

@@ -227,6 +240,69 @@ async function reformulateQuery(
227240
}
228241
}
229242

243+
// Determines whether to apply metadata filtering based on user query.
244+
function includeFilters(query: string) {
245+
const lowerQuery = query.toLocaleLowerCase();
246+
const relaventBreadthRequirements: string[] = [];
247+
const relaventYearLevels: string[] = [];
248+
249+
Object.entries(BREADTH_REQUIREMENT_KEYWORDS).forEach(
250+
([namespace, keywords]) => {
251+
if (keywords.some((keyword) => lowerQuery.includes(keyword))) {
252+
relaventBreadthRequirements.push(convertBreadthRequirement(namespace));
253+
}
254+
},
255+
);
256+
257+
Object.entries(YEAR_LEVEL_KEYWORDS).forEach(([namespace, keywords]) => {
258+
if (keywords.some((keyword) => lowerQuery.includes(keyword))) {
259+
relaventYearLevels.push(convertYearLevel(namespace));
260+
}
261+
});
262+
263+
let filter = {};
264+
if (relaventBreadthRequirements.length > 0 && relaventYearLevels.length > 0) {
265+
filter = {
266+
$and: [
267+
{
268+
$or: relaventBreadthRequirements.map((req) => ({
269+
breadth_requirement: { $eq: req },
270+
})),
271+
},
272+
{
273+
$or: relaventYearLevels.map((yl) => ({ year_level: { $eq: yl } })),
274+
},
275+
],
276+
};
277+
} else if (relaventBreadthRequirements.length > 0) {
278+
filter = {
279+
$or: relaventBreadthRequirements.map((req) => ({
280+
breadth_requirement: { $eq: req },
281+
})),
282+
};
283+
} else if (relaventYearLevels.length > 0) {
284+
filter = {
285+
$or: relaventYearLevels.map((yl) => ({ year_level: { $eq: yl } })),
286+
};
287+
}
288+
return filter;
289+
}
290+
291+
/**
292+
* @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
293+
*
294+
* @param {Request} req - The Express request object, containing:
295+
* @param {Object[]} req.body.messages - Array of message objects representing the conversation history.
296+
* @param {string} req.body.messages[].role - The role of the message sender (e.g., "user", "assistant").
297+
* @param {Object[]} req.body.messages[].content - An array containing message content objects.
298+
* @param {string} req.body.messages[].content[].text - The actual text of the message.
299+
*
300+
* @param {Response} res - The Express response object used to stream the generated response.
301+
*
302+
* @returns {void} Responds with a streamed text response of the AI output
303+
*
304+
* @throws {Error} If query reformulation or knowledge retrieval fails.
305+
*/
230306
export const chat = asyncHandler(async (req: Request, res: Response) => {
231307
const { messages } = req.body;
232308
const latestMessage = messages[messages.length - 1].content[0].text;
@@ -258,11 +334,15 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
258334
)}`,
259335
);
260336

337+
const filters = includeFilters(reformulatedQuery);
338+
// console.log("Filters: ", JSON.stringify(filters))
339+
261340
// Search only relevant namespaces
262341
const searchResults = await searchSelectedNamespaces(
263342
reformulatedQuery,
264343
3,
265344
relevantNamespaces,
345+
Object.keys(filters).length === 0 ? undefined : filters,
266346
);
267347
// console.log("Search Results: ", searchResults);
268348

@@ -274,7 +354,7 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
274354
console.log("Query does not require knowledge retrieval, skipping search");
275355
}
276356

277-
// console.log("CONTEXT: ", context);
357+
console.log("CONTEXT: ", context);
278358

279359
const result = streamText({
280360
model: openai("gpt-4o-mini"),

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@ 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+
*/
711
aiRouter.post("/chat", authRouter, chat);
12+
/**
13+
* @route POST /api/ai/test-similarity-search
14+
* @description Test vector database similarity search feature
15+
*/
816
aiRouter.post("/test-similarity-search", testSimilaritySearch);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const convertBreadthRequirement = (code: string) => {
2+
if (code === "ART_LIT_LANG") return "Arts, Literature and Language";
3+
else if (code === "HIS_PHIL_CUL")
4+
return "History, Philosophy and Cultural Studies";
5+
else if (code === "SOCIAL_SCI") return "Social and Behavioral Sciences";
6+
else if (code === "NAT_SCI") return "Natural Sciences";
7+
else if (code === "QUANT") return "Quantitative Reasoning";
8+
else return "";
9+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const convertYearLevel = (code: string) => {
2+
if (code === "first_year") return "1st year";
3+
else if (code === "second_year") return "2nd year";
4+
else if (code === "third_year") return "3rd year";
5+
else if (code === "fourth_year") return "4th year";
6+
else return "";
7+
};

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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";
89

910
console.log("Running embeddings process...");
1011

@@ -37,6 +38,35 @@ async function processCSV(filePath: string, namespace: string) {
3738
});
3839
}
3940

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+
4070
// Generate embeddings for pdfs
4171
async function processPDF(filePath: string, namespace: string) {
4272
const fileName = path.basename(filePath);
@@ -98,6 +128,7 @@ async function processPDF(filePath: string, namespace: string) {
98128
// processCSV("../data/tables/offerings_winter_2026.csv", "offerings")
99129
// processCSV("../data/tables/departments.csv", "departments")
100130
// processCSV("../data/tables/courses_with_year.csv", "courses_v2")
131+
// processCoursesCSV("../data/tables/courses_with_year.csv", "courses_v3");
101132

102133
console.log("embeddings done.");
103134

course-matrix/frontend/src/components/time-picker-hr.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function TimePickerHr({ date, setDate }: TimePickerHrProps) {
6060
</div> */}
6161
<div className="grid gap-1 text-center">
6262
<Label htmlFor="period" className="text-xs">
63-
Period
63+
AM/PM
6464
</Label>
6565
<TimePeriodSelect
6666
period={period}

0 commit comments

Comments
 (0)