edwinbh commited on
Commit
005a4c3
·
verified ·
1 Parent(s): 79e79a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -140
app.py CHANGED
@@ -134,9 +134,10 @@
134
  # )
135
 
136
 
 
137
  """
138
- 🏥 Pharmacy RAG System - Advanced Multi-Agent Architecture
139
- Uses: OpenRouter (GPT-4o-mini + text-embedding-3-small), Qdrant, NetworkX
140
  """
141
 
142
  import os
@@ -168,8 +169,8 @@ OPENROUTER_API_KEY = "sk-or-v1-23bc69e32d37529bd5143ae2bb542552c44fbe1fc696d4a84
168
  QDRANT_URL = "http://130.185.121.155:6333"
169
  COLLECTION_NAME = "pharmacy_products"
170
 
171
- LLM_MODEL = "openai/gpt-4.1-mini" # برای reasoning
172
- EMBEDDING_MODEL = "openai/text-embedding-3-small" # برای embeddings
173
 
174
 
175
  # ============================================================================
@@ -190,9 +191,10 @@ class Product:
190
  @dataclass
191
  class QueryIntent:
192
  """مدل intent شناسایی شده"""
193
- intent_type: str # simple_lookup, comparison, complex_reasoning
194
  extracted_symptoms: List[str]
195
  extracted_products: List[str]
 
196
  requires_graph: bool
197
  confidence: float
198
 
@@ -228,11 +230,10 @@ class OpenRouterClient:
228
  return result['data'][0]['embedding']
229
  except Exception as e:
230
  print(f"❌ Embedding Error: {e}")
231
- # Fallback: return zero vector
232
  return [0.0] * 1536
233
 
234
- def generate(self, messages: List[Dict], temperature: float = 0.7, max_tokens: int = 1500) -> str:
235
- """تولید متن با LLM"""
236
  url = f"{self.base_url}/chat/completions"
237
 
238
  payload = {
@@ -260,22 +261,20 @@ class VectorDB:
260
  """مدیریت Qdrant Vector Database"""
261
 
262
  def __init__(self, url: str, collection_name: str):
263
- self.client = QdrantClient(url=url, timeout=60) # افزایش timeout به 60 ثانیه
264
  self.collection_name = collection_name
265
  self.fallback_mode = False
266
- self.fallback_vectors = [] # برای حالت fallback
267
  self.fallback_metadata = []
268
 
269
  def create_collection(self, vector_size: int = 1536):
270
  """ساخت collection"""
271
  try:
272
- # بررسی وجود collection
273
  collections = self.client.get_collections().collections
274
  if any(c.name == self.collection_name for c in collections):
275
  print(f"✅ Collection '{self.collection_name}' already exists")
276
  return
277
 
278
- # ساخت collection جدید
279
  self.client.create_collection(
280
  collection_name=self.collection_name,
281
  vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE)
@@ -287,8 +286,7 @@ class VectorDB:
287
  def upsert_points(self, points: List[PointStruct]):
288
  """اضافه کردن points به collection"""
289
  try:
290
- # تقسیم به batch های کوچکتر برای جلوگیری از timeout
291
- batch_size = 5 # کاهش به 5 برای جلوگیری از timeout
292
  total = len(points)
293
  failed_batches = 0
294
 
@@ -296,7 +294,6 @@ class VectorDB:
296
  batch = points[i:i+batch_size]
297
  print(f" Uploading batch {i//batch_size + 1}/{(total + batch_size - 1)//batch_size}...")
298
 
299
- # تلاش مجدد در صورت خطا
300
  max_retries = 3
301
  batch_failed = True
302
  for attempt in range(max_retries):
@@ -304,7 +301,7 @@ class VectorDB:
304
  self.client.upsert(
305
  collection_name=self.collection_name,
306
  points=batch,
307
- wait=True # صبر برای تکمیل
308
  )
309
  batch_failed = False
310
  break
@@ -312,7 +309,6 @@ class VectorDB:
312
  if attempt == max_retries - 1:
313
  print(f" ⚠️ Failed batch {i//batch_size + 1}: {e}")
314
  failed_batches += 1
315
- # ذخیره در fallback
316
  for point in batch:
317
  self.fallback_vectors.append(point.vector)
318
  self.fallback_metadata.append(point.payload)
@@ -328,7 +324,6 @@ class VectorDB:
328
  print(f"✅ Upserted {len(points)} points (with batching)")
329
  except Exception as e:
330
  print(f"❌ Error upserting points: {e}")
331
- # فعال کردن حالت fallback
332
  self.fallback_mode = True
333
  for point in points:
334
  self.fallback_vectors.append(point.vector)
@@ -337,7 +332,6 @@ class VectorDB:
337
 
338
  def search(self, query_vector: List[float], limit: int = 5) -> List[Dict]:
339
  """جستجوی vector"""
340
- # اگر در حالت fallback هستیم
341
  if self.fallback_mode and self.fallback_vectors:
342
  return self._search_fallback(query_vector, limit)
343
 
@@ -360,7 +354,6 @@ class VectorDB:
360
  ]
361
  except Exception as e:
362
  print(f"❌ Search error: {e}")
363
- # تلاش با fallback
364
  if self.fallback_vectors:
365
  print(f" Using in-memory fallback...")
366
  return self._search_fallback(query_vector, limit)
@@ -375,7 +368,6 @@ class VectorDB:
375
 
376
  for i, vec in enumerate(self.fallback_vectors):
