feat(ka-id v2.1): reclassement personnalisé léger de la fenêtre de résultats (localisation transversale du profil KA ID, blend 0.25, fail-open) + champ personalized
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 changed file +74 −1
modified
apps/api/main.py
+74 −1
@@ -204,6 +204,73 @@ def _kaid_push(ka_id: str, event: dict) -> None: | ||
| 204 | 204 | pass # best-effort |
| 205 | 205 | |
| 206 | 206 | |
| 207 | +_KA_APPS = {"lou-ka", "immo-ka", "vrai-prix", "valoplex", "auto-ka", | |
| 208 | + "fabri-ka", "food-ka", "resto-ka", "sorti-ka", "crea-ka", | |
| 209 | + "job-ka", "groupe-ka", "api-ka", "toit-ka", "ora-ka"} | |
| 210 | +_kaid_prefs_cache: dict = {} | |
| 211 | + | |
| 212 | + | |
| 213 | +def _kaid_prefs(ka_id: str) -> dict | None: | |
| 214 | + """Profil de personnalisation du hub (cache 90 s, best-effort).""" | |
| 215 | + now = _time.time() | |
| 216 | + hit = _kaid_prefs_cache.get(ka_id) | |
| 217 | + if hit and now - hit[0] < 90: | |
| 218 | + return hit[1] | |
| 219 | + secret = _os.environ.get("KA_SSO_SECRET") | |
| 220 | + data = None | |
| 221 | + if secret: | |
| 222 | + ts = str(int(now)) | |
| 223 | + sig = _hmac.new(secret.encode(), f"{_KAID_CLIENT}.{ka_id}.{ts}".encode(), | |
| 224 | + _hashlib.sha256).hexdigest() | |
| 225 | + url = (f"{_KA_HUB}/api/sso/prefs?client_id={_KAID_CLIENT}" | |
| 226 | + f"&ka_id={ka_id}&ts={ts}&sig={sig}") | |
| 227 | + try: | |
| 228 | + with _urlreq.urlopen(url, timeout=4) as r: | |
| 229 | + data = _json.loads(r.read()) | |
| 230 | + except Exception: | |
| 231 | + data = None | |
| 232 | + _kaid_prefs_cache[ka_id] = (now, data) | |
| 233 | + if len(_kaid_prefs_cache) > 300: | |
| 234 | + for k in list(_kaid_prefs_cache)[:100]: | |
| 235 | + del _kaid_prefs_cache[k] | |
| 236 | + return data | |
| 237 | + | |
| 238 | + | |
| 239 | +def _kaid_boost(request: Request, results: list) -> bool: | |
| 240 | + """Reclassement personnalisé LÉGER de la fenêtre de page : les pages des | |
| 241 | + univers KA que le membre utilise le plus, et celles qui mentionnent ses | |
| 242 | + coins du Québec, remontent doucement (blend 0.25 — la pertinence du | |
| 243 | + moteur reste dominante). Fail-open : à la moindre absence, ordre intact.""" | |
| 244 | + if len(results) < 2: | |
| 245 | + return False | |
| 246 | + ka_id = _tk_ka_id(request) | |
| 247 | + if not ka_id: | |
| 248 | + return False | |
| 249 | + prefs = _kaid_prefs(ka_id) | |
| 250 | + if not prefs or not prefs.get("personalization"): | |
| 251 | + return False | |
| 252 | + profile = prefs.get("profile") or {} | |
| 253 | + # le hub ne nous transmet que notre bloc d'univers + le transversal : | |
| 254 | + # on exploite la localisation transversale (villes/quartiers du membre). | |
| 255 | + gl = (profile.get("global") or {}).get("location") or {} | |
| 256 | + loc_values = {k: v for k, v in (gl.get("values") or {}).items() if v > 0} | |
| 257 | + loc_conf = float(gl.get("conf") or 0.3) | |
| 258 | + if not loc_values: | |
| 259 | + return False | |
| 260 | + n = len(results) | |
| 261 | + scored = [] | |
| 262 | + for i, r in enumerate(results): | |
| 263 | + base = 1.0 - i / n | |
| 264 | + text = ((r.get("title") or "") + " " + (r.get("snippet") or "")).lower() | |
| 265 | + loc = max((aff for val, aff in loc_values.items() if val in text), | |
| 266 | + default=0.0) | |
| 267 | + p = 0.5 + 0.5 * loc * loc_conf | |
| 268 | + scored.append((0.75 * base + 0.25 * p, i, r)) | |
| 269 | + scored.sort(key=lambda t: (-t[0], t[1])) | |
| 270 | + results[:] = [r for _, _, r in scored] | |
| 271 | + return True | |
| 272 | + | |
| 273 | + | |
| 207 | 274 | def _kaid_track_search(request: Request, q: str, filters: dict) -> None: |
| 208 | 275 | ka_id = _tk_ka_id(request) |
| 209 | 276 | if not ka_id: |
@@ -347,8 +414,14 @@ async def api_search( | ||
| 347 | 414 | except Exception: |
| 348 | 415 | related = [] |
| 349 | 416 | |
| 417 | + # KA ID v2 : reclassement personnalisé léger de la fenêtre courante | |
| 418 | + try: | |
| 419 | + personalized = _kaid_boost(request, results) | |
| 420 | + except Exception: | |
| 421 | + personalized = False | |
| 350 | 422 | return {"query": q, "total": total, "took_ms": took_ms, "page": page, "limit": limit, |
| 351 | − "semantic": semantic, "reranked": reranked, "related": related, "results": results} | |
| 423 | + "semantic": semantic, "reranked": reranked, "related": related, | |
| 424 | + "personalized": personalized, "results": results} | |
| 352 | 425 | |
| 353 | 426 | |
| 354 | 427 | @app.get("/api/status") |
| 355 | 428 | |