chatbot-salim

#2
by DmbOran - opened
Files changed (2) hide show
  1. app.py +106 -666
  2. requirements.txt +0 -3
app.py CHANGED
@@ -1,734 +1,174 @@
1
  import gradio as gr
2
  import os
 
3
  from qdrant_client import QdrantClient
4
  import requests
5
  from groq import Groq
6
  import json
7
- import re
8
- import unicodedata
9
  from datetime import datetime
10
- from typing import Tuple, List, Dict, Optional, Any
11
 
12
 
13
  # --- Configuration ---
 
14
  QDRANT_URL = "https://f1db4954-34db-4b00-b9bb-8acf74c1d6e4.eu-central-1-0.aws.cloud.qdrant.io"
15
  QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')
16
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
17
- VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY")
18
 
19
- DB_Server = os.getenv("DB_Server")
20
- DB_User = os.getenv("DB_User")
21
- DB_Password = os.getenv("DB_Password")
22
- DB_Name = os.getenv("DB_Name")
23
 
 
 
24
 
25
- # =============================================================================
26
- # ADVANCED TOPIC CLASSIFICATION & FILTERING
27
- # =============================================================================
28
-
29
- class TopicClassifier:
30
- """
31
- Advanced topic classifier to ensure the chatbot stays on-topic.
32
- Uses keyword matching + semantic understanding to filter off-topic queries.
33
- """
34
-
35
- # Allowed domains - Real estate in Algeria
36
- ALLOWED_TOPICS = {
37
- "real_estate_search": [
38
- "appartement", "maison", "terrain", "villa", "studio", "duplex", "local",
39
- "f1", "f2", "f3", "f4", "f5", "f6", "logement", "habitation", "residence",
40
- "immeuble", "etage", "chambre", "salon", "cuisine", "salle de bain",
41
- "garage", "parking", "balcon", "terrasse", "jardin", "cave", "grenier",
42
- "surface", "superficie", "m2", "m²", "pièces", "pieces"
43
- ],
44
- "real_estate_transaction": [
45
- "acheter", "achat", "vendre", "vente", "louer", "location", "bail",
46
- "locataire", "propriétaire", "proprietaire", "bailleur", "prix", "budget",
47
- "négocier", "negocier", "offre", "contre-offre", "compromis", "promesse",
48
- "signature", "acte", "notaire", "frais", "commission", "honoraires",
49
- "acompte", "caution", "garantie", "depot", "dépôt", "loyer", "charges"
50
- ],
51
- "legal_admin": [
52
- "document", "papier", "dossier", "formalité", "formalite", "procédure",
53
- "procedure", "démarche", "demarche", "administration", "mairie", "préfecture",
54
- "cadastre", "conservation", "hypothèque", "hypotheque", "enregistrement",
55
- "timbre", "taxe", "impôt", "impot", "fiscalité", "fiscalite", "tva",
56
- "livret foncier", "acte de propriété", "titre foncier", "permis de construire",
57
- "certificat", "urbanisme", "zonage", "loi", "réglementation", "reglementation",
58
- "droit", "juridique", "légal", "legal", "contrat", "clause"
59
- ],
60
- "financing": [
61
- "crédit", "credit", "prêt", "pret", "emprunt", "banque", "financement",
62
- "taux", "intérêt", "interet", "mensualité", "mensualite", "apport",
63
- "capacité", "capacite", "endettement", "simulation", "simulateur",
64
- "assurance", "garantie", "hypothécaire", "hypothecaire", "remboursement"
65
- ],
66
- "market_info": [
67
- "marché", "marche", "immobilier", "investissement", "investir", "rentabilité",
68
- "rentabilite", "rendement", "prix moyen", "estimation", "évaluation",
69
- "evaluation", "expertise", "tendance", "évolution", "evolution", "quartier",
70
- "zone", "secteur", "emplacement", "localisation"
71
- ],
72
- "algerian_cities": [
73
- "alger", "oran", "constantine", "annaba", "blida", "bejaia", "béjaïa",
74
- "tizi ouzou", "setif", "sétif", "batna", "chlef", "boumerdes", "boumerdès",
75
- "mostaganem", "relizane", "tlemcen", "sidi bel abbes", "béchar", "bechar",
76
- "skikda", "ghardaia", "guelma", "tiaret", "msila", "m'sila", "el oued",
77
- "ouargla", "tipaza", "djelfa", "naama", "médea", "medea", "saida", "saïda",
78
- "khenchela", "el bayadh", "illizi", "ain temouchent", "aïn temouchent",
79
- "bordj bou arreridj", "bouira", "algerie", "algérie", "dz"
80
- ],
81
- "greetings": [
82
- "bonjour", "bonsoir", "salut", "hello", "hi", "hey", "coucou", "salam",
83
- "bonne journée", "bonne soirée", "comment allez", "comment vas", "ça va",
84
- "ca va", "merci", "au revoir", "à bientôt", "a bientot"
85
- ]
86
- }
87
-
88
- # Explicitly OFF-TOPIC keywords
89
- OFF_TOPIC_KEYWORDS = [
90
- # General off-topic
91
- "recette", "cuisine", "manger", "restaurant", "football", "sport", "match",
92
- "film", "musique", "chanson", "jeu", "game", "video", "politique", "élection",
93
- "president", "gouvernement", "religion", "météo", "meteo", "vacances", "voyage",
94
- "avion", "train", "voiture", "moto", "vélo", "santé", "sante", "médecin",
95
- "hôpital", "hopital", "maladie", "covid", "école", "ecole", "université",
96
- "universite", "examen", "cours", "professeur", "étudiant", "etudiant",
97
-
98
- # Tech unrelated
99
- "programmer", "coder", "javascript", "python", "java", "android", "iphone",
100
- "ordinateur", "laptop", "téléphone", "telephone", "internet", "wifi",
101
-
102
- # Completely unrelated
103
- "guerre", "armée", "armee", "militaire", "drogue", "arme", "violence",
104
- "sexe", "pornographie", "gambling", "casino", "pari", "crypto", "bitcoin",
105
-
106
- # Other services
107
- "coiffeur", "dentiste", "avocat", "médecin", "docteur", "plombier",
108
- "électricien", "electricien", "mécanicien", "mecanicien"
109
- ]
110
-
111
- @classmethod
112
- def normalize(cls, text: str) -> str:
113
- if not text:
114
- return ""
115
- text = text.lower()
116
- text = unicodedata.normalize('NFD', text)
117
- text = re.sub(r"[\u0300-\u036f]", "", text)
118
- text = re.sub(r"\s+", " ", text)
119
- return text.strip()
120
-
121
- @classmethod
122
- def classify(cls, query: str) -> Tuple[bool, str, float]:
123
- """
124
- Classify if query is on-topic for real estate.
125
- Returns: (is_on_topic, detected_category, confidence_score)
126
- """
127
- normalized = cls.normalize(query)
128
- words = set(normalized.split())
129
-
130
- # Check for explicit off-topic keywords
131
- off_topic_matches = sum(1 for kw in cls.OFF_TOPIC_KEYWORDS if kw in normalized)
132
- if off_topic_matches >= 2:
133
- return False, "off_topic", 0.9
134
-
135
- # Check for on-topic keywords
136
- topic_scores = {}
137
- for category, keywords in cls.ALLOWED_TOPICS.items():
138
- score = sum(1 for kw in keywords if kw in normalized)
139
- if score > 0:
140
- topic_scores[category] = score
141
-
142
- if topic_scores:
143
- best_category = max(topic_scores, key=topic_scores.get)
144
- confidence = min(topic_scores[best_category] / 3.0, 1.0)
145
- return True, best_category, confidence
146
-
147
- # Ambiguous - check query length and patterns
148
- if len(words) <= 3:
149
- # Short queries might be greetings or simple questions
150
- return True, "general", 0.3
151
-
152
- # Default: slightly lean towards allowing (benefit of doubt)
153
- return True, "unknown", 0.2
154
-
155
- @classmethod
156
- def get_off_topic_response(cls) -> str:
157
- return (
158
- "🏠 Je suis un assistant spécialisé exclusivement dans **l'immobilier en Algérie**.\n\n"
159
- "Je peux vous aider avec :\n"
160
- "• Recherche de biens (appartements, maisons, terrains, villas...)\n"
161
- "• Informations sur l'achat, la vente ou la location\n"
162
- "• Démarches administratives et juridiques\n"
163
- "• Financement et crédits immobiliers\n"
164
- "• Prix et tendances du marché algérien\n\n"
165
- "Votre question semble hors de mon domaine d'expertise. "
166
- "Pourriez-vous reformuler votre demande en rapport avec l'immobilier ?"
167
- )
168
-
169
-
170
- # =============================================================================
171
- # INTENT DETECTION - More granular
172
- # =============================================================================
173
-
174
- class IntentDetector:
175
- """
176
- Detect user intent for more precise responses.
177
- """
178
-
179
- INTENTS = {
180
- "greeting": {
181
- "patterns": ["bonjour", "bonsoir", "salut", "hello", "hi", "hey", "coucou", "salam"],
182
- "max_words": 5
183
- },
184
- "thanks": {
185
- "patterns": ["merci", "thank", "remercie"],
186
- "max_words": 10
187
- },
188
- "goodbye": {
189
- "patterns": ["au revoir", "bye", "à bientôt", "a bientot", "bonne journée", "bonne soirée"],
190
- "max_words": 6
191
- },
192
- "property_search": {
193
- "patterns": ["cherche", "recherche", "trouve", "trouver", "besoin", "veux", "voudrais",
194
- "souhaite", "intéressé", "interesse", "disponible"],
195
- "requires": ["appartement", "maison", "villa", "terrain", "studio", "f1", "f2", "f3",
196
- "f4", "f5", "duplex", "local", "logement"]
197
- },
198
- "price_inquiry": {
199
- "patterns": ["prix", "coût", "cout", "combien", "budget", "tarif", "estimation"],
200
- "requires": []
201
- },
202
- "procedure_inquiry": {
203
- "patterns": ["comment", "étape", "etape", "procédure", "procedure", "démarche",
204
- "demarche", "document", "formalité", "formalite", "quoi faire"],
205
- "requires": []
206
- },
207
- "legal_inquiry": {
208
- "patterns": ["loi", "légal", "legal", "droit", "juridique", "réglementation",
209
- "reglementation", "permis", "autorisation"],
210
- "requires": []
211
- },
212
- "comparison": {
213
- "patterns": ["différence", "difference", "comparer", "comparaison", "mieux",
214
- "meilleur", "vs", "ou bien", "lequel"],
215
- "requires": []
216
- }
217
- }
218
-
219
- @classmethod
220
- def detect(cls, query: str) -> Tuple[str, float]:
221
- """
222
- Detect the primary intent of the query.
223
- Returns: (intent_name, confidence)
224
- """
225
- normalized = TopicClassifier.normalize(query)
226
- word_count = len(normalized.split())
227
-
228
- for intent, config in cls.INTENTS.items():
229
- patterns = config.get("patterns", [])
230
- max_words = config.get("max_words", 100)
231
- requires = config.get("requires", [])
232
-
233
- pattern_match = any(p in normalized for p in patterns)
234
-
235
- if pattern_match:
236
- if max_words and word_count > max_words:
237
- continue
238
- if requires and not any(r in normalized for r in requires):
239
- continue
240
- return intent, 0.8
241
-
242
- return "general_query", 0.5
243
-
244
-
245
- # =============================================================================
246
- # RESPONSE TEMPLATES
247
- # =============================================================================
248
-
249
- class ResponseTemplates:
250
- """
251
- Pre-built responses for common intents.
252
- """
253
-
254
- GREETING = (
255
- "👋 Bonjour et bienvenue !\n\n"
256
- "Je suis votre assistant immobilier spécialisé en **Algérie**. "
257
- "Je suis là pour vous accompagner dans vos projets immobiliers.\n\n"
258
- "**Comment puis-je vous aider ?**\n"
259
- "• Rechercher un bien (appartement, maison, terrain...)\n"
260
- "• Répondre à vos questions sur l'achat ou la location\n"
261
- "• Expliquer les démarches administratives\n"
262
- "• Vous informer sur les prix du marché\n\n"
263
- "N'hésitez pas à me poser votre question ! 😊"
264
- )
265
-
266
- THANKS = (
267
- "De rien ! Je suis ravi d'avoir pu vous aider. 😊\n\n"
268
- "N'hésitez pas si vous avez d'autres questions sur l'immobilier en Algérie."
269
- )
270
-
271
- GOODBYE = (
272
- "Au revoir et bonne continuation dans vos recherches ! 👋\n\n"
273
- "N'hésitez pas à revenir si vous avez d'autres questions immobilières."
274
- )
275
-
276
- NO_RESULTS = (
277
- "Je n'ai pas trouvé d'informations correspondant exactement à votre demande "
278
- "dans notre base de données.\n\n"
279
- "**Suggestions :**\n"
280
- "• Essayez avec des critères différents (ville, type de bien, budget)\n"
281
- "• Consultez notre liste complète de biens : https://jacheteenalgerie.com/liste-des-biens/\n"
282
- "• Prenez rendez-vous avec un expert : https://jacheteenalgerie.com/prendre-rdv/"
283
- )
284
-
285
-
286
- # =============================================================================
287
- # IMPROVED EMBEDDING & SEARCH
288
- # =============================================================================
289
-
290
- print("🚀 Initializing Advanced RAG Chatbot...")
291
-
292
- def get_embedding_via_voyage_ai(text: str) -> Optional[List[float]]:
293
- """Get embeddings with error handling and retry logic."""
294
- url = "https://api.voyageai.com/v1/embeddings"
295
- headers = {
296
- "Authorization": f"Bearer {VOYAGE_API_KEY}",
297
- "Content-Type": "application/json"
298
- }
299
- payload = {
300
- "input": [text],
301
- "model": "voyage-2"
302
- }
303
-
304
- for attempt in range(3):
305
- try:
306
- response = requests.post(url, headers=headers, json=payload, timeout=30)
307
- response.raise_for_status()
308
- result = response.json()
309
- if result and "data" in result and len(result["data"]) > 0:
310
- return result["data"][0]["embedding"]
311
- except requests.exceptions.RequestException as e:
312
- print(f"⚠️ Voyage AI attempt {attempt + 1} failed: {e}")
313
- if attempt == 2:
314
- return None
315
- return None
316
-
317
 