377
  vec_arr = np.array(vec)
378
- # محاسبه cosine similarity
379
  similarity = np.dot(query_vec, vec_arr) / (np.linalg.norm(query_vec) * np.linalg.norm(vec_arr))
380
  results.append({
381
  "id": i,
@@ -383,7 +375,6 @@ class VectorDB:
383
  "payload": self.fallback_metadata[i]
384
  })
385
 
386
- # مرتب‌سازی بر اساس score
387
  results.sort(key=lambda x: x["score"], reverse=True)
388
  return results[:limit]
389
 
@@ -451,38 +442,58 @@ class KnowledgeGraph:
451
 
452
 
453
  # ============================================================================
454
- # QUERY UNDERSTANDING AGENT
455
  # ============================================================================
456
 
457
  class QueryUnderstandingAgent:
458
- """Agent درک query کاربر"""
459
 
460
  def __init__(self, llm: OpenRouterClient):
461
  self.llm = llm
462
 
463
- def analyze_query(self, query: str) -> QueryIntent:
464
- """تحلیل query و استخراج intent"""
 
 
 
 
 
 
 
 
465
 
466
- prompt = f"""تو یک متخصص تحلیل سوالات پزشکی هستی. سوال کاربر را تحلیل کن:
 
 
 
467
 
468
- سوال: "{query}"
469
 
470
- لطفا خروجی را به صورت JSON برگردان:
 
 
 
 
 
 
471
  {{
472
- "intent_type": "simple_lookup یا comparison یا complex_reasoning",
473
- "extracted_symptoms": ["لیست علائم"],
474
- "extracted_products": ["لیست نام محصولات"],
475
- "requires_graph": true/false,
 
 
 
 
476
  "confidence": 0.0-1.0
477
  }}
478
 
479
- فقط JSON برگردان، هیچ توضیح اضافه نده."""
480
 
481
  messages = [{"role": "user", "content": prompt}]
482
- response = self.llm.generate(messages, temperature=0.3)
483
 
484
  try:
485
- # پاک کردن markdown و استخراج JSON
486
  clean_response = response.strip()
487
  if "```json" in clean_response:
488
  clean_response = clean_response.split("```json")[1].split("```")[0]
@@ -492,21 +503,22 @@ class QueryUnderstandingAgent:
492
  intent_data = json.loads(clean_response.strip())
493
 
494
  return QueryIntent(
495
- intent_type=intent_data.get("intent_type", "simple_lookup"),
496
  extracted_symptoms=intent_data.get("extracted_symptoms", []),
497
  extracted_products=intent_data.get("extracted_products", []),
 
498
  requires_graph=intent_data.get("requires_graph", False),
499
  confidence=intent_data.get("confidence", 0.5)
500
  )
501
  except Exception as e:
502
  print(f"⚠️ Intent parsing error: {e}")
503
- # Fallback
504
  return QueryIntent(
505
- intent_type="simple_lookup",
506
  extracted_symptoms=[],
507
  extracted_products=[],
 
508
  requires_graph=False,
509
- confidence=0.5
510
  )
511
 
512
 
@@ -522,19 +534,14 @@ class RetrievalAgent:
522
  self.kg = knowledge_graph
523
  self.llm = llm
524
 
525
- def retrieve(self, query: str, intent: QueryIntent, top_k: int = 5) -> List[Dict]:
526
- """بازیابی اطلاعات بر اساس intent"""
527
 
528
- # دریافت embedding
529
  query_vector = self.llm.get_embedding(query)
530
-
531
- # جستجوی vector
532
  vector_results = self.vector_db.search(query_vector, limit=top_k)
533
 
534
- # اگر نیاز به graph داریم
535
  if intent.requires_graph and intent.extracted_symptoms:
536
  graph_results = self._graph_search(intent.extracted_symptoms)
537
- # ترکیب نتایج
538
  return self._merge_results(vector_results, graph_results)
539
 
540
  return vector_results
@@ -543,7 +550,6 @@ class RetrievalAgent:
543
  """جستجو در graph"""
544
  results = []
545
  for symptom in symptoms:
546
- # پیدا کردن node های مرتبط
547
  symptom_clean = symptom.lower().strip()
548
  related = self.kg.multi_hop_query([symptom_clean], max_hops=2)
549
  results.append({"symptom": symptom, "graph_data": related})
@@ -577,19 +583,16 @@ class GradingAgent:
577
  "relevance_score": score
578
  })
579
 
580
- # مرتب‌سازی بر اساس نمره
581
  graded_docs.sort(key=lambda x: x["relevance_score"], reverse=True)
582
  return graded_docs
583
 
584
  def _score_document(self, query: str, doc: Dict) -> float:
585
  """محاسبه نمره relevance"""
586
- # اگر از vector search اومده، از cosine score استفاده کن
587
  if "score" in doc:
588
  return doc["score"]
589
 
590
- # در غیر این صورت، از LLM بپرس
591
  try:
592
- doc_text = str(doc.get("payload", doc))[:500] # محدود کردن طول
593
 
