|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
🏥 Pharmacy RAG System - Interactive Chat Support Version |
|
|
تعاملی - سوالمحور - مشاورهای |
|
|
""" |
|
|
|
|
|
import os |
|
|
import json |
|
|
import re |
|
|
from typing import List, Dict, Any, Optional |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from dataclasses import dataclass |
|
|
import networkx as nx |
|
|
from collections import defaultdict |
|
|
|
|
|
|
|
|
from qdrant_client import QdrantClient |
|
|
from qdrant_client.models import Distance, VectorParams, PointStruct |
|
|
|
|
|
|
|
|
import requests |
|
|
|
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OPENROUTER_API_KEY = "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" |
|
|
QDRANT_URL = "http://130.185.121.155:6333" |
|
|
COLLECTION_NAME = "pharmacy_products" |
|
|
|
|
|
LLM_MODEL = "openai/gpt-4o-mini" |
|
|
EMBEDDING_MODEL = "openai/text-embedding-3-small" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class Product: |
|
|
"""مدل محصول""" |
|
|
category: str |
|
|
problem_title: str |
|
|
symptoms: str |
|
|
treatment_info: str |
|
|
urls: List[str] |
|
|
product_names: List[str] |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class QueryIntent: |
|
|
"""مدل intent شناسایی شده""" |
|
|
intent_type: str |
|
|
extracted_symptoms: List[str] |
|
|
extracted_products: List[str] |
|
|
missing_info: List[str] |
|
|
requires_graph: bool |
|
|
confidence: float |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenRouterClient: |
|
|
"""کلاینت OpenRouter برای LLM و Embedding""" |
|
|
|
|
|
def __init__(self, api_key: str): |
|
|
self.api_key = api_key |
|
|
self.base_url = "https://openrouter.ai/api/v1" |
|
|
self.headers = { |
|
|
"Authorization": f"Bearer {api_key}", |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
|
|
|
def get_embedding(self, text: str) -> List[float]: |
|
|
"""دریافت embedding از OpenRouter""" |
|
|
url = f"{self.base_url}/embeddings" |
|
|
|
|
|
payload = { |
|
|
"model": EMBEDDING_MODEL, |
|
|
"input": text |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.post(url, headers=self.headers, json=payload, timeout=30) |
|
|
response.raise_for_status() |
|
|
result = response.json() |
|
|
return result['data'][0]['embedding'] |
|
|
except Exception as e: |
|
|
print(f"❌ Embedding Error: {e}") |
|
|
return [0.0] * 1536 |
|
|
|
|
|
def generate(self, messages: List[Dict], temperature: float = 0.7, max_tokens: int = 800) -> str: |
|
|
"""تولید متن با LLM - محدودیت طول برای پاسخهای کوتاهتر""" |
|
|
url = f"{self.base_url}/chat/completions" |
|
|
|
|
|
payload = { |
|
|
"model": LLM_MODEL, |
|
|
"messages": messages, |
|
|
"temperature": temperature, |
|
|
"max_tokens": max_tokens |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.post(url, headers=self.headers, json=payload, timeout=60) |
|
|
response.raise_for_status() |
|
|
result = response.json() |
|
|
return result['choices'][0]['message']['content'] |
|
|
except Exception as e: |
|
|
print(f"❌ LLM Error: {e}") |
|
|
return f"خطا در ارتباط با مدل: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VectorDB: |
|
|
"""مدیریت Qdrant Vector Database""" |
|
|
|
|
|
def __init__(self, url: str, collection_name: str): |
|
|
self.client = QdrantClient(url=url, timeout=60) |
|
|
self.collection_name = collection_name |
|
|
self.fallback_mode = False |
|
|
self.fallback_vectors = [] |
|
|
self.fallback_metadata = [] |
|
|
|
|
|
def create_collection(self, vector_size: int = 1536): |
|
|
"""ساخت collection""" |
|
|
try: |
|
|
collections = self.client.get_collections().collections |
|
|
if any(c.name == self.collection_name for c in collections): |
|
|
print(f"✅ Collection '{self.collection_name}' already exists") |
|
|
return |
|
|
|
|
|
self.client.create_collection( |
|
|
collection_name=self.collection_name, |
|
|
vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE) |
|
|
) |
|
|
print(f"✅ Created collection: {self.collection_name}") |
|
|
except Exception as e: |
|
|
print(f"❌ Error creating collection: {e}") |
|
|
|
|
|
def upsert_points(self, points: List[PointStruct]): |
|
|
"""اضافه کردن points به collection""" |
|
|
try: |
|
|
batch_size = 5 |
|
|
total = len(points) |
|
|
failed_batches = 0 |
|
|
|
|
|
for i in range(0, total, batch_size): |
|
|
batch = points[i:i+batch_size] |
|
|
print(f" Uploading batch {i//batch_size + 1}/{(total + batch_size - 1)//batch_size}...") |
|
|
|
|
|
max_retries = 3 |
|
|
batch_failed = True |
|
|
for attempt in range(max_retries): |
|
|
try: |
|
|
self.client.upsert( |
|
|
collection_name=self.collection_name, |
|
|
points=batch, |
|
|
wait=True |
|
|
) |
|
|
batch_failed = False |
|
|
break |
|
|
except Exception as e: |
|
|
if attempt == max_retries - 1: |
|
|
print(f" ⚠️ Failed batch {i//batch_size + 1}: {e}") |
|
|
failed_batches += 1 |
|
|
for point in batch: |
|
|
self.fallback_vectors.append(point.vector) |
|
|
self.fallback_metadata.append(point.payload) |
|
|
else: |
|
|
print(f" ⚠️ Retry {attempt + 1}/{max_retries}...") |
|
|
import time |
|
|
time.sleep(2) |
|
|
|
|
|
if failed_batches > 0: |
|
|
print(f"⚠️ {failed_batches} batches failed - using in-memory fallback") |
|
|
self.fallback_mode = True |
|
|
|
|
|
print(f"✅ Upserted {len(points)} points (with batching)") |
|
|
except Exception as e: |
|
|
print(f"❌ Error upserting points: {e}") |
|
|
self.fallback_mode = True |
|
|
for point in points: |
|
|
self.fallback_vectors.append(point.vector) |
|
|
self.fallback_metadata.append(point.payload) |
|
|
print(f"⚠️ Switched to in-memory fallback mode") |
|
|
|
|
|
def search(self, query_vector: List[float], limit: int = 5) -> List[Dict]: |
|
|
"""جستجوی vector""" |
|
|
if self.fallback_mode and self.fallback_vectors: |
|
|
return self._search_fallback(query_vector, limit) |
|
|
|
|
|
try: |
|
|
from qdrant_client.models import SearchRequest |
|
|
|
|
|
results = self.client.query_points( |
|
|
collection_name=self.collection_name, |
|
|
query=query_vector, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
return [ |
|
|
{ |
|
|
"id": hit.id, |
|
|
"score": hit.score, |
|
|
"payload": hit.payload |
|
|
} |
|
|
for hit in results.points |
|
|
] |
|
|
except Exception as e: |
|
|
print(f"❌ Search error: {e}") |
|
|
if self.fallback_vectors: |
|
|
print(f" Using in-memory fallback...") |
|
|
return self._search_fallback(query_vector, limit) |
|
|
return [] |
|
|
|
|
|
def _search_fallback(self, query_vector: List[float], limit: int = 5) -> List[Dict]: |
|
|
"""جستجوی fallback با محاسبه cosine similarity""" |
|
|
import numpy as np |
|
|
|
|
|
query_vec = np.array(query_vector) |
|
|
results = [] |
|
|
|
|
|
for i, vec in enumerate(self.fallback_vectors): |
|
|
vec_arr = np.array(vec) |
|
|
similarity = np.dot(query_vec, vec_arr) / (np.linalg.norm(query_vec) * np.linalg.norm(vec_arr)) |
|
|
results.append({ |
|
|
"id": i, |
|
|
"score": float(similarity), |
|
|
"payload": self.fallback_metadata[i] |
|
|
}) |
|
|
|
|
|
results.sort(key=lambda x: x["score"], reverse=True) |
|
|
return results[:limit] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KnowledgeGraph: |
|
|
"""گراف دانش برای روابط بین مشکلات، اکتیوها و محصولات""" |
|
|
|
|
|
def __init__(self): |
|
|
self.graph = nx.DiGraph() |
|
|
|
|
|
def add_node(self, node_id: str, node_type: str, attributes: Dict): |
|
|
"""اضافه کردن node""" |
|
|
self.graph.add_node(node_id, type=node_type, **attributes) |
|
|
|
|
|
def add_edge(self, source: str, target: str, relation: str): |
|
|
"""اضافه کردن edge""" |
|
|
self.graph.add_edge(source, target, relation=relation) |
|
|
|
|
|
def find_path(self, start: str, end: str) -> List[str]: |
|
|
"""پیدا کردن مسیر بین دو node""" |
|
|
try: |
|
|
return nx.shortest_path(self.graph, start, end) |
|
|
except: |
|
|
return [] |
|
|
|
|
|
def get_neighbors(self, node: str, relation: str = None) -> List[str]: |
|
|
"""دریافت همسایگان یک node""" |
|
|
if node not in self.graph: |
|
|
return [] |
|
|
|
|
|
neighbors = [] |
|
|
for _, target, data in self.graph.out_edges(node, data=True): |
|
|
if relation is None or data.get('relation') == relation: |
|
|
neighbors.append(target) |
|
|
return neighbors |
|
|
|
|
|
def multi_hop_query(self, start_nodes: List[str], max_hops: int = 3) -> Dict: |
|
|
"""پرسوجوی چند مرحلهای""" |
|
|
result = defaultdict(list) |
|
|
visited = set() |
|
|
|
|
|
def dfs(node, depth): |
|
|
if depth > max_hops or node in visited: |
|
|
return |
|
|
visited.add(node) |
|
|
|
|
|
node_data = self.graph.nodes.get(node, {}) |
|
|
result[depth].append({ |
|
|
"node": node, |
|
|
"type": node_data.get("type"), |
|
|
"data": node_data |
|
|
}) |
|
|
|
|
|
for neighbor in self.graph.neighbors(node): |
|
|
dfs(neighbor, depth + 1) |
|
|
|
|
|
for start in start_nodes: |
|
|
dfs(start, 0) |
|
|
|
|
|
return dict(result) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QueryUnderstandingAgent: |
|
|
"""Agent درک query کاربر - نسخه تعاملی""" |
|
|
|
|
|
def __init__(self, llm: OpenRouterClient): |
|
|
self.llm = llm |
|
|
|
|
|
def analyze_query(self, query: str, conversation_history: List[Dict] = None) -> QueryIntent: |
|
|
"""تحلیل query و تشخیص نیاز به اطلاعات بیشتر""" |
|
|
|
|
|
history_context = "" |
|
|
if conversation_history and len(conversation_history) > 1: |
|
|
recent_msgs = conversation_history[-6:] |
|
|
history_context = "\n\nتاریخچه مکالمه:\n" + "\n".join([ |
|
|
f"{msg['role']}: {msg['content'][:150]}" |
|
|
for msg in recent_msgs |
|
|
]) |
|
|
|
|
|
prompt = f"""تو یک متخصص تحلیل گفتگوهای مشاوره داروخانه هستی. |
|
|
|
|
|
پیام جدید کاربر: "{query}" |
|
|
{history_context} |
|
|
|
|
|
**وظیفه تو**: تشخیص بده که آیا اطلاعات کافی برای پیشنهاد محصول داریم یا نه. |
|
|
|
|
|
اطلاعات لازم برای پیشنهاد محصول: |
|
|
1. نوع مشکل (جوش، لک، چربی زیاد، خشکی و...) |
|
|
2. نوع پوست (چرب، خشک، مختلط، حساس) |
|
|
3. شدت مشکل (خفیف، متوسط، شدید) |
|
|
4. محدودیت بودجه (اقتصادی یا نامحدود) |
|
|
|
|
|
خروجی JSON: |
|
|
{{ |
|
|
"intent_type": "needs_clarification" یا "ready_to_recommend", |
|
|
"extracted_symptoms": ["علائم ذکر شده"], |
|
|
"extracted_products": ["محصولات خاص ذکر شده"], |
|
|
"skin_type_mentioned": true/false, |
|
|
"severity_mentioned": true/false, |
|
|
"budget_mentioned": true/false, |
|
|
"missing_info": ["چه اطلاعاتی کم است"], |
|
|
"requires_graph": false, |
|
|
"confidence": 0.0-1.0 |
|
|
}} |
|
|
|
|
|
فقط JSON برگردان، بدون توضیح.""" |
|
|
|
|
|
messages = [{"role": "user", "content": prompt}] |
|
|
response = self.llm.generate(messages, temperature=0.2, max_tokens=400) |
|
|
|
|
|
try: |
|
|
clean_response = response.strip() |
|
|
if "```json" in clean_response: |
|
|
clean_response = clean_response.split("```json")[1].split("```")[0] |
|
|
elif "```" in clean_response: |
|
|
clean_response = clean_response.split("```")[1].split("```")[0] |
|
|
|
|
|
intent_data = json.loads(clean_response.strip()) |
|
|
|
|
|
return QueryIntent( |
|
|
intent_type=intent_data.get("intent_type", "needs_clarification"), |
|
|
extracted_symptoms=intent_data.get("extracted_symptoms", []), |
|
|
extracted_products=intent_data.get("extracted_products", []), |
|
|
missing_info=intent_data.get("missing_info", []), |
|
|
requires_graph=intent_data.get("requires_graph", False), |
|
|
confidence=intent_data.get("confidence", 0.5) |
|
|
) |
|
|
except Exception as e: |
|
|
print(f"⚠️ Intent parsing error: {e}") |
|
|
return QueryIntent( |
|
|
intent_type="needs_clarification", |
|
|
extracted_symptoms=[], |
|
|
extracted_products=[], |
|
|
missing_info=["نوع پوست", "شدت مشکل"], |
|
|
requires_graph=False, |
|
|
confidence=0.3 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RetrievalAgent: |
|
|
"""Agent بازیابی اطلاعات""" |
|
|
|
|
|
def __init__(self, vector_db: VectorDB, knowledge_graph: KnowledgeGraph, llm: OpenRouterClient): |
|
|
self.vector_db = vector_db |
|
|
self.kg = knowledge_graph |
|
|
self.llm = llm |
|
|
|
|
|
def retrieve(self, query: str, intent: QueryIntent, top_k: int = 3) -> List[Dict]: |
|
|
"""بازیابی اطلاعات - فقط top 3 برای پاسخ کوتاهتر""" |
|
|
|
|
|
query_vector = self.llm.get_embedding(query) |
|
|
vector_results = self.vector_db.search(query_vector, limit=top_k) |
|
|
|
|
|
if intent.requires_graph and intent.extracted_symptoms: |
|
|
graph_results = self._graph_search(intent.extracted_symptoms) |
|
|
return self._merge_results(vector_results, graph_results) |
|
|
|
|
|
return vector_results |
|
|
|
|
|
def _graph_search(self, symptoms: List[str]) -> List[Dict]: |
|
|
"""جستجو در graph""" |
|
|
results = [] |
|
|
for symptom in symptoms: |
|
|
symptom_clean = symptom.lower().strip() |
|
|
related = self.kg.multi_hop_query([symptom_clean], max_hops=2) |
|
|
results.append({"symptom": symptom, "graph_data": related}) |
|
|
return results |
|
|
|
|
|
def _merge_results(self, vector_results: List[Dict], graph_results: List[Dict]) -> List[Dict]: |
|
|
"""ترکیب نتایج vector و graph""" |
|
|
merged = vector_results.copy() |
|
|
merged.extend([{"source": "graph", "data": gr} for gr in graph_results]) |
|
|
return merged |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GradingAgent: |
|
|
"""Agent ارزیابی کیفیت نتایج""" |
|
|
|
|
|
def __init__(self, llm: OpenRouterClient): |
|
|
self.llm = llm |
|
|
|
|
|
def grade_relevance(self, query: str, retrieved_docs: List[Dict]) -> List[Dict]: |
|
|
"""نمرهدهی به مرتبط بودن documents""" |
|
|
|
|
|
graded_docs = [] |
|
|
for doc in retrieved_docs: |
|
|
score = self._score_document(query, doc) |
|
|
graded_docs.append({ |
|
|
**doc, |
|
|
"relevance_score": score |
|
|
}) |
|
|
|
|
|
graded_docs.sort(key=lambda x: x["relevance_score"], reverse=True) |
|
|
return graded_docs |
|
|
|
|
|
def _score_document(self, query: str, doc: Dict) -> float: |
|
|
"""محاسبه نمره relevance""" |
|
|
if "score" in doc: |
|
|
return doc["score"] |
|
|
|
|
|
try: |
|
|
doc_text = str(doc.get("payload", doc))[:500] |
|
|
|
|
|
prompt = f"""این document چقدر به سوال کاربر مرتبط است؟ |
|
|
|
|
|
سوال: {query} |
|
|
Document: {doc_text} |
|
|
|
|
|
فقط یک عدد بین 0.0 تا 1.0 برگردان (مثلا 0.85)""" |
|
|
|
|
|
messages = [{"role": "user", "content": prompt}] |
|
|
response = self.llm.generate(messages, temperature=0.1, max_tokens=10) |
|
|
|
|
|
score = float(re.findall(r'0\.\d+|1\.0', response)[0]) |
|
|
return score |
|
|
except: |
|
|
return 0.5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GenerationAgent: |
|
|
"""Agent تولید پاسخ - نسخه تعاملی و پرسشگر""" |
|
|
|
|
|
def __init__(self, llm: OpenRouterClient): |
|
|
self.llm = llm |
|
|
|
|
|
def generate_clarification(self, query: str, intent: QueryIntent, conversation_history: List[Dict] = None) -> str: |
|
|
"""تولید سوالات برای جمعآوری اطلاعات بیشتر""" |
|
|
|
|
|
history_context = "" |
|
|
if conversation_history and len(conversation_history) > 1: |
|
|
history_context = "\n\nمکالمه قبلی:\n" + "\n".join([ |
|
|
f"{msg['role']}: {msg['content'][:100]}" |
|
|
for msg in conversation_history[-4:] |
|
|
]) |
|
|
|
|
|
missing_info_str = ", ".join(intent.missing_info) if intent.missing_info else "اطلاعات تکمیلی" |
|
|
|
|
|
prompt = f"""تو یک مشاور داروخانه دوستانه و حرفهای هستی که میخواهی بهترین محصول رو به مشتری پیشنهاد بدی. |
|
|
|
|
|
پیام مشتری: "{query}" |
|
|
{history_context} |
|
|
|
|
|
اطلاعات ناقص: {missing_info_str} |
|
|
|
|
|
**وظیفه تو**: |
|
|
- یک سوال کوتاه و دوستانه بپرس تا اطلاعات لازم رو جمع کنی |
|
|
- فقط یک سوال در هر پیام (نه لیست سوالات!) |
|
|
- خیلی گرم و صمیمی باش |
|
|
- اگر مشتری قبلا چیزی گفته، بهش اشاره کن |
|
|
|
|
|
مثالهای خوب: |
|
|
"باشه! یه سوال، پوست شما چرب هست یا خشک؟ 😊" |
|
|
"عالیه! چقدر شدیده این جوشها؟ یعنی زیاده یا فقط گاهی پیش میاد؟" |
|
|
"متوجه شدم! به بودجه محدودیتی دارید یا میتونید کمی بیشتر خرج کنید؟" |
|
|
|
|
|
پاسخ کوتاه و دوستانه:""" |
|
|
|
|
|
messages = [{"role": "user", "content": prompt}] |
|
|
response = self.llm.generate(messages, temperature=0.8, max_tokens=150) |
|
|
|
|
|
return response.strip() |
|
|
|
|
|
def generate_recommendation(self, query: str, context_docs: List[Dict], conversation_history: List[Dict] = None) -> str: |
|
|
"""تولید پیشنهاد نهایی - کوتاه و مختصر""" |
|
|
|
|
|
context = self._prepare_context(context_docs) |
|
|
|
|
|
history_context = "" |
|
|
if conversation_history and len(conversation_history) > 1: |
|
|
history_context = "\n\nخلاصه مکالمه:\n" + "\n".join([ |
|
|
f"{msg['role']}: {msg['content'][:80]}" |
|
|
for msg in conversation_history[-4:] |
|
|
]) |
|
|
|
|
|
prompt = f"""تو یک مشاور داروخانه حرفهای هستی. الان وقت پیشنهاد نهایی است! |
|
|
|
|
|
{history_context} |
|
|
|
|
|
سوال نهایی: {query} |
|
|
|
|
|
محصولات موجود: |
|
|
{context} |
|
|
|
|
|
**قوانین مهم**: |
|
|
1. فقط 1-2 محصول پیشنهاد بده (نه همه!) |
|
|
2. توضیح خیلی کوتاه بده (2-3 جمله) |
|
|
3. لینک محصول رو حتما بذار |
|
|
4. اگر 2 تا پیشنهاد داری، تفاوتشون رو خیلی کوتاه بگو |
|
|
5. در آخر بپرس: "سوال دیگهای دارید؟" یا "میخواید درباره نحوه استفاده بدونید؟" |
|
|
|
|
|
مثال پاسخ خوب: |
|
|
"برای جوشهای سرسیاه، سرم مارگریت رو پیشنهاد میکنم - خیلی قوی و تخصصیه: |
|
|
🔗 [لینک محصول] |
|
|
|
|
|
اگه بودجه محدودتره، ژل سبوما آردن هم عالیه و ارزونتره: |
|
|
🔗 [لینک محصول] |
|
|
|
|
|
سوال دیگهای دارید؟ 😊" |
|
|
|
|
|
پاسخ (کوتاه و مفید):""" |
|
|
|
|
|
messages = [{"role": "user", "content": prompt}] |
|
|
answer = self.llm.generate(messages, temperature=0.7, max_tokens=400) |
|
|
|
|
|
return answer.strip() |
|
|
|
|
|
def _prepare_context(self, docs: List[Dict]) -> str: |
|
|
"""آمادهسازی context از documents - خلاصهتر""" |
|
|
context_parts = [] |
|
|
|
|
|
for i, doc in enumerate(docs[:3], 1): |
|
|
payload = doc.get("payload", {}) |
|
|
|
|
|
products_str = ", ".join(payload.get('products', ['نامشخص'])[:2]) |
|
|
url = payload.get('url', payload.get('urls', [''])[0] if payload.get('urls') else '') |
|
|
|
|
|
text = f"""محصول {i}: {products_str} |
|
|
مشکل: {payload.get('problem', 'نامشخص')} |
|
|
لینک: {url} |
|
|
""" |
|
|
context_parts.append(text) |
|
|
|
|
|
return "\n".join(context_parts) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PharmacyRAGSystem: |
|
|
"""سیستم RAG کامل داروخانه - نسخه تعاملی""" |
|
|
|
|
|
def __init__(self): |
|
|
print("🚀 Initializing Interactive Pharmacy RAG System...") |
|
|
|
|
|
self.llm = OpenRouterClient(OPENROUTER_API_KEY) |
|
|
self.vector_db = VectorDB(QDRANT_URL, COLLECTION_NAME) |
|
|
self.kg = KnowledgeGraph() |
|
|
|
|
|
self.query_agent = QueryUnderstandingAgent(self.llm) |
|
|
self.retrieval_agent = RetrievalAgent(self.vector_db, self.kg, self.llm) |
|
|
self.grading_agent = GradingAgent(self.llm) |
|
|
self.generation_agent = GenerationAgent(self.llm) |
|
|
|
|
|
print("✅ Interactive System initialized!") |
|
|
|
|
|
def load_data(self, csv_path: str): |
|
|
"""بارگذاری دادهها از CSV""" |
|
|
print(f"📊 Loading data from {csv_path}...") |
|
|
|
|
|
df = pd.read_excel(csv_path) |
|
|
products = self._parse_dataframe(df) |
|
|
|
|
|
self.vector_db.create_collection() |
|
|
|
|
|
points = [] |
|
|
for i, product in enumerate(products): |
|
|
text = f"{product.problem_title} {product.symptoms} {product.treatment_info}" |
|
|
vector = self.llm.get_embedding(text) |
|
|
|
|
|
point = PointStruct( |
|
|
id=i, |
|
|
vector=vector, |
|
|
payload={ |
|
|
"category": product.category, |
|
|
"problem": product.problem_title, |
|
|
"symptoms": product.symptoms, |
|
|
"treatment": product.treatment_info, |
|
|
"urls": product.urls, |
|
|
"products": product.product_names, |
|
|
"url": product.urls[0] if product.urls else "" |
|
|
} |
|
|
) |
|
|
points.append(point) |
|
|
self._build_graph_from_product(product, i) |
|
|
|
|
|
self.vector_db.upsert_points(points) |
|
|
|
|
|
print(f"✅ Loaded {len(products)} products!") |
|
|
print(f"✅ Graph has {self.kg.graph.number_of_nodes()} nodes and {self.kg.graph.number_of_edges()} edges") |
|
|
|
|
|
def _parse_dataframe(self, df: pd.DataFrame) -> List[Product]: |
|
|
"""تبدیل DataFrame به لیست Product""" |
|
|
products = [] |
|
|
|
|
|
for _, row in df.iterrows(): |
|
|
urls = re.findall(r'https://[^\s]+', str(row['محصولات پیشنهادی درمانی'])) |
|
|
product_names = re.findall(r'(?:سرم|ژل|کرم|فوم|محلول|اسپری|تونر|فلوئید)\s+[^\n]+', |
|
|
str(row['محصولات پیشنهادی درمانی'])) |
|
|
|
|
|
product = Product( |
|
|
category=str(row['دسته بندی کلی مشکل']), |
|
|
problem_title=str(row['عنوان مشکلات']), |
|
|
symptoms=str(row['توضیحات']), |
|
|
treatment_info=str(row['محصولات پیشنهادی درمانی']), |
|
|
urls=urls, |
|
|
product_names=product_names |
|
|
) |
|
|
products.append(product) |
|
|
|
|
|
return products |
|
|
|
|
|
def _build_graph_from_product(self, product: Product, product_id: int): |
|
|
"""ساخت گراف از یک محصول""" |
|
|
problem_id = f"problem_{product_id}" |
|
|
self.kg.add_node(problem_id, "problem", {"name": product.problem_title}) |
|
|
|
|
|
for i, url in enumerate(product.urls): |
|
|
product_node_id = f"product_{product_id}_{i}" |
|
|
product_name = product.product_names[i] if i < len(product.product_names) else f"محصول {i+1}" |
|
|
|
|
|
self.kg.add_node(product_node_id, "product", { |
|
|
"name": product_name, |
|
|
"url": url |
|
|
}) |
|
|
|
|
|
self.kg.add_edge(problem_id, product_node_id, "TREATED_BY") |
|
|
|
|
|
def query(self, user_query: str, conversation_history: List[Dict] = None) -> str: |
|
|
"""پردازش query کاربر - با رویکرد تعاملی""" |
|
|
print(f"\n🔍 Processing query: {user_query}") |
|
|
|
|
|
|
|
|
intent = self.query_agent.analyze_query(user_query, conversation_history) |
|
|
print(f" Intent: {intent.intent_type} (confidence: {intent.confidence:.2f})") |
|
|
|
|
|
|
|
|
if intent.intent_type == "needs_clarification" and intent.confidence > 0.4: |
|
|
|
|
|
print(f" -> Need more info: {intent.missing_info}") |
|
|
return self.generation_agent.generate_clarification(user_query, intent, conversation_history) |
|
|
|
|
|
|
|
|
retrieved_docs = self.retrieval_agent.retrieve(user_query, intent, top_k=3) |
|
|
print(f" Retrieved: {len(retrieved_docs)} documents") |
|
|
|
|
|
|
|
|
graded_docs = self.grading_agent.grade_relevance(user_query, retrieved_docs) |
|
|
print(f" Top score: {graded_docs[0]['relevance_score']:.2f}") |
|
|
|
|
|
|
|
|
answer = self.generation_agent.generate_recommendation(user_query, graded_docs, conversation_history) |
|
|
|
|
|
return answer |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_gradio_interface(rag_system: PharmacyRAGSystem): |
|
|
"""ساخت رابط کاربری Gradio - با تاریخچه مکالمه""" |
|
|
|
|
|
def chat(message, history): |
|
|
"""تابع چت با تاریخچه""" |
|
|
try: |
|
|
|
|
|
conversation_history = [] |
|
|
for h in history: |
|
|
conversation_history.append({"role": "user", "content": h[0]}) |
|
|
conversation_history.append({"role": "assistant", "content": h[1]}) |
|
|
|
|
|
|
|
|
conversation_history.append({"role": "user", "content": message}) |
|
|
|
|
|
|
|
|
answer = rag_system.query(message, conversation_history) |
|
|
return answer |
|
|
except Exception as e: |
|
|
return f"❌ خطا: {str(e)}" |
|
|
|
|
|
with gr.Blocks(title="🏥 مشاور هوشمند داروخانه") as demo: |
|
|
gr.Markdown(""" |
|
|
# 🏥 مشاور هوشمند داروخانه |
|
|
### چت پشتیبانی تعاملی - با هوش مصنوعی |
|
|
|
|
|
سلام! من دستیار شما هستم. میخوام بهترین محصول رو برای شما پیدا کنم 😊 |
|
|
""") |
|
|
|
|
|
chatbot = gr.ChatInterface( |
|
|
fn=chat, |
|
|
examples=[ |
|
|
"سلام، برای جوش صورتم چیکار کنم؟", |
|
|
"پوستم خیلی چربه", |
|
|
"یه چیز اقتصادی میخوام", |
|
|
"میخوام منافذ پوستم کوچیک بشه", |
|
|
], |
|
|
title="", |
|
|
description="با من چت کنید تا بهترین محصول رو پیدا کنیم!", |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
**این سیستم چطور کار میکنه؟** |
|
|
1. 🤔 سوالات شما رو میفهمه |
|
|
2. ❓ سوالات هدفمند میپرسه تا بهترین محصول رو پیدا کنه |
|
|
3. 🎯 فقط 1-2 محصول مناسب پیشنهاد میده (نه همه محصولات!) |
|
|
4. 💬 مثل یک مشاور واقعی باهاتون صحبت میکنه |
|
|
|
|
|
**تکنولوژی:** GPT-4o-mini + Qdrant + NetworkX |
|
|
""") |
|
|
|
|
|
return demo |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
rag_system = PharmacyRAGSystem() |
|
|
|
|
|
rag_system.load_data("7590053231020941057_391109923615173.xlsx") |
|
|
|
|
|
demo = create_gradio_interface(rag_system) |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=True, |
|
|
) |