318
  # Initialize Qdrant client
319
  print("🔗 Connecting to Qdrant...")
320
- qdrant_client = None
321
  try:
322
- qdrant_client = QdrantClient(
323
- url=QDRANT_URL,
324
- api_key=QDRANT_API_KEY,
325
- check_compatibility=False,
326
- prefer_grpc=False # Use REST API to avoid asyncio event loop issues
327
- )
328
- collection_info = qdrant_client.get_collection(collection_name="biens")
329
- print(f"✅ Connected to Qdrant! Collection 'biens' has {collection_info.points_count} documents.")
330
- collection_info_articles = qdrant_client.get_collection(collection_name="articles")
331
- print(f"✅ Connected to Qdrant! Collection 'articles' has {collection_info_articles.points_count} documents.")
332
  except Exception as e:
333
  print(f"❌ Failed to connect to Qdrant: {e}")
 
334
 
335
  # Initialize Groq client
336
  groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
337
 
338
 
339
- # =============================================================================
340
- # LOGGING & DATABASE
341
- # =============================================================================
342
-
343
- def log_interaction(user_message: str, response: str, context: str,
344
- tokens_max: int, temperature: float, top_p: float):
345
  log_entry = {
346
  "timestamp": datetime.utcnow().isoformat(),
347
  "question": user_message,
348
  "response": response,
349
- "context": context[:500] if context else "",
350
  "tokens_max": tokens_max,
351
  "temperature": temperature,
352
  "top_p": top_p,
353
  }