594
  prompt = f"""این document چقدر به سوال کاربر مرتبط است؟
595
 
@@ -601,63 +604,116 @@ Document: {doc_text}
601
  messages = [{"role": "user", "content": prompt}]
602
  response = self.llm.generate(messages, temperature=0.1, max_tokens=10)
603
 
604
- # استخراج عدد
605
  score = float(re.findall(r'0\.\d+|1\.0', response)[0])
606
  return score
607
  except:
608
- return 0.5 # نمره پیش‌فرض
609
 
610
 
611
  # ============================================================================
612
- # GENERATION AGENT
613
  # ============================================================================
614
 
615
  class GenerationAgent:
616
- """Agent تولید پاسخ نهایی"""
617
 
618
  def __init__(self, llm: OpenRouterClient):
619
  self.llm = llm
620
 
621
- def generate_answer(self, query: str, context_docs: List[Dict]) -> str:
622
- """تولید پاسخ نهایی"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
 
624
- # آماده‌سازی context
625
  context = self._prepare_context(context_docs)
626
 
627
- prompt = f"""تو یک مشاور داروخانه حرفه‌ای هستی. بر اساس اطلاعات زیر به سوال کاربر پاسخ بده:
 
 
 
 
 
 
 
 
 
628
 
629
- سوال کاربر: {query}
630
 
631
- اطلاعات موجود:
632
  {context}
633
 
634
- راهنمایی‌ها:
635
- - پاسخ کامل و دقیق بده
636
- - لینک محصولات را حتما اضافه کن
637
- - اگر چند محصول هست، تفاوت‌هاشون رو توضیح بده
638
- - اگر اطلاعات کافی نداری، صادقانه بگو
 
 
 
 
 
 
 
 
639
 
640
- پاسخ:"""
 
 
641
 
642
  messages = [{"role": "user", "content": prompt}]
643
- answer = self.llm.generate(messages, temperature=0.7, max_tokens=1500)
644
 
645
- return answer
646
 
647
  def _prepare_context(self, docs: List[Dict]) -> str:
648
- """آماده‌سازی context از documents"""
649
  context_parts = []
650
 
651
- for i, doc in enumerate(docs[:5], 1): # فقط top 5
652
  payload = doc.get("payload", {})
653
 
654
- text = f"""
655
- محصول {i}:
656
- - دسته‌بندی: {payload.get('category', 'نامشخص')}
657
- - مشکل: {payload.get('problem', 'نامشخص')}
658
- - علائم: {payload.get('symptoms', 'نامشخص')}
659
- - درمان: {payload.get('treatment', 'نامشخص')}
660
- - لینک: {payload.get('url', 'ندارد')}
661
  """
662
  context_parts.append(text)
663
 
@@ -665,27 +721,25 @@ class GenerationAgent:
665
 
666
 
667
  # ============================================================================
668
- # MAIN RAG SYSTEM
669
  # ============================================================================
670
 
671
  class PharmacyRAGSystem:
672
- """سیستم RAG کامل داروخانه"""
673
 
674
  def __init__(self):
675
- print("🚀 Initializing Pharmacy RAG System...")
676
 
677
- # کلاینت‌ها
678
  self.llm = OpenRouterClient(OPENROUTER_API_KEY)
679
  self.vector_db = VectorDB(QDRANT_URL, COLLECTION_NAME)
680
  self.kg = KnowledgeGraph()
681
 
682
- # Agents
683
  self.query_agent = QueryUnderstandingAgent(self.llm)
684
  self.retrieval_agent = RetrievalAgent(self.vector_db, self.kg, self.llm)
685
  self.grading_agent = GradingAgent(self.llm)
686
  self.generation_agent = GenerationAgent(self.llm)
687
 
688
- print("✅ System initialized!")
689
 
690
  def load_data(self, csv_path: str):
691
  """بارگذاری داده‌ها از CSV"""
@@ -694,19 +748,13 @@ class PharmacyRAGSystem:
694
  df = pd.read_excel(csv_path)
695
  products = self._parse_dataframe(df)
696
 
697
- # ساخت collection
698
  self.vector_db.create_collection()
699
 
700
- # آماده‌سازی points برای Qdrant
701
  points = []
702
  for i, product in enumerate(products):
703
- # ساخت متن برای embedding
704
  text = f"{product.problem_title} {product.symptoms} {product.treatment_info}"
705
-
706
- # دریافت embedding
707
  vector = self.llm.get_embedding(text)
708
 
709
- # ساخت point
710
  point = PointStruct(
711
  id=i,
712
  vector=vector,
@@ -721,11 +769,8 @@ class PharmacyRAGSystem:
721
  }
722
  )
723
  points.append(point)
724
-
725
- # ساخت graph
726
  self._build_graph_from_product(product, i)
727
 
728
- # آپلود به Qdrant
729
  self.vector_db.upsert_points(points)
730
 
731
  print(f"✅ Loaded {len(products)} products!")
@@ -736,10 +781,7 @@ class PharmacyRAGSystem:
736
  products = []
737
 
738
  for _, row in df.iterrows():
739
- # استخراج URLs
740
  urls = re.findall(r'https://[^\s]+', str(row['محصولات پیشنهادی درمانی']))
741
-
742
- # استخراج نام محصولات
743
  product_names = re.findall(r'(?:سرم|ژل|کرم|فوم|محلول|اسپری|تونر|فلوئید)\s+[^\n]+',
744
  str(row['محصولات پیشنهادی درمانی']))
745
 
@@ -757,11 +799,9 @@ class PharmacyRAGSystem:
757
 
758
  def _build_graph_from_product(self, product: Product, product_id: int):
759
  """ساخت گراف از یک محصول"""
760
- # Node برای مشکل
761
  problem_id = f"problem_{product_id}"
762
  self.kg.add_node(problem_id, "problem", {"name": product.problem_title})
763
 
764
- # Node برای محصولات
765
  for i, url in enumerate(product.urls):
