Commit ·
e9527e6
1
Parent(s): a6aeadf
fix(autocomplete): true context awareness - GPT-2 now adds own candidates + cache key uses 5 words
Browse files- src/app.py +1 -1
- src/nlp/autocomplete/autocomplete_service.py +36 -9
src/app.py
CHANGED
|
@@ -457,7 +457,7 @@ def autocomplete():
|
|
| 457 |
t0 = time.time()
|
| 458 |
suggestions = ac_model.predict(context, n=n)
|
| 459 |
elapsed = int((time.time() - t0) * 1000)
|
| 460 |
-
logger.info(f"[AUTOCOMPLETE] {elapsed}ms | mode={ac_model.get_mode()} | suggestions={suggestions}")
|
| 461 |
|
| 462 |
return jsonify({
|
| 463 |
'suggestions': suggestions,
|
|
|
|
| 457 |
t0 = time.time()
|
| 458 |
suggestions = ac_model.predict(context, n=n)
|
| 459 |
elapsed = int((time.time() - t0) * 1000)
|
| 460 |
+
logger.info(f"[AUTOCOMPLETE] {elapsed}ms | mode={ac_model.get_mode()} | context='{context[:80]}' | suggestions={suggestions}")
|
| 461 |
|
| 462 |
return jsonify({
|
| 463 |
'suggestions': suggestions,
|
src/nlp/autocomplete/autocomplete_service.py
CHANGED
|
@@ -47,9 +47,9 @@ def get_autocomplete_model():
|
|
| 47 |
|
| 48 |
# ─── Cache key helper ─────────────────────────────────────────────────────────
|
| 49 |
def _context_key(context: str) -> str:
|
| 50 |
-
"""
|
| 51 |
words = context.strip().split()
|
| 52 |
-
return " ".join(words[-
|
| 53 |
|
| 54 |
|
| 55 |
# ─── Main Service ─────────────────────────────────────────────────────────────
|
|
@@ -216,7 +216,13 @@ class HybridAutoComplete:
|
|
| 216 |
return [w for w, _ in preds[:n]]
|
| 217 |
|
| 218 |
def _hybrid_predict(self, context: str, n: int = 5) -> list:
|
| 219 |
-
"""Hybrid prediction: bigram + GPT-2 scoring.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
from .autocomplete_rules import merge_similar_predictions, filter_suggestions
|
| 221 |
|
| 222 |
tokens = context.strip().split()
|
|
@@ -225,7 +231,7 @@ class HybridAutoComplete:
|
|
| 225 |
|
| 226 |
last_word = tokens[-1]
|
| 227 |
|
| 228 |
-
# 1. Get bigram candidates
|
| 229 |
stat_candidates = []
|
| 230 |
if last_word in self.bigrams:
|
| 231 |
for w, c in self.bigrams[last_word].items():
|
|
@@ -233,27 +239,48 @@ class HybridAutoComplete:
|
|
| 233 |
continue
|
| 234 |
stat_candidates.append((w, c))
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
if not stat_candidates:
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
return self._bigram_predict(context, n)
|
| 239 |
|
| 240 |
total = sum(c for _, c in stat_candidates)
|
| 241 |
if total == 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
return self._bigram_predict(context, n)
|
| 243 |
|
| 244 |
stat_preds = [(w, c / total) for w, c in stat_candidates]
|
| 245 |
stat_preds.sort(key=lambda x: x[1], reverse=True)
|
| 246 |
stat_preds = merge_similar_predictions(stat_preds, top_k=20)
|
| 247 |
|
| 248 |
-
#
|
| 249 |
-
gpt2_probs = self._gpt2_next_token_probs(context, top_k=50)
|
| 250 |
-
|
| 251 |
-
# 3. Hybrid scoring
|
| 252 |
results = []
|
|
|
|
| 253 |
for w, stat_p in stat_preds:
|
| 254 |
neural_p = gpt2_probs.get(w, 1e-8)
|
| 255 |
score = self.alpha * stat_p + (1 - self.alpha) * neural_p
|
| 256 |
results.append((w, score))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
results.sort(key=lambda x: x[1], reverse=True)
|
| 259 |
results = filter_suggestions(results)
|
|
|
|
| 47 |
|
| 48 |
# ─── Cache key helper ─────────────────────────────────────────────────────────
|
| 49 |
def _context_key(context: str) -> str:
|
| 50 |
+
"""Use last 5 words for cache key — preserves enough context for GPT-2 awareness."""
|
| 51 |
words = context.strip().split()
|
| 52 |
+
return " ".join(words[-5:]) if words else ""
|
| 53 |
|
| 54 |
|
| 55 |
# ─── Main Service ─────────────────────────────────────────────────────────────
|
|
|
|
| 216 |
return [w for w, _ in preds[:n]]
|
| 217 |
|
| 218 |
def _hybrid_predict(self, context: str, n: int = 5) -> list:
|
| 219 |
+
"""Hybrid prediction: bigram + GPT-2 scoring.
|
| 220 |
+
|
| 221 |
+
GPT-2 receives the FULL sentence as context for true context awareness.
|
| 222 |
+
Bigram provides frequency-based candidates from the last word.
|
| 223 |
+
GPT-2's own top predictions are ADDED as candidates so contextually
|
| 224 |
+
appropriate words that bigram doesn't know about can still appear.
|
| 225 |
+
"""
|
| 226 |
from .autocomplete_rules import merge_similar_predictions, filter_suggestions
|
| 227 |
|
| 228 |
tokens = context.strip().split()
|
|
|
|
| 231 |
|
| 232 |
last_word = tokens[-1]
|
| 233 |
|
| 234 |
+
# 1. Get bigram candidates (frequency-based, last word only)
|
| 235 |
stat_candidates = []
|
| 236 |
if last_word in self.bigrams:
|
| 237 |
for w, c in self.bigrams[last_word].items():
|
|
|
|
| 239 |
continue
|
| 240 |
stat_candidates.append((w, c))
|
| 241 |
|
| 242 |
+
# 2. Get GPT-2 next-token probabilities using FULL context
|
| 243 |
+
# GPT-2 sees the entire sentence, not just the last word
|
| 244 |
+
gpt2_probs = self._gpt2_next_token_probs(context, top_k=50)
|
| 245 |
+
|
| 246 |
+
# 3. If no bigram candidates, use GPT-2 predictions directly
|
| 247 |
if not stat_candidates:
|
| 248 |
+
if gpt2_probs:
|
| 249 |
+
# Use GPT-2's own contextual predictions
|
| 250 |
+
gpt2_preds = sorted(gpt2_probs.items(), key=lambda x: x[1], reverse=True)
|
| 251 |
+
gpt2_preds = filter_suggestions(gpt2_preds)
|
| 252 |
+
return [w for w, _ in gpt2_preds[:n]]
|
| 253 |
return self._bigram_predict(context, n)
|
| 254 |
|
| 255 |
total = sum(c for _, c in stat_candidates)
|
| 256 |
if total == 0:
|
| 257 |
+
if gpt2_probs:
|
| 258 |
+
gpt2_preds = sorted(gpt2_probs.items(), key=lambda x: x[1], reverse=True)
|
| 259 |
+
gpt2_preds = filter_suggestions(gpt2_preds)
|
| 260 |
+
return [w for w, _ in gpt2_preds[:n]]
|
| 261 |
return self._bigram_predict(context, n)
|
| 262 |
|
| 263 |
stat_preds = [(w, c / total) for w, c in stat_candidates]
|
| 264 |
stat_preds.sort(key=lambda x: x[1], reverse=True)
|
| 265 |
stat_preds = merge_similar_predictions(stat_preds, top_k=20)
|
| 266 |
|
| 267 |
+
# 4. Hybrid scoring: combine bigram frequency with GPT-2 context score
|
|
|
|
|
|
|
|
|
|
| 268 |
results = []
|
| 269 |
+
seen_words = set()
|
| 270 |
for w, stat_p in stat_preds:
|
| 271 |
neural_p = gpt2_probs.get(w, 1e-8)
|
| 272 |
score = self.alpha * stat_p + (1 - self.alpha) * neural_p
|
| 273 |
results.append((w, score))
|
| 274 |
+
seen_words.add(w)
|
| 275 |
+
|
| 276 |
+
# 5. ADD GPT-2's own top contextual predictions as bonus candidates
|
| 277 |
+
# These are words GPT-2 thinks fit the context but bigram doesn't know
|
| 278 |
+
for w, neural_p in sorted(gpt2_probs.items(), key=lambda x: x[1], reverse=True)[:10]:
|
| 279 |
+
if w not in seen_words and len(w) >= 2:
|
| 280 |
+
# Give these a lower alpha since they have no bigram backing
|
| 281 |
+
score = 0.3 * neural_p # Lower weight, but still contextual
|
| 282 |
+
results.append((w, score))
|
| 283 |
+
seen_words.add(w)
|
| 284 |
|
| 285 |
results.sort(key=lambda x: x[1], reverse=True)
|
| 286 |
results = filter_suggestions(results)
|