354
  try:
355
- requests.post("https://ee98552a720c.ngrok-free.app/log", json=log_entry, timeout=5)
356
  except Exception as e:
357
- print(f"⚠️ Failed to send log: {e}")
358
-
359
-
360
- def save_chat_to_mysql(question: str, answer: str, source: str):
361
- """Save Q&A to MySQL database."""
362
- try:
363
- if not all([DB_Server, DB_User, DB_Password, DB_Name]):
364
- return
365
-
366
- import pymysql
367
- connection = pymysql.connect(
368
- host=DB_Server,
369
- user=DB_User,
370
- password=DB_Password,
371
- database=DB_Name,
372
- charset="utf8mb4",
373
- cursorclass=pymysql.cursors.DictCursor,
374
- autocommit=True,
375
- )
376
- try:
377
- with connection.cursor() as cursor:
378
- sql = (
379
- "INSERT INTO qr_chatbot (question, answer, source, created_at) "
380
- "VALUES (%s, %s, %s, %s)"
381
- )
382
- cursor.execute(sql, (question or "", answer or "", source or "", datetime.now()))
383
- finally:
384
- connection.close()
385
- except Exception as e:
386
- print(f"❌ MySQL save error: {e}")
387
-
388
-
389
- # =============================================================================
390
- # ADVANCED COLLECTION DECISION
391
- # =============================================================================
392
-
393
- def decide_collection_intelligently(query: str) -> str:
394
- """
395
- Decide which collection to search based on query analysis.
396
- More efficient than using LLM for every decision.
397
- """
398
- normalized = TopicClassifier.normalize(query)
399
-
400
- # Property-related keywords -> biens
401
- property_keywords = [
402
- "appartement", "maison", "villa", "terrain", "studio", "duplex", "local",
403
- "f1", "f2", "f3", "f4", "f5", "f6", "pièce", "piece", "chambre", "m2", "m²",
404
- "superficie", "surface", "louer", "location", "acheter", "achat", "vente",
405
- "prix", "budget", "dzd", "million", "oran", "alger", "constantine"
406
- ]
407
-
408
- # Article-related keywords -> articles
409
- article_keywords = [
410
- "comment", "étape", "etape", "procédure", "procedure", "démarche", "demarche",
411
- "document", "loi", "droit", "juridique", "conseil", "guide", "fiscalité",
412
- "fiscalite", "réglementation", "reglementation", "investir", "investissement",
413
- "quoi faire", "quels sont", "expliquer", "expliquez"
414
- ]
415
-
416
- property_score = sum(1 for kw in property_keywords if kw in normalized)
417
- article_score = sum(1 for kw in article_keywords if kw in normalized)
418
-
419
- if property_score > article_score:
420
- return "biens"
421
- elif article_score > property_score:
422
- return "articles"
423
- else:
424
- # Default to biens for ambiguous queries
425
- return "biens"
426
-
427
-
428
- def _extract_points_from_qdrant_response(res) -> List[Any]:
429
- """Extract points from various Qdrant response formats."""
430
- if not res:
431
- return []
432
- if hasattr(res, "points"):
433
- return res.points or []
434
- if isinstance(res, list):
435
- return res
436
- if isinstance(res, dict):
437
- for key in ("result", "points", "data", "hits", "items"):
438
- v = res.get(key)
439
- if isinstance(v, list):
440
- return v
441
- return []
442
 