766
  product_node_id = f"product_{product_id}_{i}"
767
  product_name = product.product_names[i] if i < len(product.product_names) else f"محصول {i+1}"
@@ -771,80 +811,90 @@ class PharmacyRAGSystem:
771
  "url": url
772
  })
773
 
774
- # Edge: مشکل -> محصول
775
  self.kg.add_edge(problem_id, product_node_id, "TREATED_BY")
776
 
777
- def query(self, user_query: str) -> str:
778
- """پردازش query کاربر"""
779
  print(f"\n🔍 Processing query: {user_query}")
780
 
781
  # مرحله 1: فهم query
782
- intent = self.query_agent.analyze_query(user_query)
783
  print(f" Intent: {intent.intent_type} (confidence: {intent.confidence:.2f})")
784
 
785
- # مرحله 2: بازیابی
786
- retrieved_docs = self.retrieval_agent.retrieve(user_query, intent, top_k=5)
 
 
 
 
 
 
787
  print(f" Retrieved: {len(retrieved_docs)} documents")
788
 
789
  # مرحله 3: ارزیابی
790
  graded_docs = self.grading_agent.grade_relevance(user_query, retrieved_docs)
791
  print(f" Top score: {graded_docs[0]['relevance_score']:.2f}")
792
 
793
- # مرحله 4: تولید پاسخ
794
- answer = self.generation_agent.generate_answer(user_query, graded_docs)
795
 
796
  return answer
797
 
798
 
799
  # ============================================================================
800
- # GRADIO UI
801
  # ============================================================================
802
 
803
  def create_gradio_interface(rag_system: PharmacyRAGSystem):
804
- """ساخت رابط کاربری Gradio"""
805
 
806
  def chat(message, history):
807
- """تابع چت"""
808
  try:
809
- answer = rag_system.query(message)
 
 
 
 
 
 
 
 
 
 
810
  return answer
811
  except Exception as e:
812
  return f"❌ خطا: {str(e)}"
813
 
814
- # طراحی UI با Gradio 6.0
815
- with gr.Blocks(title="🏥 سیستم مشاوره داروخانه") as demo:
816
  gr.Markdown("""
817
- # 🏥 سیستم هوشمند مشاوره داروخانه
818
- ### مبتنی بر هوش مصنوعی - RAG با معماری Multi-Agent
819
 
820
- سوالات خود درباره مشکلات پوستی و محصولات مناسب را بپرسید!
821
  """)
822
 
823
- # استفاده از ChatInterface ساده
824
  chatbot = gr.ChatInterface(
825
  fn=chat,
826
  examples=[
827
- "محصول مناسب برای جوش سرسیاه چیه؟",
828
- "پوستم خیلی چربه و براقه، چیکار کنم؟",
829
- "تفاوت سالیسیلیک اسید و نیاسینامید چیه؟",
830
- "برای جوش‌های قرمز و دردناک چه محصولی پیشنهاد میدی؟",
831
- "محصول اقتصادی برای منافذ باز",
832
- ]
 
833
  )
834
 
835
  gr.Markdown("""
836
  ---
837
- **ویژگی‌ها:**
838
- - 🧠 درک هوشمند سوال شما
839
- - 🔍 جستجوی پیشرفته در پایگاه داده محصولات
840
- - 🕸️ استفاده از گراف دانش برای پیدا کردن روابط
841
- - خود-اصلاحی و بهبود کیفیت پاسخ
842
- - 📊 پیشنهاد محصولات با لینک مستقیم
843
-
844
- **تکنولوژی:**
845
- - LLM: GPT-4o-mini (OpenRouter)
846
- - Vector DB: Qdrant
847
- - Knowledge Graph: NetworkX
848
  """)
849
 
850
  return demo
@@ -855,18 +905,13 @@ def create_gradio_interface(rag_system: PharmacyRAGSystem):
855
  # ============================================================================
856
 
857
  if __name__ == "__main__":
858
- # ساخت سیستم
859
  rag_system = PharmacyRAGSystem()
860
 
861
- # بارگذاری داده‌ها
862
  rag_system.load_data("7590053231020941057_391109923615173.xlsx")
863
 
864
- # راه‌اندازی UI
865
  demo = create_gradio_interface(rag_system)
866
  demo.launch(
867
  server_name="0.0.0.0",
868
  server_port=7860,
869
- share=True, # برای دریافت لینک عمومی
870
- theme=gr.themes.Soft()
871
  )
872
-
 
134
  # )
135
 
136
 
137
+
138
  """
139
+ 🏥 Pharmacy RAG System - Interactive Chat Support Version
140
+ تعاملی - سوال‌محور - مشاوره‌ای
141
  """
142
 
143
  import os
 
169
  QDRANT_URL = "http://130.185.121.155:6333"
170
  COLLECTION_NAME = "pharmacy_products"
171
 
172
+ LLM_MODEL = "openai/gpt-4o-mini"
173
+ EMBEDDING_MODEL = "openai/text-embedding-3-small"
174
 
175
 
176
  # ============================================================================
 
191
  @dataclass
192
  class QueryIntent:
193
  """مدل intent شناسایی شده"""
194
+ intent_type: str
195
  extracted_symptoms: List[str]
196
  extracted_products: List[str]
197
+ missing_info: List[str]
198
  requires_graph: bool
199
  confidence: float
200
 
 
230
  return result['data'][0]['embedding']
231
  except Exception as e:
232
  print(f"❌ Embedding Error: {e}")
 
