ewannhugging commited on
Commit
09757f0
·
1 Parent(s): bf61a7b

Last update

Browse files
Files changed (2) hide show
  1. app.py +151 -29
  2. prompts/router_prompt.txt +33 -0
app.py CHANGED
@@ -89,17 +89,22 @@ ENABLE_BM25 = True
89
  RRF_K = 60
90
 
91
  # ---------------------------------------------------------------------------
92
- # Configuration par type de question (auto-détectée à chaque requête)
93
  # ---------------------------------------------------------------------------
94
- # QCM : la réponse est l'une des options → peu de contexte suffit
95
- QCM_TOP_K_RETRIEVAL = 10
96
- QCM_TOP_K_RERANKED = 2
97
- QCM_LLM_MODEL = "gpt-5.mini" # None = utiliser LLM_MODEL_NAME (défini après chargement config)
98
 
99
- # OPEN : réponse développée → plus de contexte et modèle plus puissant si disponible
100
  OPEN_TOP_K_RETRIEVAL = 20
101
- OPEN_TOP_K_RERANKED = 6
102
- OPEN_LLM_MODEL = "gpt-5.1" # None = utiliser LLM_MODEL_NAME
 
 
 
 
 
 
 
103
 
104
  # Settings loaded from data/config.json (runtime config).
105
  # Falls back to the root config.json which serves as the example/template.
@@ -133,6 +138,9 @@ if not AZURE_API_KEY:
133
  _PROMPT_TEMPLATE_PATH = Path(__file__).parent / "prompts" / "rag_prompt.txt"
134
  RAG_PROMPT_TEMPLATE = _PROMPT_TEMPLATE_PATH.read_text(encoding="utf-8")
135
 
 
 
 
136
  # ---------------------------------------------------------------------------
137
  # Step 1: Embedding via Azure OpenAI
138
  # ---------------------------------------------------------------------------
@@ -318,6 +326,103 @@ def detect_qcm(query: str) -> bool:
318
  return bool(re.search(r'\b[A-D][).]\s', query))