443
-
444
- def _get_payload_from_hit(hit) -> Dict:
445
- """Extract payload from a search hit."""
446
- if not hit:
447
- return {}
448
- if hasattr(hit, "payload"):
449
- return hit.payload or {}
450
- if isinstance(hit, dict):
451
- return hit.get("payload", {})
452
- return {}
453
-
454
-
455
- def retrieve_context(query: str, top_k: int = 8) -> Tuple[str, str]:
456
- """
457
- Retrieve relevant context from Qdrant.
458
- Returns: (context_text, collection_name)
459
- """
460
  if not qdrant_client:
461
- print("❌ Qdrant client not initialized.")
462
- return "", ""
463
-
464
- query_embedding = get_embedding_via_voyage_ai(query)
465
- if query_embedding is None:
466
- print("❌ Failed to generate embedding.")
467
- return "", ""
468
-
469
- chosen_collection = decide_collection_intelligently(query)
470
- print(f"📚 Searching collection: {chosen_collection}")
471
-
472
  try:
473
- # Try query_points first
474
- res = qdrant_client.query_points(
475
- collection_name=chosen_collection,
476
- query=query_embedding,
477
- limit=top_k * 2,
478
- with_payload=True,
479
- with_vectors=False,
480
  )
481
- points = _extract_points_from_qdrant_response(res)
482
- except Exception as e:
483
- print(f"⚠️ query_points failed: {e}")
484
- try:
485
- res = qdrant_client.search(
486
- collection_name=chosen_collection,
487
- query_vector=query_embedding,
488
- limit=top_k * 2,
489
- with_payload=True,
490
- )
491
- points = res if isinstance(res, list) else []
492
- except Exception as e2:
493
- print(f"❌ Search failed: {e2}")
494
- return "", ""
495
-
496
- if not points:
497
- return "", chosen_collection
498
-
499
- # Build context from results
500
- context_parts = []
501
- for idx, p in enumerate(points[:top_k], 1):
502
- payload = _get_payload_from_hit(p)
503
-
504
- if chosen_collection == "biens":
505
- titre = payload.get('Titre') or payload.get('titre') or ""
506
- ville = payload.get('Ville') or payload.get('ville_norm') or ""
507
- superficie = payload.get('superficie') or payload.get('Superficie') or ""
508
- pieces = payload.get('pieces') or payload.get('Nombre de pièces') or ""
509
- prix = payload.get('prix') or payload.get('Prix') or ""
510
- ref = payload.get('REF') or payload.get('ref') or ""
511
- url = payload.get('URL') or payload.get('url') or ""
512
- commodites = payload.get('commodites') or []
513
-
514
- parts = [f"**{idx}. {titre}**"] if titre else [f"**{idx}. Bien immobilier**"]
515
- if ville:
516
- parts.append(f"📍 Ville: {ville}")
517
- if superficie:
518
- parts.append(f"📐 Superficie: {superficie} m²")
519
- if pieces:
520
- parts.append(f"🚪 Pièces: {pieces}")
521
- if prix:
522
- parts.append(f"💰 Prix: {prix}")
523
- if commodites and isinstance(commodites, list):
524
- parts.append(f"✨ Commodités: {', '.join(commodites[:5])}")
525
- if ref:
526
- parts.append(f"🔖 Réf: {ref}")
527
- if url:
528
- parts.append(f"🔗 Lien: {url}")
529
-
530
- context_parts.append("\n".join(parts))
531
-
532
- else: # articles
533
- titre = payload.get('Titre') or payload.get('titre') or ""
534
- date_pub = payload.get('Date de Publication') or payload.get('date') or ""
535
- contenu = payload.get('Contenu Texte') or payload.get('contenu') or ""
536
- url = payload.get('URL') or payload.get('url') or ""
537
-
538
- parts = [f"**Article: {titre}**"] if titre else ["**Article**"]
539
- if date_pub:
540
- parts.append(f"📅 Date: {date_pub}")
541
- if contenu:
542
- # Truncate long content
543
- excerpt = contenu[:600] + "..." if len(contenu) > 600 else contenu
544
- parts.append(f"📝 {excerpt}")
545
- if url:
546
- parts.append(f"🔗 Lien: {url}")
547
-
548
- context_parts.append("\n".join(parts))
549
-
550
- context = "\n\n---\n\n".join(context_parts)
551
- return context, chosen_collection
552
-
553
 