233
  return [0.0] * 1536
234
 
235
+ def generate(self, messages: List[Dict], temperature: float = 0.7, max_tokens: int = 800) -> str:
236
+ """تولید متن با LLM - محدودیت طول برای پاسخ‌های کوتاه‌تر"""
237
  url = f"{self.base_url}/chat/completions"
238
 
239
  payload = {
 
261
  """مدیریت Qdrant Vector Database"""
262
 
263
  def __init__(self, url: str, collection_name: str):
264
+ self.client = QdrantClient(url=url, timeout=60)
265
  self.collection_name = collection_name
266
  self.fallback_mode = False
267
+ self.fallback_vectors = []
268
  self.fallback_metadata = []
269
 
270
  def create_collection(self, vector_size: int = 1536):
271
  """ساخت collection"""
272
  try:
 
273
  collections = self.client.get_collections().collections
274
  if any(c.name == self.collection_name for c in collections):
275
  print(f"✅ Collection '{self.collection_name}' already exists")
276
  return
277
 
 
278
  self.client.create_collection(
279
  collection_name=self.collection_name,
280
  vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE)
 
286
  def upsert_points(self, points: List[PointStruct]):
287
  """اضافه کردن points به collection"""
288
  try:
289
+ batch_size = 5
 
290
  total = len(points)
291
  failed_batches = 0
292
 
 
294
  batch = points[i:i+batch_size]
295
  print(f" Uploading batch {i//batch_size + 1}/{(total + batch_size - 1)//batch_size}...")
296
 
 
297
  max_retries = 3
298
  batch_failed = True
299
  for attempt in range(max_retries):
 
301
  self.client.upsert(
302
  collection_name=self.collection_name,
303
  points=batch,
304
+ wait=True
305
  )
306
  batch_failed = False
307
  break
 
309
  if attempt == max_retries - 1:
310
  print(f" ⚠️ Failed batch {i//batch_size + 1}: {e}")
311
  failed_batches += 1
 
312
  for point in batch:
313
  self.fallback_vectors.append(point.vector)
314
  self.fallback_metadata.append(point.payload)
 
324
  print(f"✅ Upserted {len(points)} points (with batching)")
325
  except Exception as e:
326
  print(f"❌ Error upserting points: {e}")
 
327
  self.fallback_mode = True
328
  for point in points:
329
  self.fallback_vectors.append(point.vector)
 
332
 
333
  def search(self, query_vector: List[float], limit: int = 5) -> List[Dict]:
334
  """جستجوی vector"""
 
335
  if self.fallback_mode and self.fallback_vectors:
336
  return self._search_fallback(query_vector, limit)
337
 
 
354
  ]
355
  except Exception as e:
356
  print(f"❌ Search error: {e}")
 
357
  if self.fallback_vectors:
358
  print(f" Using in-memory fallback...")
359
  return self._search_fallback(query_vector, limit)
 
368
 
369
  for i, vec in enumerate(self.fallback_vectors):
370
  vec_arr = np.array(vec)
 
371
  similarity = np.dot(query_vec, vec_arr) / (np.linalg.norm(query_vec) * np.linalg.norm(vec_arr))
372
  results.append({
373
  "id": i,
 
375
  "payload": self.fallback_metadata[i]
376
  })
377
 
 
378
  results.sort(key=lambda x: x["score"], reverse=True)
379
  return results[:limit]
380
 
 
442
 
443
 
444
  # ============================================================================
445
+ # QUERY UNDERSTANDING AGENT (تغییر اساسی اینجا)
446
  # ============================================================================
447
 
448
  class QueryUnderstandingAgent:
449
+ """Agent درک query کاربر - نسخه تعاملی"""
450
 
451
  def __init__(self, llm: OpenRouterClient):
452
  self.llm = llm
453
 
454
+ def analyze_query(self, query: str, conversation_history: List[Dict] = None) -> QueryIntent:
455
+ """تحلیل query و تشخیص نیاز به اطلاعات بیشتر"""
456
+
457
+ history_context = ""
458
+ if conversation_history and len(conversation_history) > 1:
459
+ recent_msgs = conversation_history[-6:] # 6 پیام آخر
460
+ history_context = "\n\nتاریخچه مکالمه:\n" + "\n".join([
461
+ f"{msg['role']}: {msg['content'][:150]}"
462
+ for msg in recent_msgs
463
+ ])
464
 
465
+ prompt = f"""تو یک متخصص تحلیل گفتگوهای مشاوره داروخانه هستی.
466
+
467
+ پیام جدید کاربر: "{query}"
468
+ {history_context}
469
 
470
+ **وظیفه تو**: تشخیص بده که آیا اطلاعات کافی برای پیشنهاد محصول داریم یا نه.
471
 
472
+ اطلاعات لازم برای پیشنهاد محصول:
473
+ 1. نوع مشکل (جوش، لک، چربی زیاد، خشکی و...)
474
+ 2. نوع پوست (چرب، خشک، مختلط، حساس)
475
+ 3. شدت مشکل (خفیف، متوسط، شدید)
476
+ 4. محدودیت بودجه (اقتصادی یا نامحدود)
477
+
478
+ خروجی JSON:
479
  {{
480
+ "intent_type": "needs_clarification" یا "ready_to_recommend",
481
+ "extracted_symptoms": ["علائم ذکر شده"],
482
+ "extracted_products": ["محصولات خاص ذکر شده"],
483
+ "skin_type_mentioned": true/false,
484
+ "severity_mentioned": true/false,
485
+ "budget_mentioned": true/false,
486
+ "missing_info": ["چه اطلاعاتی کم است"],
487
+ "requires_graph": false,
488
  "confidence": 0.0-1.0
489
  }}
490
 
491
+ فقط JSON برگردان، بدون توضیح."""
492
 
