lojol469-cmd commited on
Commit
f3a56a5
·
1 Parent(s): b54b689

Initial commit: Kibali AI with RTX 5090 Blackwell support and CUDA 13.0 Nightly

Browse files
Files changed (5) hide show
  1. Dockerfile +21 -11
  2. main.py +8 -3
  3. requirements.txt +5 -8
  4. tools/todo.py +24 -156
  5. tools/web.py +7 -8
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- # --- STAGE 1 : Build du Frontend (Vite) ---
2
  FROM node:18-alpine AS build-frontend
3
  WORKDIR /app/frontend
4
  COPY kibali-ui/package*.json ./
@@ -6,31 +6,41 @@ RUN npm install
6
  COPY kibali-ui/ ./
7
  RUN npm run build
8
 
9
- # --- STAGE 2 : Backend + Serveur Statique ---
10
- FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
 
11
  WORKDIR /app
12
 
13
- # Installation de Python
 
14
  RUN apt-get update && apt-get install -y \
15
  python3-pip \
16
  python3-dev \
 
 
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
- # Installation de PyTorch
20
- RUN pip3 install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
 
 
21
 
22
- # Installation des dépendances
23
  COPY requirements.txt .
24
  RUN pip3 install --no-cache-dir -r requirements.txt
25
 
26
- # On récupère le dossier 'dist' de Vite et on le renomme 'static'
27
- COPY --from=build-frontend /app/frontend/dist ./static
28
 
29
- # Copie du code Python
30
  COPY . .
31
 
 
 
 
32
  ENV PYTHONUNBUFFERED=1
 
 
33
  EXPOSE 8000
34
 
35
- # Commande corrigée pour Ubuntu (python3)
36
  CMD ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
 
1
+ # --- STAGE 1 : Build du Frontend ---
2
  FROM node:18-alpine AS build-frontend
3
  WORKDIR /app/frontend
4
  COPY kibali-ui/package*.json ./
 
6
  COPY kibali-ui/ ./
7
  RUN npm run build
8
 
9
+ # --- STAGE 2 : Backend (Base NVIDIA Blackwell Compatible) ---
10
+ # On utilise une base 12.6 qui supporte les drivers de la série 50
11
+ FROM nvidia/cuda:12.6.1-runtime-ubuntu22.04
12
  WORKDIR /app
13
 
14
+ ENV DEBIAN_FRONTEND=noninteractive
15
+
16
  RUN apt-get update && apt-get install -y \
17
  python3-pip \
18
  python3-dev \