554
- # =============================================================================
555
- # LLM QUERY
556
- # =============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
- def query_groq_llm(messages: List[Dict], tokens_max: int,
559
- temperature: float, top_p: float) -> str:
560
- """Query Groq LLM and return complete response."""
561
  if not groq_client:
562
- print("❌ Groq client not initialized")
563
- return "❌ Erreur : Service LLM non disponible."
564
-
565
  try:
566
- print(f"🤖 Calling Groq LLM with {len(messages)} messages...")
567
  chat_completion = groq_client.chat.completions.create(
568
- model="openai/gpt-oss-safeguard-20b",
569
  messages=messages,
570
  temperature=temperature,
571
  max_tokens=int(tokens_max),
572
  top_p=top_p,
573
  stream=True,
574
  )
575
-
576
  full_response = ""
577
  for chunk in chat_completion:
578
- if chunk.choices and chunk.choices[0].delta:
579
- delta = chunk.choices[0].delta.content or ""
580
- full_response += delta
581
-
582
- if not full_response.strip():
583
- print("⚠️ LLM returned empty response")
584
- return "Je n'ai pas pu générer une réponse. Veuillez réessayer."
585
-
586
- print(f"✅ LLM response received: {len(full_response)} chars")
587
- return full_response.strip()
588
  except Exception as e:
589
- print(f"❌ LLM Error: {type(e).__name__}: {e}")
590
- return f"❌ Erreur LLM: {e}"
591
-
592
-
593
- # =============================================================================
594
- # IMPROVED SYSTEM PROMPT
595
- # =============================================================================
596
-
597
- SYSTEM_PROMPT = """Vous êtes un assistant immobilier expert spécialisé EXCLUSIVEMENT en immobilier en Algérie.
598
-
599
- RÈGLES STRICTES :
600
- 1. Répondez UNIQUEMENT aux questions liées à l'immobilier en Algérie
601
- 2. Si une question est hors sujet (cuisine, sport, politique, etc.), refusez poliment et rappelez votre spécialisation
602
- 3. Basez vos réponses UNIQUEMENT sur le contexte fourni
603
- 4. Si le contexte ne contient pas l'information demandée, dites clairement que vous n'avez pas cette information
604
-
605
- FORMAT DE RÉPONSE :
606
- - Soyez concis et direct
607
- - Structurez clairement les informations
608
- - Pour les biens : numérotez-les (1., 2., 3.)
609
- - Mettez en évidence les prix avec 💰
610
- - Incluez les liens sources quand disponibles
611
- - N'inventez JAMAIS de données ou de liens
612
-
613
- COMPORTEMENT :
614
- - Ton professionnel mais accessible
615
- - Pas de markdown excessif (évitez ###, ****, etc. sauf pour mettre en valeur)
616
- - Réponses en français
617
- - Pas de commentaires sur votre processus interne"""
618
-
619
-
620
- # =============================================================================
621
- # MAIN RESPONSE FUNCTION
622
- # =============================================================================
623
-
624
- def respond(message: str, tokens_max: int, temperature: float, top_p: float) -> Tuple[str, str]:
625
- """
626
- Main response function with topic filtering and intent detection.
627
- """
628
- message = message.strip()
629
- if not message:
630
- return "Veuillez saisir une question.", ""
631
-
632
- # Step 1: Topic Classification
633
- is_on_topic, category, confidence = TopicClassifier.classify(message)
634
-
635
- if not is_on_topic:
636
- off_topic_response = TopicClassifier.get_off_topic_response()
637
- log_interaction(message, off_topic_response, "OFF_TOPIC", tokens_max, temperature, top_p)
638
- try:
639
- save_chat_to_mysql(message, off_topic_response, "off_topic")
640
- except:
641
- pass
642
- return off_topic_response, "Question hors sujet détectée"
643
-
644
- # Step 2: Intent Detection
645
- intent, intent_confidence = IntentDetector.detect(message)
646
-
647
- # Handle simple intents directly
648
- if intent == "greeting":
649
- response = ResponseTemplates.GREETING
650
- log_interaction(message, response, "GREETING", tokens_max, temperature, top_p)
651
- save_chat_to_mysql(message, response, "greeting")
652
- return response, ""
653
-
654
- if intent == "thanks":
655
- response = ResponseTemplates.THANKS
656
- log_interaction(message, response, "THANKS", tokens_max, temperature, top_p)
657
- save_chat_to_mysql(message, response, "thanks")
658
- return response, ""
659
-
660
- if intent == "goodbye":
661
- response = ResponseTemplates.GOODBYE
662
- log_interaction(message, response, "GOODBYE", tokens_max, temperature, top_p)
663
- save_chat_to_mysql(message, response, "goodbye")
664
- return response, ""
665
-
666
- # Step 3: Retrieve Context
667
- context, collection = retrieve_context(message, top_k=8)
668
-
669
- if not context:
670
- response = ResponseTemplates.NO_RESULTS
671
- log_interaction(message, response, "NO_CONTEXT", tokens_max, temperature, top_p)
672
- save_chat_to_mysql(message, response, "no_results")
673
- return response, "Aucun résultat trouvé"
674
-
675
- # Step 4: Generate Response with LLM
676
- user_content = f"""Question de l'utilisateur : {message}
677
-
678
- Contexte pertinent de notre base de données :
679
 
680
- {context}
 
 
 
 
 
 
 
681
 
682
- Répondez de manière précise et structurée en vous basant UNIQUEMENT sur ce contexte."""
 
683
 
684
- messages = [
685
- {"role": "system", "content": SYSTEM_PROMPT},
686
- {"role": "user", "content": user_content}
687
- ]
688
-
689
- response = query_groq_llm(messages, tokens_max, temperature, top_p)
690
-
691
- # Step 5: Add relevant links based on collection
692
- if collection == "biens":
693
- response += "\n\n---\n📋 Explorer plus de biens : https://jacheteenalgerie.com/liste-des-biens/"
694
- response += "\n📅 Besoin d'accompagnement ? : https://jacheteenalgerie.com/nos-offres-immobilier/"
695
- elif collection == "articles":
696
- response += "\n\n---\n📅 Besoin d'accompagnement ? : https://jacheteenalgerie.com/nos-offres-immobilier/"
697
 