493
  messages = [{"role": "user", "content": prompt}]
494
+ response = self.llm.generate(messages, temperature=0.2, max_tokens=400)
495
 
496
  try:
 
497
  clean_response = response.strip()
498
  if "```json" in clean_response:
499
  clean_response = clean_response.split("```json")[1].split("```")[0]
 
503
  intent_data = json.loads(clean_response.strip())
504
 
505
  return QueryIntent(
506
+ intent_type=intent_data.get("intent_type", "needs_clarification"),
507
  extracted_symptoms=intent_data.get("extracted_symptoms", []),
508
  extracted_products=intent_data.get("extracted_products", []),
509
+ missing_info=intent_data.get("missing_info", []),
510
  requires_graph=intent_data.get("requires_graph", False),
511
  confidence=intent_data.get("confidence", 0.5)
512
  )
513
  except Exception as e:
514
  print(f"⚠️ Intent parsing error: {e}")
 
515
  return QueryIntent(
516
+ intent_type="needs_clarification",
517
  extracted_symptoms=[],
518
  extracted_products=[],
519
+ missing_info=["نوع پوست", "شدت مشکل"],
520
  requires_graph=False,
521
+ confidence=0.3
522
  )
523
 
524
 
 
534
  self.kg = knowledge_graph
535
  self.llm = llm
536
 
537
+ def retrieve(self, query: str, intent: QueryIntent, top_k: int = 3) -> List[Dict]:
538
+ """بازیابی اطلاعات - فقط top 3 برای پاسخ کوتاه‌تر"""
539
 
 
540
  query_vector = self.llm.get_embedding(query)
 
 
541
  vector_results = self.vector_db.search(query_vector, limit=top_k)
542
 
 
543
  if intent.requires_graph and intent.extracted_symptoms:
544
  graph_results = self._graph_search(intent.extracted_symptoms)
 
545
  return self._merge_results(vector_results, graph_results)
546
 
547
  return vector_results
 
550
  """جستجو در graph"""
551
  results = []
552
  for symptom in symptoms:
 
553
  symptom_clean = symptom.lower().strip()
554
  related = self.kg.multi_hop_query([symptom_clean], max_hops=2)
555
  results.append({"symptom": symptom, "graph_data": related})
 
583
  "relevance_score": score
584
  })
585
 
 
586
  graded_docs.sort(key=lambda x: x["relevance_score"], reverse=True)
587
  return graded_docs
588
 
589
  def _score_document(self, query: str, doc: Dict) -> float:
590
  """محاسبه نمره relevance"""
 
591
  if "score" in doc:
592
  return doc["score"]
593
 
 
594
  try:
595
+ doc_text = str(doc.get("payload", doc))[:500]
596
 
597
  prompt = f"""این document چقدر به سوال کاربر مرتبط است؟
598
 
 
604
  messages = [{"role": "user", "content": prompt}]
605
  response = self.llm.generate(messages, temperature=0.1, max_tokens=10)
606
 
 
607
  score = float(re.findall(r'0\.\d+|1\.0', response)[0])
608
  return score
609
  except:
610
+ return 0.5
611
 
612
 
613
  # ============================================================================
614
+ # GENERATION AGENT (تغییر اساسی اینجا)
615
  # ============================================================================
616
 
617
  class GenerationAgent:
618
+ """Agent تولید پاسخ - نسخه تعاملی و پرسشگر"""
619
 
620
  def __init__(self, llm: OpenRouterClient):
621
  self.llm = llm
622
 
623
+ def generate_clarification(self, query: str, intent: QueryIntent, conversation_history: List[Dict] = None) -> str:
624
+ """تولید سوالات برای جمع‌آوری اطلاعات بیشتر"""
625
+
626
+ history_context = ""
627
+ if conversation_history and len(conversation_history) > 1:
628
+ history_context = "\n\nمکالمه قبلی:\n" + "\n".join([
629
+ f"{msg['role']}: {msg['content'][:100]}"
630
+ for msg in conversation_history[-4:]
631
+ ])
632
+
633
+ missing_info_str = ", ".join(intent.missing_info) if intent.missing_info else "اطلاعات تکمیلی"
634
+
635
+ prompt = f"""تو یک مشاور داروخانه دوستانه و حرفه‌ای هستی که می‌خواهی بهترین محصول رو به مشتری پیشنهاد بدی.
636
+
637
+ پیام مشتری: "{query}"
638
+ {history_context}
639
+
640
+ اطلاعات ناقص: {missing_info_str}
641
+
642
+ **وظیفه تو**:
643
+ - یک سوال کوتاه و دوستانه بپرس تا اطلاعات لازم رو جمع کنی
644
+ - فقط یک سوال در هر پیام (نه لیست سوالات!)
645
+ - خیلی گرم و صمیمی باش
646
+ - اگر مشتری قبلا چیزی گفته، بهش اشاره کن
647
+
648
+ مثال‌های خوب:
649
+ "باشه! یه سوال، پوست شما چرب هست یا خشک؟ 😊"
650
+ "عالیه! چقدر شدیده این جوش‌ها؟ یعنی زیاده یا فقط گاهی پیش میاد؟"
651
+ "متوجه شدم! به بودجه محدودیتی دارید یا می‌تونید کمی بیشتر خرج کنید؟"
652
+
653
+ پاسخ کوتاه و دوستانه:"""
654
+
655
+ messages = [{"role": "user", "content": prompt}]
656
+ response = self.llm.generate(messages, temperature=0.8, max_tokens=150)
657
+
658
+ return response.strip()
659
+
660
+ def generate_recommendation(self, query: str, context_docs: List[Dict], conversation_history: List[Dict] = None) -> str:
661
+ """تولید پیشنهاد نهایی - کوتاه و مختصر"""
662
 
 
663
  context = self._prepare_context(context_docs)
664
 
665
+ history_context = ""
666
+ if conversation_history and len(conversation_history) > 1:
667
+ history_context = "\n\nخلاصه مکالمه:\n" + "\n".join([
668
+ f"{msg['role']}: {msg['content'][:80]}"
669
+ for msg in conversation_history[-4:]
670
+ ])
671
+
672
+ prompt = f"""تو یک مشاور داروخانه حرفه‌ای هستی. الان وقت پیشنهاد نهایی است!
673
+
674
+ {history_context}
675
 
