Skip to content

Commit 3e1c6d0

Browse files
authored
Merge branch 'develop' into ty/scrum-52-timetable-generation
2 parents 50b2265 + e71811e commit 3e1c6d0

File tree

16 files changed

+811
-53
lines changed

16 files changed

+811
-53
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"),

0 commit comments

Comments
 (0)