698
- # Step 6: Log and save
699
- log_interaction(message, response, context[:500], tokens_max, temperature, top_p)
700
- try:
701
- save_chat_to_mysql(message, response, collection)
702
- except Exception as e:
703
- print(f"⚠️ MySQL save failed: {e}")
704
-
705
- return response, context
706
 
707
-
708
- # =============================================================================
709
- # GRADIO API
710
- # =============================================================================
 
 
 
 
 
711
 
712
  demo = gr.Interface(
713
  fn=respond,
714
  inputs=[
715
- gr.Textbox(label="question"),
716
- gr.Number(value=1024, label="tokens_max"),
717
- gr.Number(value=0.3, label="temperature"),
718
- gr.Number(value=0.85, label="top_p"),
719
- ],
720
- outputs=[
721
- gr.Textbox(label="response"),
722
- gr.Textbox(label="context"),
723
  ],
724
- api_name="predict",
725
- concurrency_limit=10 # Allow up to 10 concurrent requests
 
 
 
 
 
 
 
 
 
 
726
  )
727
 
728
  if __name__ == "__main__":
729
- demo.launch(
730
- server_name="0.0.0.0",
731
- server_port=7860,
732
- show_error=True,
733
- max_threads=20 # Enable multi-threading for concurrent requests
734
- )
 
1
  import gradio as gr
2
  import os
3
+ from sentence_transformers import SentenceTransformer
4
  from qdrant_client import QdrantClient
5
  import requests
6
  from groq import Groq
7
  import json
 
 
8
  from datetime import datetime
 
9
 
10
 
11
  # --- Configuration ---
12
+ DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY')
13
  QDRANT_URL = "https://f1db4954-34db-4b00-b9bb-8acf74c1d6e4.eu-central-1-0.aws.cloud.qdrant.io"
14
  QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')
15
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
 
16
 
17
+ print("🔑 DEEPSEEK_API_KEY:", DEEPSEEK_API_KEY)
18
+ print("🔑 QDRANT_API_KEY:", QDRANT_API_KEY)
19
+ print("🔑 GROQ_API_KEY:", GROQ_API_KEY)
20
+ print("🔧 Gradio version:", gr.__version__)
21
 
22
+ # --- Initialize Clients ---
23
+ print("🚀 Initializing RAG Chatbot...")
24
 