319
 
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  def retrieve_relevant_context(
322
  query: str,
323
  top_k: int = TOP_K_RETRIEVAL,
@@ -418,35 +523,48 @@ def build_rag_prompt(query: str, contexts: list[dict]) -> str:
418
 
419
  def rag_query(
420
  query: str,
421
- top_k: Optional[int] = None, # None → auto-détecté selon le type de question
422
  *,
423
  temperature: float = LLM_TEMPERATURE,
424
  max_tokens: int = LLM_MAX_TOKENS,
425
  enable_reranking: bool = ENABLE_RERANKING,
426
- top_k_reranked: Optional[int] = None, # None → auto-détecté selon le type de question
427
- model: Optional[str] = None, # None → auto-détecté selon le type de question
428
  enable_bm25: bool = ENABLE_BM25,
 
429
  ) -> dict:
430
  """
431
- End-to-end RAG pipeline: Query → Retrieve → Generate.
432
 
433
- top_k, top_k_reranked et model sont auto-détectés selon le type de question (QCM vs OPEN)
434
- si non fournis explicitement.
 
435
  """
436
  start_time = time.perf_counter()
437
 
438
- # Auto-détection du type de question et application des paramètres adaptés
439
- is_qcm = detect_qcm(query)
440
- question_type = "QCM" if is_qcm else "OPEN"
 
 
 
 
 
 
441
 
 
442
  if top_k is None:
443
- top_k = QCM_TOP_K_RETRIEVAL if is_qcm else OPEN_TOP_K_RETRIEVAL
444
  if top_k_reranked is None:
445
- top_k_reranked = QCM_TOP_K_RERANKED if is_qcm else OPEN_TOP_K_RERANKED
446
  if model is None:
447
- model = (QCM_LLM_MODEL or LLM_MODEL_NAME) if is_qcm else (OPEN_LLM_MODEL or LLM_MODEL_NAME)
448
 
449
- logger.info(f"Question type: {question_type} → top_k={top_k}, top_k_reranked={top_k_reranked}, model={model}")
 
 
 
 
450
 
451
  # Step 1: Retrieve relevant document chunks
452
  contexts = retrieve_relevant_context(
@@ -510,16 +628,20 @@ def rag_query(
510
  "answer": answer,
511
  "sources": [{"source": ctx["source"], "score": ctx["similarity_score"], "ref_text": ctx["text"]} for ctx in contexts],
512
  "explanation": explanation,
513
- "question_type": question_type,
514
- "model_used": model,
 
 
 
 
515
  "top_k_reranked_used": top_k_reranked,
516
- "total_token": total_token,
517
- "prompt_tokens": tokens["prompt"],
518
  "completion_tokens": tokens["completion"],
519
- "cached_tokens": tokens["cached"],
520
- "co2_grams": llm_result["co2_grams"],
521
- "energy_kwh": llm_result["energy_kwh"],
522
- "run_time_in_ms": elapsed_ms,
523
  }
524
 
525
 
 
89
  RRF_K = 60
90
 
91
  # ---------------------------------------------------------------------------
92
+ # Configuration par type de question — valeurs de fallback si le routeur échoue
93
  # ---------------------------------------------------------------------------
94
+ QCM_TOP_K_RETRIEVAL = 12
95
+ QCM_TOP_K_RERANKED = 2
96
+ QCM_LLM_MODEL = "gpt-5.mini"
 
97
 
 
98
  OPEN_TOP_K_RETRIEVAL = 20
99
+ OPEN_TOP_K_RERANKED = 4
100
+ OPEN_LLM_MODEL = "gpt-5.1"
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Router configuration (routage intelligent avant retrieval)
104
+ # ---------------------------------------------------------------------------
105
+ # Mettre à False pour désactiver le routeur et revenir aux heuristiques simples
106
+ ENABLE_ROUTER = True
107
+ ROUTER_MODEL = "gpt-5.mini" # modèle rapide/peu coûteux pour le seul routage
108
 
109
  # Settings loaded from data/config.json (runtime config).
110
  # Falls back to the root config.json which serves as the example/template.
 
138
  _PROMPT_TEMPLATE_PATH = Path(__file__).parent / "prompts" / "rag_prompt.txt"
139
  RAG_PROMPT_TEMPLATE = _PROMPT_TEMPLATE_PATH.read_text(encoding="utf-8")
140
 
141
+ _ROUTER_PROMPT_PATH = Path(__file__).parent / "prompts" / "router_prompt.txt"
142
+ ROUTER_PROMPT = _ROUTER_PROMPT_PATH.read_text(encoding="utf-8")
143
+
144
  # ---------------------------------------------------------------------------
145
  # Step 1: Embedding via Azure OpenAI
146
  # ---------------------------------------------------------------------------
 
326
  return bool(re.search(r'\b[A-D][).]\s', query))
327
 
328
 
329
+ def _fallback_routing(query: str) -> dict:
330
+ """Heuristiques de routage si le routeur LLM est désactivé ou en erreur."""
331
+ if detect_qcm(query):
332
+ return {
333
+ "question_type": "qcm", "answer_type": "factual",
334
+ "breadth": "narrow", "difficulty": "low",
335
+ "top_k_retrieval": QCM_TOP_K_RETRIEVAL,
336
+ "top_k_reranked": QCM_TOP_K_RERANKED,
337
+ "model": QCM_LLM_MODEL or LLM_MODEL_NAME,
338
+ }
339
+ q = query.lower()
340
+ has_comparison = any(w in q for w in ["compar", "différence", "versus", "vs ", "contrast"])
341
+ has_synthesis = any(w in q for w in ["enjeux", "limites", "défis", "implications", "impact", "challenges"])
342
+ has_method = any(w in q for w in ["étape", "processus", "comment", "décrire", "expliquer", "démarche", "how to"])
343
+ word_count = len(query.split())
344
+
345
+ if has_comparison:
346
+ return {
347
+ "question_type": "open", "answer_type": "comparison",
348
+ "breadth": "broad", "difficulty": "high",
349
+ "top_k_retrieval": 45, "top_k_reranked": 7,
350
+ "model": OPEN_LLM_MODEL or LLM_MODEL_NAME,
351
+ }
352
+ if has_synthesis:
353
+ return {
354
+ "question_type": "open", "answer_type": "synthesis",
355
+ "breadth": "broad", "difficulty": "high",
356
+ "top_k_retrieval": 45, "top_k_reranked": 7,
357
+ "model": OPEN_LLM_MODEL or LLM_MODEL_NAME,
358
+ }
359
+ if has_method or word_count > 15:
360
+ return {
361
+ "question_type": "open", "answer_type": "method",
362
+ "breadth": "medium", "difficulty": "medium",
363
+ "top_k_retrieval": 35, "top_k_reranked": 5,
364
+ "model": OPEN_LLM_MODEL or LLM_MODEL_NAME,
365
+ }
366
+ return {
367
+ "question_type": "open", "answer_type": "factual",
368
+ "breadth": "narrow", "difficulty": "low",
369
+ "top_k_retrieval": 20, "top_k_reranked": 3,
370
+ "model": QCM_LLM_MODEL or LLM_MODEL_NAME,
371
+ }
372
+
373
+
374
+ def _call_router_llm(query: str) -> str:
375
+ """Appel direct à GPT-5-mini pour classer la question (sans ecologits)."""
376
+ headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
377
+ payload = {
378
+ "model": ROUTER_MODEL,
379
+ "messages": [
380
+ {"role": "system", "content": ROUTER_PROMPT},
381
+ {"role": "user", "content": query},
382
+ ],
383
+ "max_completion_tokens": 200,
384
+ "temperature": 0.0,
385
+ }
386
+ resp = http_requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload, timeout=30)
387
+ resp.raise_for_status()
388
+ return resp.json()["choices"][0]["message"]["content"]
389
+
390
+
391
+ def route_question(query: str) -> dict:
392
+ """
393
+ Classe la question et retourne les paramètres RAG adaptés.
394
+
395
+ Retourne un dict avec :
396
+ question_type, answer_type, breadth, difficulty,
397
+ top_k_retrieval, top_k_reranked, model, router_used
398
+ """
399
+ if not ENABLE_ROUTER:
400
+ return {**_fallback_routing(query), "router_used": False}
401
+
402
+ try:
403
+ raw = _call_router_llm(query).strip()
404
+ if raw.startswith("```"):
405
+ raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
406
+ routing = json.loads(raw)
407
+ required = {"question_type", "answer_type", "breadth", "difficulty",
408
+ "top_k_retrieval", "top_k_reranked", "model"}
409
+ if not required.issubset(routing.keys()):
410
+ raise ValueError(f"Champs manquants : {required - routing.keys()}")
411
+ routing["top_k_retrieval"] = int(routing["top_k_retrieval"])
412
+ routing["top_k_reranked"] = int(routing["top_k_reranked"])
413
+ routing["router_used"] = True
414
+ logger.info(
415
+ f"Router → type={routing['question_type']} answer={routing['answer_type']} "
416
+ f"breadth={routing['breadth']} diff={routing['difficulty']} "
417
+ f"top_k={routing['top_k_retrieval']} reranked={routing['top_k_reranked']} "
418
+ f"model={routing['model']}"
419
+ )
420
+ return routing
421
+ except Exception as e:
422
+ logger.warning(f"Routeur LLM échoué ({e}) — fallback heuristiques.")
423
+ return {**_fallback_routing(query), "router_used": False}
424
+
425
+
426
  def retrieve_relevant_context(
427
  query: str,
428
  top_k: int = TOP_K_RETRIEVAL,
 
523
 
524
  def rag_query(
525
  query: str,
526
+ top_k: Optional[int] = None,
527
  *,
528
  temperature: float = LLM_TEMPERATURE,
529
  max_tokens: int = LLM_MAX_TOKENS,
530
  enable_reranking: bool = ENABLE_RERANKING,
531
+ top_k_reranked: Optional[int] = None,
532
+ model: Optional[str] = None,
533
  enable_bm25: bool = ENABLE_BM25,
534
+ enable_router: bool = ENABLE_ROUTER,
535
  ) -> dict:
536
  """
537
+ End-to-end RAG pipeline: Query → Route → Retrieve → Generate.
538
 
539
+ Le routeur LLM détermine automatiquement top_k, top_k_reranked et model
540
+ selon le type, la nature et la complexité de la question.
541
+ Les valeurs passées explicitement priment toujours sur le routeur.
542
  """
543
  start_time = time.perf_counter()
544
 
545
+ # Routage intelligent (ou fallback heuristique si désactivé / en erreur)
546
+ _router_enabled = enable_router and ENABLE_ROUTER
547
+ routing = route_question(query) if _router_enabled else {**_fallback_routing(query), "router_used": False}
548
+
549
+ question_type = routing["question_type"].upper()
550
+ answer_type = routing.get("answer_type", "factual")
551
+ breadth = routing.get("breadth", "medium")
552
+ difficulty = routing.get("difficulty", "medium")
553
+ router_used = routing.get("router_used", False)
554
 
555
+ # Les valeurs explicites priment sur le routeur
556
  if top_k is None:
557
+ top_k = routing["top_k_retrieval"]
558
  if top_k_reranked is None:
559
+ top_k_reranked = routing["top_k_reranked"]
560
  if model is None:
561
+ model = routing["model"]
562
 
563
+ logger.info(
564
+ f"Routing → type={question_type} answer={answer_type} breadth={breadth} "
565
+ f"difficulty={difficulty} | top_k={top_k} reranked={top_k_reranked} "
566
+ f"model={model} router={'LLM' if router_used else 'heuristic'}"
567
+ )
568
 
569
  # Step 1: Retrieve relevant document chunks
570
  contexts = retrieve_relevant_context(
 
628
  "answer": answer,
629
  "sources": [{"source": ctx["source"], "score": ctx["similarity_score"], "ref_text": ctx["text"]} for ctx in contexts],
630
  "explanation": explanation,
631
+ "question_type": question_type,
632
+ "answer_type": answer_type,
633
+ "breadth": breadth,
634
+ "difficulty": difficulty,
635
+ "router_used": router_used,
636
+ "model_used": model,
637
  "top_k_reranked_used": top_k_reranked,
638
+ "total_token": total_token,
639
+ "prompt_tokens": tokens["prompt"],
640
  "completion_tokens": tokens["completion"],
641
+ "cached_tokens": tokens["cached"],
642
+ "co2_grams": llm_result["co2_grams"],
643
+ "energy_kwh": llm_result["energy_kwh"],
644
+ "run_time_in_ms": elapsed_ms,
645
  }
646
 
647
 
prompts/router_prompt.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are a routing classifier for a RAG system. Your only task is to analyze the question and return a JSON object.
2
+
3
+ RULES:
4
+ - Never answer the question.
5
+ - Never explain your reasoning.
6
+ - Return ONLY valid JSON, nothing else.
7
+
8
+ CLASSIFICATION DIMENSIONS:
9
+ - question_type: "qcm" if the question contains multiple-choice options such as A), B), C), D), else "open"
10
+ - answer_type:
11
+ "factual" → definition, precise fact, or single concept
12
+ "method" → procedure, steps, process, or methodology
13
+ "comparison" → compare two or more approaches, models, or techniques
14
+ "synthesis" → combine multiple concepts, challenges, implications, or limitations
15
+ - breadth:
16
+ "narrow" → single concept only
17
+ "medium" → one concept with several aspects
18
+ "broad" → multiple sub-topics, steps, challenges, or hypotheses
19
+ - difficulty: "low", "medium", or "high"
20
+
21
+ PARAMETER SELECTION TABLE (apply the first matching row):
22
+ | question_type | answer_type | breadth | top_k_retrieval | top_k_reranked | model |
23
+ |---------------|----------------------|----------------|-----------------|----------------|------------|
24
+ | qcm | any | any | 12 | 2 | gpt-5.mini |
25
+ | open | factual | narrow | 15 | 3 | gpt-5.mini |
26
+ | open | factual | medium or broad| 20 | 3 | gpt-5.mini |
27
+ | open | method | narrow or medium| 35 | 5 | gpt-5.1 |
28
+ | open | method | broad | 40 | 6 | gpt-5.1 |
29
+ | open | comparison | any | 45 | 7 | gpt-5.1 |
30
+ | open | synthesis | any | 45 | 7 | gpt-5.1 |
31
+
32
+ Return ONLY the following JSON (no markdown fences, no extra text):
33
+ {"question_type":"...","answer_type":"...","breadth":"...","difficulty":"...","top_k_retrieval":0,"top_k_reranked":0,"model":"..."}