676
+ سوال نهایی: {query}
677
 
678
+ محصولات موجود:
679
  {context}
680
 
681
+ **قوانین مهم**:
682
+ 1. فقط 1-2 محصول پیشنهاد بده (نه همه!)
683
+ 2. توضیح خیلی کوتاه بده (2-3 جمله)
684
+ 3. لینک محصول رو حتما بذار
685
+ 4. اگر 2 تا پیشنهاد داری، تفاوتشون رو خیلی کوتاه بگو
686
+ 5. در آخر بپرس: "سوال دیگه‌ای دارید؟" یا "می‌خواید درباره نحوه استفاده بدونید؟"
687
+
688
+ مثال پاسخ خوب:
689
+ "برای جوش‌های سرسیاه، سرم مارگریت رو پیشنهاد می‌کنم - خیلی قوی و تخصصیه:
690
+ 🔗 [لینک محصول]
691
+
692
+ اگه بودجه محدودتره، ژل سبوما آردن هم عالیه و ارزون‌تره:
693
+ 🔗 [لینک محصول]
694
 
695
+ سوال دیگه‌ای دارید؟ 😊"
696
+
697
+ پاسخ (کوتاه و مفید):"""
698
 
699
  messages = [{"role": "user", "content": prompt}]
700
+ answer = self.llm.generate(messages, temperature=0.7, max_tokens=400)
701
 
702
+ return answer.strip()
703
 
704
  def _prepare_context(self, docs: List[Dict]) -> str:
705
+ """آماده‌سازی context از documents - خلاصه‌تر"""
706
  context_parts = []
707
 
708
+ for i, doc in enumerate(docs[:3], 1): # فقط 3 تای اول
709
  payload = doc.get("payload", {})
710
 
711
+ products_str = ", ".join(payload.get('products', ['نامشخص'])[:2]) # فقط 2 محصول اول
712
+ url = payload.get('url', payload.get('urls', [''])[0] if payload.get('urls') else '')
713
+
714
+ text = f"""محصول {i}: {products_str}
715
+ مشکل: {payload.get('problem', 'نامشخص')}
716
+ لینک: {url}
 
