RwanAshraf commited on
Commit
cc86d95
·
1 Parent(s): 5761926

Update src/query.py

Browse files
Files changed (1) hide show
  1. src/query.py +128 -46
src/query.py CHANGED
@@ -6,16 +6,30 @@ import json
6
  # =========================
7
 
8
  def detect_category(question: str) -> str:
9
- q = question.lower()
10
-
11
- if "fee" in q or "cost" in q or "credit hour" in q:
12
- return "Fees"
13
- if "apply" in q or "admission" in q:
14
- return "Admissions"
15
- if "course" in q or "program" in q:
16
- return "Academics"
17
-
18
- return "General"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  # =========================
@@ -23,45 +37,113 @@ def detect_category(question: str) -> str:
23
  # =========================
24
 
25
  def retrieve_with_chroma(query_embedding, top_k=5, category_filter=None):
26
- """
27
- This function is called by app.py.
28
- Replace its body with your real ChromaDB logic if needed.
29
- """
30
-
31
- # Example fallback data (SAFE DEFAULT)
32
- docs = [
33
- "The cost per credit hour is 1500 EGP for undergraduate programs."
34
- ]
35
-
36
- metas = [
37
- {
38
- "qa_id": 1,
39
- "category": "Fees",
40
- "source": "Admissions Handbook"
41
- }
42
- ]
43
-
44
- return docs[:top_k], metas[:top_k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
 
47
  def local_retrieve(question, top_k=3, category_filter=None):
48
- """
49
- Backup keyword-based retrieval
50
- """
51
-
52
- docs = [
53
- "Each credit hour costs 1500 EGP according to university fee regulations."
54
- ]
55
-
56
- metas = [
57
- {
58
- "qa_id": 2,
59
- "category": "Fees",
60
- "source": "Local FAQ"
61
- }
62
- ]
63
-
64
- return docs[:top_k], metas[:top_k]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
 
67
  # =========================
 
6
  # =========================
7
 
8
  def detect_category(question: str) -> str:
9
+ """Detect the category of a question based on keywords."""
10
+ query_lower = query.lower()
11
+
12
+ # Category keywords based on Categories.txt
13
+ category_keywords = {
14
+ "Admissions": ["apply", "admission", "accept", "requirements", "application", "enroll"],
15
+ "Fees": ["fee", "tuition", "cost", "payment", "credit", "price", "pay", "refund"],
16
+ "Academics": ["gpa", "grades", "scores", "grade", "cgpa", "dean"],
17
+ "Academic Advising": ["advisor", "track", "course", "major", "register", "summer course"],
18
+ "IT & Systems": ["portal", "moodle", "login", "system", "technical", "support"],
19
+ "Emails": ["email", "gmail", "outlook", "mail", "inbox", "address", "contact email"],
20
+ }
21
+
22
+ # Count keyword matches for each category
23
+ category_scores = {}
24
+ for category, keywords in category_keywords.items():
25
+ score = sum(1 for keyword in keywords if keyword in query_lower)
26
+ if score > 0:
27
+ category_scores[category] = score
28
+
29
+ # Return category with highest score, or None if no matches
30
+ if category_scores:
31
+ return max(category_scores, key=category_scores.get)
32
+ return None
33
 
34
 
35
  # =========================
 
37
  # =========================
38
 
39
  def retrieve_with_chroma(query_embedding, top_k=5, category_filter=None):
40
+ """Retrieve relevant Q&A pairs from Chroma with optional category filtering."""
41
+ try:
42
+ import chromadb
43
+
44
+ # Prefer persistent client; fall back quietly
45
+ client = None
46
+ try:
47
+ client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIR)
48
+ except Exception:
49
+ try:
50
+ from chromadb.config import Settings
51
+ client = chromadb.Client(Settings(persist_directory=CHROMA_PERSIST_DIR))
52
+ except Exception:
53
+ try:
54
+ client = chromadb.EphemeralClient()
55
+ except Exception:
56
+ return [], []
57
+
58
+ # Get qa_knowledge collection (stores JSON Q&A data)
59
+ col = None
60
+ try:
61
+ col = client.get_collection("qa_knowledge")
62
+ except Exception:
63
+ return [], []
64
+
65
+ # Apply category filter if provided (e.g., only Fees, Admissions, etc.)
66
+ if category_filter:
67
+ results = col.query(
68
+ query_embeddings=[query_embedding],
69
+ n_results=top_k,
70
+ where={"category": category_filter}
71
+ )
72
+ else:
73
+ results = col.query(
74
+ query_embeddings=[query_embedding],
75
+ n_results=top_k
76
+ )
77
+
78
+ docs = results.get("documents", [[]])[0]
79
+ metas = results.get("metadatas", [[]])[0]
80
+ return docs, metas
81
+ except Exception as e:
82
+ # If there's an error, print it for debugging and return empty
83
+ print(f"ChromaDB error: {e}")
84
+ return [], []
85
 
86
 
87
  def local_retrieve(question, top_k=3, category_filter=None):
88
+ """Keyword-overlap retrieval over JSON data on disk with optional category filtering."""
89
+ from src.utils import chunk_text, list_pdfs, load_pdf_text
90
+
91
+ cwd = os.getcwd()
92
+ json_path = os.path.join(cwd, "data.json")
93
+ all_chunks = []
94
+ metadatas = []
95
+
96
+ if os.path.exists(json_path):
97
+ try:
98
+ with open(json_path, "r", encoding="utf-8") as f:
99
+ data = json.load(f)
100
+
101
+ items = []
102
+ for entry in data:
103
+ category = entry.get("category", "General")
104
+
105
+ # Apply category filter if provided
106
+ if category_filter and category != category_filter:
107
+ continue
108
+
109
+ qa_id = entry.get("id", "")
110
+ qtext = str(entry.get("question", "")).strip()
111
+ atext = str(entry.get("answer", "")).strip()
112
+
113
+ if qtext or atext:
114
+ combined = f"Question: {qtext}\nAnswer: {atext}"
115
+ items.append(combined)
116
+ # Store metadata with each item
117
+ metadatas.append({
118
+ "source": json_path,
119
+ "qa_id": str(qa_id),
120
+ "category": category,
121
+ "question": qtext,
122
+ "answer": atext
123
+ })
124
+ except Exception as e:
125
+ print(f"Error reading data.json: {e}")
126
+ items = []
127
+ metadatas = []
128
+ else:
129
+ items = []
130
+ metadatas = []
131
+
132
+ # Use items directly (already have metadata)
133
+ if not items:
134
+ return [], []
135
+
136
+ # naive keyword overlap scoring
137
+ q_tokens = set(w.lower() for w in query.split())
138
+ scores = []
139
+ for item in items:
140
+ item_tokens = set(w.lower().strip('.,()\"') for w in item.split())
141
+ scores.append(len(q_tokens & item_tokens))
142
+
143
+ ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
144
+ top = [items[i] for i, s in ranked[:top_k] if s > 0]
145
+ top_meta = [metadatas[i] for i, s in ranked[:top_k] if s > 0]
146
+ return top, top_meta
147
 
148
 
149
  # =========================