import json import networkx as nx import numpy as np import os from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity from duckduckgo_search import DDGS try: import chromadb from chromadb.config import Settings CHROMA_AVAILABLE = True except ImportError: CHROMA_AVAILABLE = False print("⚠ ChromaDB not found. Falling back to in-memory vector search.") # Config DATA_PATH = "data/mental_health_knowledge.json" MODEL_NAME = 'all-MiniLM-L6-v2' CHROMA_HOST = os.getenv("CHROMA_HOST", "localhost") CHROMA_PORT = int(os.getenv("CHROMA_PORT", 8000)) class RAGEngine: def __init__(self): print("Initializing RAG Engine (ChromaDB + Graph + Web)...") self.encoder = SentenceTransformer(MODEL_NAME) # Initialize ChromaDB Client try: if CHROMA_AVAILABLE: if os.getenv("CHROMA_HOST"): self.chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=CHROMA_PORT) else: self.chroma_client = chromadb.PersistentClient(path="./chroma_db") # Persistent local storage self.collection = self.chroma_client.get_or_create_collection(name="mental_health_docs") self.use_chroma = True print(f"✓ Connected to ChromaDB (Persistent)") else: self.use_chroma = False except Exception as e: print(f"⚠ ChromaDB connection failed ({e}), falling back to in-memory vector search.") self.use_chroma = False self.load_knowledge_base() def load_knowledge_base(self): with open(DATA_PATH, 'r') as f: data = json.load(f) self.documents = data['documents'] self.nodes = data['nodes'] self.edges = data['edges'] # Populate Vector DB if using Chroma if self.use_chroma: try: if self.collection.count() == 0: print("Populating ChromaDB...") ids = [d['id'] for d in self.documents] docs = [d['content'] for d in self.documents] metadatas = [{'title': d['title']} for d in self.documents] embeddings = self.encoder.encode(docs).tolist() self.collection.add( embeddings=embeddings, documents=docs, metadatas=metadatas, ids=ids ) except Exception as e: print(f"Error populating Chroma: {e}") else: # Fallback self.doc_texts = [d['content'] for d in self.documents] self.doc_embeddings = self.encoder.encode(self.doc_texts) # Build Knowledge Graph self.graph = nx.DiGraph() for node in self.nodes: self.graph.add_node(node['id'], **node) for edge in self.edges: self.graph.add_edge(edge['source'], edge['target'], relation=edge['relation']) def vector_search(self, query, top_k=2): if self.use_chroma: try: query_vec = self.encoder.encode([query]).tolist() results = self.collection.query( query_embeddings=query_vec, n_results=top_k ) output = [] # Chroma returns list of lists if results['documents']: for i, doc in enumerate(results['documents'][0]): meta = results['metadatas'][0][i] output.append({ 'source': 'Local Knowledge (Vector)', 'title': meta['title'], 'content': doc, 'score': 0.9 # Chroma doesn't always return normalized score easily }) return output except Exception as e: print(f"Chroma search error: {e}") return [] else: # Fallback implementation query_vec = self.encoder.encode([query]) sims = cosine_similarity(query_vec, self.doc_embeddings)[0] top_indices = np.argsort(sims)[-top_k:][::-1] results = [] for idx in top_indices: if sims[idx] > 0.3: results.append({ 'source': 'Local Knowledge', 'title': self.documents[idx]['title'], 'content': self.documents[idx]['content'], 'score': float(sims[idx]) }) return results def graph_search(self, query): """Find related concepts in graph""" related_nodes = [] query_lower = query.lower() start_nodes = [n for n in self.graph.nodes if n in query_lower or query_lower in n] for start_node in start_nodes: neighbors = list(self.graph.neighbors(start_node)) related_nodes.extend(neighbors) related_nodes.append(start_node) results = [] unique_nodes = set(related_nodes) for node_id in unique_nodes: node_data = self.graph.nodes[node_id] desc = node_data.get('description', '') results.append({ 'source': 'Knowledge Graph', 'title': f"Concept: {node_id.title()}", 'content': f"{node_id.title()} ({node_data.get('type')}): {desc}", 'score': 1.0 }) return results def web_search(self, query, max_results=2): print(f"Searching web for: {query}") results = [] try: with DDGS() as ddgs: context_query = f"{query} mental health research" search_results = list(ddgs.text(context_query, max_results=max_results)) for r in search_results: results.append({ 'source': 'Web Search (DuckDuckGo)', 'title': r['title'], 'content': r['body'], 'url': r['href'], 'score': 0.8 }) except Exception as e: print(f"Web search failed: {e}") return results def save_memory(self, text, metadata=None): if self.use_chroma: try: import uuid doc_id = str(uuid.uuid4()) embedding = self.encoder.encode([text]).tolist() self.collection.add( embeddings=embedding, documents=[text], metadatas=[metadata or {"source": "user_memory"}], ids=[doc_id] ) print(f"saved memory {doc_id}") except Exception as e: print(f"Error saving memory: {e}") def query(self, user_query): vector_results = self.vector_search(user_query) graph_results = self.graph_search(user_query) web_results = self.web_search(user_query) all_results = vector_results + graph_results + web_results seen = set() final_results = [] for r in all_results: if r['content'] not in seen: seen.add(r['content']) final_results.append(r) return final_results