CoolFace
Apppublic

hina625/agent-Backend

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
dynamic_rag.py367 linesDownload Raw Back to root
1import os2import json3import re4import requests5from bs4 import BeautifulSoup6from pypdf import PdfReader7import logging8from dotenv import load_dotenv9 10# Load env variables11load_dotenv()12 13logger = logging.getLogger("dynamic-rag")14logger.setLevel(logging.INFO)15 16# Optional dependencies check for Hybrid Search17try:18    from sentence_transformers import SentenceTransformer19    HAS_SEMANTIC = True20except ImportError:21    HAS_SEMANTIC = False22    logger.warning("sentence-transformers not found. Will use Hugging Face API fallback for embeddings.")23 24class DynamicRAG:25    def __init__(self):26        self.has_semantic = HAS_SEMANTIC27        self.dense_model = None28 29        self.supabase_url = os.getenv("SUPABASE_URL", "")30        self.supabase_key = os.getenv("SUPABASE_ANON_KEY", "")31 32        if self.has_semantic:33            try:34                logger.info("Initializing SentenceTransformer all-MiniLM-L6-v2 local model...")35                self.dense_model = SentenceTransformer('all-MiniLM-L6-v2')36                logger.info("SentenceTransformer model loaded successfully.")37            except Exception as e:38                logger.error(f"Failed to load SentenceTransformer model: {e}. Using Hugging Face API fallback.")39                self.has_semantic = False40 41        if not self.supabase_url or not self.supabase_key:42            logger.error("Supabase environment variables (SUPABASE_URL / SUPABASE_ANON_KEY) are missing!")43 44        # Auto-initialize database with default portfolio if empty45        self.check_and_init_db()46 47    def check_and_init_db(self):48        if not self.supabase_url or not self.supabase_key:49            return50        51        headers = {52            "apikey": self.supabase_key,53            "Authorization": f"Bearer {self.supabase_key}"54        }55        56        try:57            # Check if any chunk exists58            res = requests.get(59                f"{self.supabase_url}/rest/v1/document_chunks?select=id&limit=1",60                headers=headers,61                timeout=1062            )63            if res.status_code == 200:64                data = res.json()65                if len(data) == 0:66                    logger.info("Supabase database is empty. Uploading default portfolio data...")67                    self.load_default_portfolio()68            else:69                logger.error(f"Failed to check Supabase database: {res.status_code} - {res.text}")70        except Exception as e:71            logger.error(f"Error checking Supabase db: {e}")72 73    def generate_embedding(self, text: str) -> list:74        clean_text = re.sub(r'\s+', ' ', text).strip()75        if not clean_text:76            return [0.0] * 38477 78        # 1. Try local SentenceTransformer model (if installed)79        if self.has_semantic and self.dense_model:80            try:81                embedding = self.dense_model.encode([clean_text], show_progress_bar=False)82                return embedding[0].tolist()83            except Exception as e:84                logger.error(f"Local embedding generation failed: {e}. Falling back to OpenAI API.")85 86        # 2. Fallback: OpenAI Embeddings API (text-embedding-3-large with 384 dimensions)87        try:88            api_key = os.getenv("OPENAI_API_KEY", "")89            if not api_key:90                logger.error("OpenAI API Key (OPENAI_API_KEY) is missing in environment variables!")91                return [0.0] * 38492 93            # If it's a Groq key (gsk_...), Groq does not support embeddings, so warn and fallback94            if api_key.startswith("gsk_"):95                logger.error("OPENAI_API_KEY is configured with a Groq key. Groq does not support embeddings. Please use a real OpenAI API Key.")96                return [0.0] * 38497 98            headers = {99                "Content-Type": "application/json",100                "Authorization": f"Bearer {api_key}"101            }102            payload = {103                "model": "text-embedding-3-large",104                "input": clean_text,105                "dimensions": 384106            }107 108            response = requests.post(109                "https://api.openai.com/v1/embeddings",110                headers=headers,111                json=payload,112                timeout=10113            )114            if response.status_code == 200:115                result = response.json()116                if "data" in result and len(result["data"]) > 0:117                    return result["data"][0]["embedding"]118            logger.error(f"OpenAI embedding API failed: {response.status_code} - {response.text}")119        except Exception as e:120            logger.error(f"OpenAI embedding API error: {e}")121 122        # Final fallback: return zero vector123        return [0.0] * 384124 125    def upload_chunks(self, chunks: list) -> bool:126        if not chunks:127            return True128        if not self.supabase_url or not self.supabase_key:129            return False130 131        headers = {132            "Content-Type": "application/json",133            "apikey": self.supabase_key,134            "Authorization": f"Bearer {self.supabase_key}"135        }136 137        try:138            res = requests.post(139                f"{self.supabase_url}/rest/v1/document_chunks",140                headers=headers,141                json=chunks,142                timeout=15143            )144            if res.status_code in [200, 201]:145                logger.info(f"Successfully uploaded {len(chunks)} chunks to Supabase.")146                return True147            else:148                logger.error(f"Supabase upload failed: {res.status_code} - {res.text}")149                return False150        except Exception as e:151            logger.error(f"Failed to upload to Supabase: {e}")152            return False153 154    def load_default_portfolio(self):155        base_dir = os.path.dirname(os.path.abspath(__file__))156        json_path = os.path.join(base_dir, "portfolio_data.json")157        if not os.path.exists(json_path):158            return159 160        try:161            with open(json_path, "r", encoding="utf-8") as f:162                data = json.load(f)163            164            raw_texts = []165 166            # Add profile167            profile = data.get("agent_profile", {})168            raw_texts.append((169                f"{profile.get('name')} profile: {profile.get('role')}. {profile.get('purpose')} Built by {profile.get('creator')}.",170                "Default Portfolio Profile"171            ))172            173            # Add pillars174            for pillar in data.get("cognitive_pillars", []):175                raw_texts.append((176                    f"Cognitive Pillar '{pillar.get('title')}': {pillar.get('description')}",177                    "Portfolio Cognitive Core"178                ))179            180            # Add projects181            for project in data.get("projects", []):182                raw_texts.append((183                    f"Project '{project.get('name')}' (Tech: {project.get('tech_stack')}, Success Rate: {project.get('success_rate')}, Complexity: {project.get('complexity')}): {project.get('description')}",184                    "Portfolio Projects"185                ))186            187            # Add capabilities188            for cap in data.get("capabilities", []):189                modules = ", ".join(cap.get("modules", []))190                raw_texts.append((191                    f"Technical Capability '{cap.get('name')}' (Category: {cap.get('category')}, Reliability: {cap.get('reliability')}): {cap.get('description')}. Modules: {modules}",192                    "Portfolio Capabilities"193                ))194            195            # Add execution lifecycle196            for step in data.get("execution_lifecycle", []):197                raw_texts.append((198                    f"Execution Lifecycle Step {step.get('step')} '{step.get('title')}': {step.get('description')}",199                    "Portfolio Lifecycle"200                ))201 202            # Chunk, embed, and prepare upload list203            chunks = []204            for text, source in raw_texts:205                cleaned_text = re.sub(r'\s+', ' ', text).strip()206                if not cleaned_text:207                    continue208                209                chunk_size = 1000210                overlap = 200211                start = 0212                while start < len(cleaned_text):213                    end = start + chunk_size214                    chunk = cleaned_text[start:end]215                    216                    embedding = self.generate_embedding(chunk)217                    chunks.append({218                        "content": chunk,219                        "source": source,220                        "embedding": embedding221                    })222                    start += chunk_size - overlap223 224            self.upload_chunks(chunks)225        except Exception as e:226            logger.error(f"Error loading default portfolio data: {e}")227 228    def add_pdf(self, file_path: str) -> bool:229        if not os.path.isabs(file_path):230            base_dir = os.path.dirname(os.path.abspath(__file__))231            file_path = os.path.join(base_dir, file_path)232 233        if not os.path.exists(file_path):234            logger.error(f"PDF path not found: {file_path}")235            return False236        try:237            reader = PdfReader(file_path)238            full_text = []239            for page in reader.pages:240                page_text = page.extract_text()241                if page_text:242                    full_text.append(page_text)243            244            combined_text = "\n".join(full_text)245            if not combined_text.strip():246                logger.error(f"PDF is empty or has non-extractable text: {file_path}")247                return False248 249            source_name = f"PDF File: {os.path.basename(file_path)}"250            251            # Chunk and generate embeddings252            cleaned_text = re.sub(r'\s+', ' ', combined_text).strip()253            chunks = []254            chunk_size = 1000255            overlap = 200256            start = 0257            while start < len(cleaned_text):258                end = start + chunk_size259                chunk = cleaned_text[start:end]260                embedding = self.generate_embedding(chunk)261                chunks.append({262                    "content": chunk,263                    "source": source_name,264                    "embedding": embedding265                })266                start += chunk_size - overlap267 268            return self.upload_chunks(chunks)269        except Exception as e:270            logger.error(f"Error reading PDF {file_path}: {e}")271            return False272 273    def add_url(self, url: str) -> bool:274        try:275            headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}276            response = requests.get(url, headers=headers, timeout=10)277            if response.status_code != 200:278                logger.error(f"Failed to fetch URL {url}, status code: {response.status_code}")279                return False280            281            soup = BeautifulSoup(response.content, "html.parser")282            283            # Remove script, style, nav, header, footer elements284            for element in soup(["script", "style", "nav", "header", "footer"]):285                element.decompose()286            287            text = soup.get_text()288            cleaned_text = "\n".join([line.strip() for line in text.splitlines() if line.strip()])289            290            if not cleaned_text.strip():291                logger.error(f"Scraped text from URL is empty: {url}")292                return False293 294            source_name = f"Web URL: {url}"295            296            # Chunk and generate embeddings297            cleaned_text = re.sub(r'\s+', ' ', cleaned_text).strip()298            chunks = []299            chunk_size = 1000300            overlap = 200301            start = 0302            while start < len(cleaned_text):303                end = start + chunk_size304                chunk = cleaned_text[start:end]305                embedding = self.generate_embedding(chunk)306                chunks.append({307                    "content": chunk,308                    "source": source_name,309                    "embedding": embedding310                })311                start += chunk_size - overlap312 313            return self.upload_chunks(chunks)314        except Exception as e:315            logger.error(f"Error fetching URL {url}: {e}")316            return False317 318    def search(self, query: str, top_n: int = 4) -> str:319        if not self.supabase_url or not self.supabase_key:320            return "Supabase credentials are not configured."321 322        try:323            query_clean = re.sub(r'[^\w\s]', '', query.lower()).strip()324            if not query_clean:325                return ""326 327            # 1. Generate query embedding vector328            query_emb = self.generate_embedding(query)329 330            # 2. Query similarity search (match_documents) function in Supabase331            headers = {332                "Content-Type": "application/json",333                "apikey": self.supabase_key,334                "Authorization": f"Bearer {self.supabase_key}"335            }336            337            payload = {338                "query_embedding": query_emb,339                "match_threshold": 0.25,340                "match_count": top_n341            }342 343            res = requests.post(344                f"{self.supabase_url}/rest/v1/rpc/match_documents",345                headers=headers,346                json=payload,347                timeout=10348            )349 350            if res.status_code == 200:351                matches = res.json()352                results = []353                for m in matches:354                    if m.get("similarity", 0) > 0.15: # Extra threshold safety filter355                        results.append(f"[{m.get('source')}]: {m.get('content')}")356                357                if not results:358                    return "No matching context found. Proceed with standard portfolio details."359                360                return "\n\n".join(results)361            else:362                logger.error(f"Supabase RPC search failed: {res.status_code} - {res.text}")363                return "Error querying search index."364        except Exception as e:365            logger.error(f"Error performing search: {e}")366            return "Error searching documents."367