19
+ libgomp1 \
20
+ git \
21
  && rm -rf /var/lib/apt/lists/*
22
 
23
+ # INSTALLATION PYTORCH NIGHTLY CUDA 13.0
24
+ # C'est ici qu'on débloque le support sm_120
25
+ RUN pip3 install --no-cache-dir --upgrade pip
26
+ RUN pip3 install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu130
27
 
28
+ # Installation du reste des dépendances
29
  COPY requirements.txt .
30
  RUN pip3 install --no-cache-dir -r requirements.txt
31
 
32
+ # On force une version récente de transformers pour le tokenizer Blackwell
33
+ RUN pip3 install --upgrade transformers accelerate bitsandbytes
34
 
35
+ COPY --from=build-frontend /app/frontend/dist ./static
36
  COPY . .
37
 
38
+ RUN mkdir -p /app/model_cache
39
+
40
+ ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH
41
  ENV PYTHONUNBUFFERED=1
42
+ ENV MODEL_PATH=/app/model_cache
43
+
44
  EXPOSE 8000
45
 
 
46
  CMD ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
main.py CHANGED
@@ -55,11 +55,16 @@ app.add_middleware(
55
  )
56
 
57
  # --- CHARGEMENT DES MODÈLES ---
58
- MODEL_PATH = "/home/belikan/geoscan/agent_kibali/model_cache"
 
 
59
  logger.info("Chargement du modèle d'embedding...")
60
- embed_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
 
 
61
  logger.info("Chargement du tokenizer et du modèle LLM...")
62
- tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, local_files_only=True)
 
63
  if tokenizer.pad_token is None:
64
  tokenizer.pad_token = tokenizer.eos_token
65
 
 
55
  )
56
 
57
  # --- CHARGEMENT DES MODÈLES ---
58
+ MODEL_PATH = os.getenv("MODEL_PATH", "./model_cache")
59
+ logger.info(f"Utilisation du chemin modèle : {MODEL_PATH}")
60
+
61
  logger.info("Chargement du modèle d'embedding...")
62
+ # Utilisation du cache_folder pour que SentenceTransformer stocke aussi dans le volume partagé
63
+ embed_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2', cache_folder=MODEL_PATH)
64
+
65
  logger.info("Chargement du tokenizer et du modèle LLM...")
66
+ # Suppression de local_files_only=True pour permettre la compatibilité initiale avec nouvelles architectures GPU
67
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
68
  if tokenizer.pad_token is None:
69
  tokenizer.pad_token = tokenizer.eos_token
70
 
requirements.txt CHANGED
@@ -1,9 +1,7 @@
1
- # --- Core IA (Versions Stables) ---
2
- # Note: On ne met pas de version figée pour torch ici,
3
- # car on l'installe via l'URL spécifique dans le Dockerfile.
4
  transformers==4.41.2
5
- bitsandbytes>=0.41.0
6
- accelerate
7
  sentence-transformers
8
  faiss-gpu
9
 
@@ -14,9 +12,8 @@ pydantic
14
  python-multipart
15
 
16
  # --- Outils & Data ---
 
17
  pypdf>=3.0.0
18
  numpy<2.0.0
19
- folium
20
  duckduckgo-search
21
- huggingface_hub==0.23.4
22
- spaces
 
1
+ # --- Core IA ---
 
 
2
  transformers==4.41.2
3
+ bitsandbytes>=0.43.0
4
+ accelerate>=0.30.0
5
  sentence-transformers
6
  faiss-gpu
7
 
 
12
  python-multipart
13
 
14
  # --- Outils & Data ---
15
+ tavily-python
16
  pypdf>=3.0.0
17
  numpy<2.0.0
 
18
  duckduckgo-search
19
+ huggingface_hub
 
tools/todo.py CHANGED
@@ -1,7 +1,10 @@
1
- import streamlit as st
2
  import time
3
  from typing import List, Optional
4
  import re
 
 
 
 
5
 
6
  def analyze_query_type(prompt: str) -> dict:
7
  """Analyse le type de requête pour adapter la stratégie de réflexion"""
@@ -17,36 +20,30 @@ def analyze_query_type(prompt: str) -> dict:
17
  "geographical": False
18
  }
19
 
20
- # Détection de questions temporelles
21
  temporal_keywords = ["aujourd'hui", "maintenant", "récent", "actuel", "dernier", "2024", "2025"]
22
  if any(kw in prompt_lower for kw in temporal_keywords):
23
  analysis["temporal"] = True
24
  analysis["needs_web"] = True
25
 
26
- # Détection géographique
27
  geo_keywords = ["gabon", "libreville", "port-gentil", "franceville", "oyem", "où", "localisation"]
28
  if any(kw in prompt_lower for kw in geo_keywords):
29
  analysis["geographical"] = True
30
 
31
- # Détection de questions sur documents
32
  doc_keywords = ["selon le document", "d'après le pdf", "dans le fichier", "uploadé"]
33
  if any(kw in prompt_lower for kw in doc_keywords):
34
  analysis["needs_docs"] = True
35
  analysis["type"] = "document_query"
36
 
37
- # Détection de continuation de conversation
38
  continuation_keywords = ["ils", "elles", "lui", "leur", "donc", "alors", "ensuite", "aussi", "également"]
39
  if any(kw in prompt_lower for kw in continuation_keywords) or len(prompt.split()) < 5:
40
  analysis["needs_memory"] = True
41
  analysis["type"] = "continuation"
42
 
43
- # Détection de complexité
44
- if len(prompt.split()) > 15 or "?" in prompt and prompt.count("?") > 1:
45
  analysis["complexity"] = "complex"
46
- elif "pourquoi" in prompt_lower or "comment" in prompt_lower or "expliquer" in prompt_lower:
47
  analysis["complexity"] = "medium"
48
 
49
- # Questions nécessitant le web
50
  web_keywords = ["actualité", "news", "prix", "cours", "météo", "horaire"]
51
  if any(kw in prompt_lower for kw in web_keywords):
52
  analysis["needs_web"] = True
@@ -57,71 +54,44 @@ def analyze_query_type(prompt: str) -> dict:
57
  def detect_subject_shift(prompt: str, current_subject: str, subject_keywords: List[str]) -> dict:
58
  """Détecte un changement de sujet et évalue la force du changement"""
59
  if not current_subject or not subject_keywords:
60
- return {
61
- "shift_detected": False,
62
- "shift_strength": 0.0,
63
- "new_subject_detected": True,
64
- "reason": "Premier message ou pas de sujet actuel"
65
- }
66
 
67
  prompt_lower = prompt.lower()
68
-
69
- # Calcul de l'overlap des mots-clés
70
  prompt_words = set(re.findall(r'\b\w{4,}\b', prompt_lower))
71
  keyword_overlap = len(prompt_words.intersection(set(subject_keywords)))
72
  overlap_ratio = keyword_overlap / max(len(subject_keywords), 1)
73
 
74
- # Détection de marqueurs de changement de sujet
75
  shift_markers = ["maintenant", "sinon", "autre chose", "parlons de", "passons à", "nouveau sujet"]
76
  has_shift_marker = any(marker in prompt_lower for marker in shift_markers)
77
 
78
- # Calcul de la force du changement
79
  shift_strength = 0.0
80
- if overlap_ratio < 0.2:
81
- shift_strength += 0.5
82
- if has_shift_marker:
83
- shift_strength += 0.3
84
- if len(prompt_words) > 5 and keyword_overlap == 0:
85
- shift_strength += 0.2
86
-
87
- shift_detected = shift_strength > 0.4
88
 
89
  return {
90
- "shift_detected": shift_detected,
91
  "shift_strength": shift_strength,
92
  "new_subject_detected": shift_strength > 0.6,
93
- "keyword_overlap": keyword_overlap,
94
- "overlap_ratio": overlap_ratio,
95
- "reason": f"Overlap: {overlap_ratio:.1%}, Marqueurs: {has_shift_marker}"
96
  }
97
 
98
  def generate_search_strategy(analysis: dict, subject_keywords: List[str], geo_info: dict) -> dict:
99
  """Génère une stratégie de recherche optimisée"""
100
  strategy = {
101
  "use_rag": analysis["needs_docs"],
102
- "use_memory": analysis["needs_memory"] or analysis["type"] == "continuation",
103
- "use_web": analysis["needs_web"] or analysis["temporal"],
104
- "memory_k": 5,
105
- "rag_k": 3,
106
- "web_enhanced": False,
107
- "search_query_suffix": ""
108
  }
109
 
110
- # Ajustement selon la complexité
111
  if analysis["complexity"] == "complex":
112
- strategy["memory_k"] = 8
113
- strategy["rag_k"] = 5
114
- elif analysis["complexity"] == "simple":
115
- strategy["memory_k"] = 3
116
- strategy["rag_k"] = 2
117
 
118
- # Enrichissement de la recherche web
119
  if analysis["needs_web"]:
120
  strategy["web_enhanced"] = True
121
- if subject_keywords:
122
- strategy["search_query_suffix"] = f" {' '.join(subject_keywords[:3])}"
123
- if analysis["geographical"]:
124
- strategy["search_query_suffix"] += f" {geo_info.get('city', 'Gabon')}"
125
 
126
  return strategy
127
 
@@ -132,118 +102,16 @@ def execute_reflection_plan(
132
  current_subject: Optional[str] = None,
133
  subject_keywords: Optional[List[str]] = None
134
  ):
135
- """
136
- Phase de réflexion structurée avec analyse contextuelle avancée et adaptation dynamique.
137
- """
138
- if geo_info is None:
139
- geo_info = {}
140
- if messages is None:
141
- messages = []
142
- if subject_keywords is None:
143
- subject_keywords = []
144
-
145
- # ÉTAPE 1: Analyse du type de requête
146
- query_analysis = analyze_query_type(prompt)
147
 
148
- # ÉTAPE 2: Détection de changement de sujet
149
  subject_shift = detect_subject_shift(prompt, current_subject, subject_keywords)
150
-
151
- # ÉTAPE 3: Génération de la stratégie de recherche
152
  search_strategy = generate_search_strategy(query_analysis, subject_keywords, geo_info)
153
 
154
- # ÉTAPE 4: Affichage de la réflexion (si Streamlit disponible)
155
- try:
156
- location = f"{geo_info.get('city', 'Libreville')}, {geo_info.get('country', 'Gabon')}"
157
-
158
- with st.status("🧠 Kibali Thinking Engine", expanded=True) as status:
159
- st.write(f"🌍 **Localisation active :** {location}")
160
- st.write("")
161
-
162
- # Analyse du type de requête
163
- st.write("### 📊 Analyse de la requête")
164
- st.write(f"- **Type :** {query_analysis['type'].replace('_', ' ').title()}")
165
- st.write(f"- **Complexité :** {query_analysis['complexity'].title()}")
166
-
167
- if query_analysis['temporal']:
168
- st.write("- ⏰ **Dimension temporelle détectée** → Recherche web activée")
169
- if query_analysis['geographical']:
170
- st.write(f"- 🗺️ **Contexte géographique :** {location}")
171
-
172
- time.sleep(0.2)
173
- st.write("")
174
-
175
- # Détection de changement de sujet
176
- st.write("### 🔄 Analyse du contexte conversationnel")
177
- if subject_shift['shift_detected']:
178
- if subject_shift['new_subject_detected']:
179
- st.write("- 🆕 **Nouveau sujet détecté** → Rafraîchissement du contexte")
180
- else:
181
- st.write(f"- ⚠️ **Changement partiel** (force: {subject_shift['shift_strength']:.0%})")
182
- st.write(f" *Raison : {subject_shift['reason']}*")
183
- else:
184
- st.write("- ✅ **Continuité du sujet actuel**")
185
- if subject_keywords:
186
- st.write(f" *Mots-clés actifs : {', '.join(subject_keywords[:5])}*")
187
- st.write(f" *Overlap : {subject_shift['keyword_overlap']}/{len(subject_keywords)} mots-clés*")
188
-
189
- time.sleep(0.2)
190
- st.write("")
191
-
192
- # Stratégie de recherche
193
- st.write("### 🎯 Stratégie de réponse")
194
- sources = []
195
- if search_strategy['use_rag']:
196
- sources.append(f"📚 Documents PDF (top {search_strategy['rag_k']})")
197
- if search_strategy['use_memory']:
198
- sources.append(f"🧠 Mémoire conversationnelle (top {search_strategy['memory_k']})")
199
- if search_strategy['use_web']:
200
- web_label = "🌐 Recherche web"
201
- if search_strategy['web_enhanced']:
202
- web_label += " (enrichie avec contexte)"
203
- sources.append(web_label)
204
-
205
- if not sources:
206
- sources.append("💭 Connaissance générale du modèle")
207
-
208
- for i, source in enumerate(sources, 1):
209
- st.write(f"{i}. {source}")
210
- time.sleep(0.15)
211
-
212
- st.write("")
213
-
214
- # Plan d'action détaillé
215
- st.write("### ⚙️ Plan d'exécution")
216
- steps = []
217
-
218
- if search_strategy['use_rag']:
219
- steps.append("Extraction des chunks pertinents depuis la base vectorielle PDF")
220
-
221
- if search_strategy['use_memory']:
222
- steps.append("Récupération des échanges similaires avec scoring de pertinence")
223
-
224
- if search_strategy['use_web']:
225
- query_suffix = search_strategy['search_query_suffix']
226
- steps.append(f"Requête web : '{prompt[:50]}...{query_suffix}'")
227
-
228
- steps.append("Synthèse des sources avec priorisation hiérarchique")
229
- steps.append("Génération de la réponse avec verrouillage contextuel")
230
-
231
- for i, step in enumerate(steps, 1):
232
- st.write(f"{i}. {step}")
233
- time.sleep(0.15)
234
-
235
- time.sleep(0.3)
236
- status.update(
237
- label="✅ Stratégie validée - Génération en cours",
238
- state="complete",
239
- expanded=False
240
- )
241
-
242
- except Exception as e:
243
- # Fallback si Streamlit n'est pas disponible
244
- print(f"[Kibali Thinking] Type: {query_analysis['type']}, Complexité: {query_analysis['complexity']}")
245
- print(f"[Kibali Thinking] Changement de sujet: {subject_shift['shift_detected']} (force: {subject_shift['shift_strength']:.0%})")
246
- print(f"[Kibali Thinking] Sources: RAG={search_strategy['use_rag']}, Memory={search_strategy['use_memory']}, Web={search_strategy['use_web']}")
247
 
248
  return {
249
  "analysis": query_analysis,
 
 
1
  import time
2
  from typing import List, Optional
3
  import re
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
 
9
  def analyze_query_type(prompt: str) -> dict:
10
  """Analyse le type de requête pour adapter la stratégie de réflexion"""
 
20
  "geographical": False
21
  }
22
 
 
23
  temporal_keywords = ["aujourd'hui", "maintenant", "récent", "actuel", "dernier", "2024", "2025"]
24
  if any(kw in prompt_lower for kw in temporal_keywords):
25
  analysis["temporal"] = True
26
  analysis["needs_web"] = True
27
 
 
28
  geo_keywords = ["gabon", "libreville", "port-gentil", "franceville", "oyem", "où", "localisation"]
29
  if any(kw in prompt_lower for kw in geo_keywords):
30
  analysis["geographical"] = True
31
 
 
32
  doc_keywords = ["selon le document", "d'après le pdf", "dans le fichier", "uploadé"]
33
  if any(kw in prompt_lower for kw in doc_keywords):
34
  analysis["needs_docs"] = True
35
  analysis["type"] = "document_query"
36
 
 
37
  continuation_keywords = ["ils", "elles", "lui", "leur", "donc", "alors", "ensuite", "aussi", "également"]
38
  if any(kw in prompt_lower for kw in continuation_keywords) or len(prompt.split()) < 5:
39
  analysis["needs_memory"] = True
40
  analysis["type"] = "continuation"
41
 
42
+ if len(prompt.split()) > 15 or (prompt.count("?") > 1):
 
43
  analysis["complexity"] = "complex"
44
+ elif any(kw in prompt_lower for kw in ["pourquoi", "comment", "expliquer"]):
45
  analysis["complexity"] = "medium"
46
 
 
47
  web_keywords = ["actualité", "news", "prix", "cours", "météo", "horaire"]
48
  if any(kw in prompt_lower for kw in web_keywords):
49
  analysis["needs_web"] = True
 
54
  def detect_subject_shift(prompt: str, current_subject: str, subject_keywords: List[str]) -> dict:
55
  """Détecte un changement de sujet et évalue la force du changement"""
56
  if not current_subject or not subject_keywords:
57
+ return {"shift_detected": False, "shift_strength": 0.0, "new_subject_detected": True, "reason": "Init"}
 
 
 
 
 
58
 
59
  prompt_lower = prompt.lower()
 
 
60
  prompt_words = set(re.findall(r'\b\w{4,}\b', prompt_lower))
61
  keyword_overlap = len(prompt_words.intersection(set(subject_keywords)))
62
  overlap_ratio = keyword_overlap / max(len(subject_keywords), 1)
63
 
 
64
  shift_markers = ["maintenant", "sinon", "autre chose", "parlons de", "passons à", "nouveau sujet"]
65
  has_shift_marker = any(marker in prompt_lower for marker in shift_markers)
66
 
 
67
  shift_strength = 0.0
68
+ if overlap_ratio < 0.2: shift_strength += 0.5
69
+ if has_shift_marker: shift_strength += 0.3
 
 
 
 
 
 
70
 
71
  return {
72
+ "shift_detected": shift_strength > 0.4,
73
  "shift_strength": shift_strength,
74
  "new_subject_detected": shift_strength > 0.6,
75
+ "reason": f"Overlap: {overlap_ratio:.1%}"
 
 
76
  }
77
 
78
  def generate_search_strategy(analysis: dict, subject_keywords: List[str], geo_info: dict) -> dict:
79
  """Génère une stratégie de recherche optimisée"""
80
  strategy = {
81
  "use_rag": analysis["needs_docs"],
82
+ "use_memory": analysis["needs_memory"],
83
+ "use_web": analysis["needs_web"],
84
+ "memory_k": 5, "rag_k": 3,
85
+ "web_enhanced": False, "search_query_suffix": ""
 
 
86
  }
87
 
 
88
  if analysis["complexity"] == "complex":
89
+ strategy.update({"memory_k": 8, "rag_k": 5})
 
 
 
 
90
 
 
91
  if analysis["needs_web"]:
92
  strategy["web_enhanced"] = True
93
+ suffix = " ".join(subject_keywords[:3]) if subject_keywords else ""
94
+ strategy["search_query_suffix"] = f"{suffix} {geo_info.get('city', 'Gabon')}"
 
 
95
 
96
  return strategy
97
 
 
102
  current_subject: Optional[str] = None,
103
  subject_keywords: Optional[List[str]] = None
104
  ):
105
+ """Phase de réflexion structurée compatible FastAPI (sans Streamlit)"""
106
+ geo_info = geo_info or {}
107
+ subject_keywords = subject_keywords or []
 
 
 
 
 
 
 
 
 
108
 
109
+ query_analysis = analyze_query_type(prompt)
110
  subject_shift = detect_subject_shift(prompt, current_subject, subject_keywords)
 
 
111
  search_strategy = generate_search_strategy(query_analysis, subject_keywords, geo_info)
112
 
113
+ # Logs internes (visibles dans Docker)
114
+ print(f"🧠 [REFLECTION] Type: {query_analysis['type']} | Web: {search_strategy['use_web']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  return {
117
  "analysis": query_analysis,
tools/web.py CHANGED
@@ -1,6 +1,5 @@
1
  from tavily import TavilyClient
2
  from duckduckgo_search import DDGS
3
- import streamlit as st
4
  import os
5
  from dotenv import load_dotenv
6
 
@@ -12,9 +11,10 @@ def web_search(query: str):
12
  results = []
13
  images = []
14
 
15
- # 1. TENTATIVE AVEC TAVILY (Plus robuste pour les agents)
16
  if TAVILY_API_KEY:
17
  try:
 
18
  tavily = TavilyClient(api_key=TAVILY_API_KEY)
19
  res = tavily.search(
20
  query=query,
@@ -28,12 +28,12 @@ def web_search(query: str):
28
  if len(results) >= 2:
29
  return {"results": results, "images": images, "query": query, "source": "Tavily"}
30
  except Exception as e:
31
- print(f"Tavily Error: {e}")
32
 
33
- # 2. FALLBACK AVEC DUCKDUCKGO (Avec gestion d'erreur propre)
 
34
  try:
35
  with DDGS() as ddgs:
36
- # Texte - Utilisation d'un timeout implicite par le context manager
37
  ddg_gen = ddgs.text(query, max_results=5)
38
  if ddg_gen:
39
  for r in ddg_gen:
@@ -43,16 +43,15 @@ def web_search(query: str):
43
  "url": r.get('href')
44
  })
45
 
46
- # Images - Séparé pour éviter de tout bloquer en cas de 403
47
  try:
48
  ddg_img_gen = ddgs.images(query, max_results=3)
49
  if ddg_img_gen:
50
  images = [img.get('image') for img in ddg_img_gen if img.get('image')]
51
  except Exception:
52
- pass # Les images sont facultatives
53
 
54
  except Exception as e:
55
- print(f"DuckDuckGo Error: {e}")
56
 
57
  return {
58
  "results": results,
 
1
  from tavily import TavilyClient
2
  from duckduckgo_search import DDGS
 
3
  import os
4
  from dotenv import load_dotenv
5
 
 
11
  results = []
12
  images = []
13
 
14
+ # 1. TENTATIVE AVEC TAVILY
15
  if TAVILY_API_KEY:
16
  try:
17
+ print(f"🔍 Recherche Tavily pour : {query}")
18
  tavily = TavilyClient(api_key=TAVILY_API_KEY)
19
  res = tavily.search(
20
  query=query,
 
28
  if len(results) >= 2:
29
  return {"results": results, "images": images, "query": query, "source": "Tavily"}
30
  except Exception as e:
31
+ print(f"⚠️ Tavily Error: {e}")
32
 
33
+ # 2. FALLBACK AVEC DUCKDUCKGO
34
+ print(f"🦆 Fallback DuckDuckGo pour : {query}")
35
  try:
36
  with DDGS() as ddgs:
 
37
  ddg_gen = ddgs.text(query, max_results=5)
38
  if ddg_gen:
39
  for r in ddg_gen:
 
43
  "url": r.get('href')
44
  })
45
 
 
46
  try:
47
  ddg_img_gen = ddgs.images(query, max_results=3)
48
  if ddg_img_gen:
49
  images = [img.get('image') for img in ddg_img_gen if img.get('image')]
50
  except Exception:
51
+ pass
52
 
53
  except Exception as e:
54
+ print(f"DuckDuckGo Error: {e}")
55
 
56
  return {
57
  "results": results,