Spaces:
Sleeping
Sleeping
Commit ·
cc86d95
1
Parent(s): 5761926
Update src/query.py
Browse files- 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 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def local_retrieve(question, top_k=3, category_filter=None):
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
]
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
"
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
# =========================
|