717
  """
718
  context_parts.append(text)
719
 
 
721
 
722
 
723
  # ============================================================================
724
+ # MAIN RAG SYSTEM (تغییر در query method)
725
  # ============================================================================
726
 
727
  class PharmacyRAGSystem:
728
+ """سیستم RAG کامل داروخانه - نسخه تعاملی"""
729
 
730
  def __init__(self):
731
+ print("🚀 Initializing Interactive Pharmacy RAG System...")
732
 
 
733
  self.llm = OpenRouterClient(OPENROUTER_API_KEY)
734
  self.vector_db = VectorDB(QDRANT_URL, COLLECTION_NAME)
735
  self.kg = KnowledgeGraph()
736
 
 
737
  self.query_agent = QueryUnderstandingAgent(self.llm)
738
  self.retrieval_agent = RetrievalAgent(self.vector_db, self.kg, self.llm)
739
  self.grading_agent = GradingAgent(self.llm)
740
  self.generation_agent = GenerationAgent(self.llm)
741
 
742
+ print("✅ Interactive System initialized!")
743
 
744
  def load_data(self, csv_path: str):
745
  """بارگذاری داده‌ها از CSV"""
 
748
  df = pd.read_excel(csv_path)
749
  products = self._parse_dataframe(df)
750
 
 
751
  self.vector_db.create_collection()
752
 
 
753
  points = []
754
  for i, product in enumerate(products):
 
755
  text = f"{product.problem_title} {product.symptoms} {product.treatment_info}"
 
 
756
  vector = self.llm.get_embedding(text)
757
 
 
758
  point = PointStruct(
759
  id=i,
760
  vector=vector,
 
769
  }
770
  )
771
  points.append(point)
 
 
772
  self._build_graph_from_product(product, i)
773
 
 
774
  self.vector_db.upsert_points(points)
775
 
776
  print(f"✅ Loaded {len(products)} products!")
 
781
  products = []
782
 
783
  for _, row in df.iterrows():
 
784
  urls = re.findall(r'https://[^\s]+', str(row['محصولات پیشنهادی درمانی']))
 
 
785
  product_names = re.findall(r'(?:سرم|ژل|کرم|فوم|محلول|اسپری|تونر|فلوئید)\s+[^\n]+',
786
  str(row['محصولات پیشنهادی درمانی']))
787
 
 
799
 
800
  def _build_graph_from_product(self, product: Product, product_id: int):
801
  """ساخت گراف از یک محصول"""
 
802
  problem_id = f"problem_{product_id}"
803
  self.kg.add_node(problem_id, "problem", {"name": product.problem_title})
804
 
 
805
  for i, url in enumerate(product.urls):
806
  product_node_id = f"product_{product_id}_{i}"
807
  product_name = product.product_names[i] if i < len(product.product_names) else f"محصول {i+1}"
 
811
  "url": url
812
  })
813
 
 
814
  self.kg.add_edge(problem_id, product_node_id, "TREATED_BY")
815
 
816
+ def query(self, user_query: str, conversation_history: List[Dict] = None) -> str:
817
+ """پردازش query کاربر - با رویکرد تعاملی"""
818
  print(f"\n🔍 Processing query: {user_query}")
819
 
820
  # مرحله 1: فهم query
821
+ intent = self.query_agent.analyze_query(user_query, conversation_history)
822
  print(f" Intent: {intent.intent_type} (confidence: {intent.confidence:.2f})")
823
 
824
+ # **تصمیم‌گیری: سوال بپرس یا پاسخ بده؟**
825
+ if intent.intent_type == "needs_clarification" and intent.confidence > 0.4:
826
+ # نیاز به سوال داریم
827
+ print(f" -> Need more info: {intent.missing_info}")
828
+ return self.generation_agent.generate_clarification(user_query, intent, conversation_history)
829
+
830
+ # مرحله 2: بازیابی (فقط اگر آماده پیشنهاد هستیم)
831
+ retrieved_docs = self.retrieval_agent.retrieve(user_query, intent, top_k=3)
832
  print(f" Retrieved: {len(retrieved_docs)} documents")
833
 
834
  # مرحله 3: ارزیابی
835
  graded_docs = self.grading_agent.grade_relevance(user_query, retrieved_docs)
836
  print(f" Top score: {graded_docs[0]['relevance_score']:.2f}")
837
 
838
+ # مرحله 4: تولید پاسخ نهایی
839
+ answer = self.generation_agent.generate_recommendation(user_query, graded_docs, conversation_history)
840
 
841
  return answer
842
 
843
 
844
  # ============================================================================
845
+ # GRADIO UI (تغییر برای نگهداری تاریخچه)
846
  # ============================================================================
847
 
848
  def create_gradio_interface(rag_system: PharmacyRAGSystem):
849
+ """ساخت رابط کاربری Gradio - با تاریخچه مکالمه"""
850
 
851
  def chat(message, history):
852
+ """تابع چت با تاریخچه"""
853
  try:
854
+ # تبدیل history به فرمت مورد نیاز
855
+ conversation_history = []
856
+ for h in history:
857
+ conversation_history.append({"role": "user", "content": h[0]})
858
+ conversation_history.append({"role": "assistant", "content": h[1]})
859
+
860
+ # اضافه کردن پیام جدید
861
+ conversation_history.append({"role": "user", "content": message})
862
+
863
+ # دریافت پاسخ
864
+ answer = rag_system.query(message, conversation_history)
865
  return answer
866
  except Exception as e:
867
  return f"❌ خطا: {str(e)}"
868
 
869
+ with gr.Blocks(title="🏥 مشاور هوشمند داروخانه", theme=gr.themes.Soft()) as demo:
 
870
  gr.Markdown("""
871
+ # 🏥 مشاور هوشمند داروخانه
872
+ ### چت پشتیبانی تعاملی - با هوش مصنوعی
873
 
874
+ سلام! من دستیار شما هستم. می‌خوام بهترین محصول رو برای شما پیدا کنم 😊
875
  """)
876
 
 
877
  chatbot = gr.ChatInterface(
878
  fn=chat,
879
  examples=[
880
+ "سلام، برای جوش صورتم چیکار کنم؟",
881
+ "پوستم خیلی چربه",
882
+ ه چیز اقتصادی می‌خوام",
883
+ "میخوام منافذ پوستم کوچیک بشه",
884
+ ],
885
+ title="",
886
+ description="با من چت کنید تا بهترین محصول رو پیدا کنیم!",
887
  )
888
 
889
  gr.Markdown("""
890
  ---
891
+ **این سیستم چطور کار میکنه؟**
892
+ 1. 🤔 سوالات شما رو می‌فهمه
893
+ 2. سوالات هدفمند می‌پرسه تا بهترین محصول رو پیدا کنه
894
+ 3. 🎯 فقط 1-2 محصول مناسب پیشنهاد می‌ده (نه همه محصولات!)
895
+ 4. 💬 مثل یک مشاور واقعی باهاتون صحبت می‌کنه
896
+
897
+ **تکنولوژی:** GPT-4o-mini + Qdrant + NetworkX
 
 
 
 
898
  """)
899
 
900
  return demo
 
905
  # ============================================================================
906
 
907
  if __name__ == "__main__":
 
908
  rag_system = PharmacyRAGSystem()
909
 
 
910
  rag_system.load_data("7590053231020941057_391109923615173.xlsx")
911
 
 
912
  demo = create_gradio_interface(rag_system)
913
  demo.launch(
914
  server_name="0.0.0.0",
915
  server_port=7860,
916
+ share=True,
 
917
  )