25
+ # Initialize embedding model
26
+ print("📡 Loading embedding model...")
27
+ embedding_model = SentenceTransformer('paraphrase-MiniLM-L3-v2')
28
+ print("✅ Embedding model loaded!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # Initialize Qdrant client
31
  print("🔗 Connecting to Qdrant...")
 
32
  try:
33
+ qdrant_client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
34
+ collection_info = qdrant_client.get_collection(collection_name="RAG JEA")
35
+ print(f"✅ Connected to Qdrant! Collection 'RAG JEA' has {collection_info.points_count} documents.")
 
 
 
 
 
 
 
36
  except Exception as e:
37
  print(f"❌ Failed to connect to Qdrant: {e}")
38
+ qdrant_client = None
39
 
40
  # Initialize Groq client
41
  groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
42
 
43
 
44
+ def log_interaction(user_message, response, context, tokens_max, temperature, top_p):
 
 
 
 
 
45
  log_entry = {
46
  "timestamp": datetime.utcnow().isoformat(),
47
  "question": user_message,
48
  "response": response,
49
+ "context": context,
50
  "tokens_max": tokens_max,
51
  "temperature": temperature,
52
  "top_p": top_p,
53
  }
54
  try:
55
+ requests.post("https://ee98552a720c.ngrok-free.app/log", json=log_entry)
56
  except Exception as e:
57
+ print(f"Failed to send log: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ # --- Helper Functions ---
60
+ def retrieve_context(query, top_k=3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  if not qdrant_client:
62
+ return ""
 
 
 
 
 
 
 
 
 
 
63
  try:
64
+ query_embedding = embedding_model.encode(query).tolist()
65
+ search_results = qdrant_client.query_points(
66
+ collection_name="RAG JEA",
67
+ query_vector=query_embedding,
68
+ limit=top_k,
69
+ with_payload=True
 
70
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ context_parts = []
73
+ for hit in search_results:
74
+ payload = hit.payload
75
+ if payload and 'text' in payload:
76
+ doc_text = payload['text']
77
+ if 'link' in payload and payload['link']:
78
+ doc_text += f"\n(Source du lien : {payload['link']})"
79
+ context_parts.append(doc_text)
80
+ if context_parts:
81
+ context = "\n\n---\n\n".join(context_parts)
82
+ print(f"✅ Retrieved {len(context_parts)} relevant documents with links")
83
+ return context
84
+ else:
85
+ print("⚠️ No relevant documents found")
86
+ return ""
87
+ except Exception as e:
88
+ print(f"❌ Error retrieving context: {e}")
89
+ return ""
90
 
91
+ # --- Groq (DeepSeek) Function ---
92
+ def query_groq_deepseek(messages, tokens_max, temperature, top_p):
 
93
  if not groq_client:
94
+ yield "❌ Erreur : Clé API Groq non configurée."
95
+ return
 
96
  try:
 
97
  chat_completion = groq_client.chat.completions.create(
98
+ model="llama3-70b-8192",
99
  messages=messages,
100
  temperature=temperature,
101
  max_tokens=int(tokens_max),
102
  top_p=top_p,
103
  stream=True,
104
  )
 
105
  full_response = ""
106
  for chunk in chat_completion:
107
+ delta = chunk.choices[0].delta.content or ""
108
+ full_response += delta
109
+ yield full_response.strip()
 
 
 
 
 
 
 
110
  except Exception as e:
111
+ yield f"❌ Erreur GROQ: {e}"
112
+
113
+ # --- System message ---
114
+ system_message = (
115
+ "Vous êtes un assistant spécialisé en immobilier algérien. "
116
+ "Pour chaque bien ou idée, commence une nouvelle ligne avec deux sauts de ligne (\\n\\n). "
117
+ "Numérote chaque bien (1., 2., 3., etc.). "
118
+ "Si un lien source est fourni dans le contexte (ex: Source du lien : https://...), mentionne ce lien à la fin de la description du bien. N'invente jamais de lien. "
119
+ "N'utilise pas de balises HTML ni de markdown, seulement du texte brut et des liens. "
120
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ # --- Main Respond Function (Groq/DeepSeek used) ---
123
+ def respond(message, tokens_max, temperature, top_p):
124
+ context = retrieve_context(message)
125
+ enhanced_system_message = system_message
126
+ if context:
127
+ enhanced_system_message += f"\n\nContexte pertinent de la base de données:\n\n{context}"
128
+ else:
129
+ enhanced_system_message += "\n\nAucun contexte spécifique trouvé dans la base de données. Fournissez des informations générales sur l'immobilier algérien."
130
 
131
+ messages = [{"role": "system", "content": enhanced_system_message}]
132
+ messages.append({"role": "user", "content": message})
133
 
134
+ response = ""
135
+ for token in query_groq_deepseek(messages, tokens_max, temperature, top_p):
136
+ response = token
137
+ log_interaction(message, response, context, tokens_max, temperature, top_p)
 
 
 
 
 
 
 
 
 
138
 
139
+ return response.strip(), context
 
 
 
 
 
 
 
140
 
141
+ # --- Gradio UI ---
142
+ example_questions = [
143
+ "Quelles sont les étapes pour acheter un appartement en Algérie ?",
144
+ "Comment fonctionne la location immobilière en Algérie ?",
145
+ "Quels sont les droits des locataires ?",
146
+ "Informations sur les prix immobiliers à Oran",
147
+ "Documents nécessaires pour un achat immobilier"
148
+ ]
149
+ formatted_examples = [[q, 1000, 0.7, 0.95] for q in example_questions]
150
 
151
  demo = gr.Interface(
152
  fn=respond,
153
  inputs=[
154
+ gr.Textbox(label="Message utilisateur"),
155
+ gr.Slider(minimum=1, maximum=2048, value=1000, step=1, label="Tokens maximum"),
156
+ gr.Slider(minimum=0.1, maximum=2.0, value=0.7, step=0.1, label="Température"),
157
+ gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p (nucleus sampling)"),
 
 
 
 
158
  ],
159
+ outputs=[gr.Textbox(label="Réponse"), gr.Textbox(label="Contexte")],
160
+ title="🏠 Assistant Immobilier Algérien - RAG",
161
+ description="""
162
+ **Assistant intelligent alimenté par une base de données de connaissances**
163
+ Posez vos questions sur l'immobilier en Algérie. L'assistant consulte une base de données
164
+ de documents pertinents pour vous fournir des réponses précises et actualisées.
165
+ 💡 **Exemples :** Achat immobilier, location, droits des locataires, prix à Oran...
166
+ 🔧 **Système :** DeepSeek via Groq + Qdrant Cloud + all-MiniLM-L6-v2
167
+ """,
168
+ examples=formatted_examples,
169
+ cache_examples=False,
170
+ theme=gr.themes.Soft(),
171
  )
172
 
173
  if __name__ == "__main__":
174
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
 
 
 
 
 
requirements.txt CHANGED
@@ -13,6 +13,3 @@ qdrant-client
13
  groq
14
  gradio>=4.16.0
15
  google-generativeai>=0.3.2
16
- fastapi
17
- uvicorn
18
- pymysql
 
13
  groq
14
  gradio>=4.16.0
15
  google-generativeai>=0.3.2