import os os.environ["TOKENIZERS_PARALLELISM"] = "false" import datetime import functools import traceback import gc from typing import List, Optional, Any, Dict, Tuple from pydantic import Field import csv import pandas as pd import tempfile import shutil import glob import torch import transformers from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline from langchain_community.llms import HuggingFacePipeline # Other LangChain and community imports from langchain_community.document_loaders import OnlinePDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from langchain.embeddings import HuggingFaceEmbeddings from langchain_community.retrievers import BM25Retriever from langchain.embeddings.base import Embeddings from langchain.retrievers import EnsembleRetriever from langchain.prompts import ChatPromptTemplate from langchain.schema import StrOutputParser, Document from langchain_core.runnables import RunnableParallel, RunnableLambda from transformers.quantizers.auto import AutoQuantizationConfig import gradio as gr from pydantic import PrivateAttr import pydantic from langchain.llms.base import LLM from typing import Any, Optional, List import typing import time import re import requests from langchain.schema import Document from langchain_community.document_loaders import PyMuPDFLoader # Updated loader import tempfile import mimetypes # Add OpenAI import for NEBIUS with version check try: import openai from importlib.metadata import version as pkg_version openai_version = pkg_version("openai") print(f"OpenAI import success, version: {openai_version}") if tuple(map(int, openai_version.split("."))) < (1, 0, 0): print("ERROR: openai version must be >= 1.0.0 for NEBIUS support. Please upgrade with: pip install --upgrade openai") sys.exit(1) from openai import OpenAI OPENAI_AVAILABLE = True except ImportError as e: OPENAI_AVAILABLE = False print("OpenAI import failed:", e) except Exception as e: print("OpenAI version check failed:", e) OPENAI_AVAILABLE = False # API Key Configuration NEBIUS_API_KEY = os.environ.get("NEBIUS_API_KEY", "") # Define NebiusLLM class at module level to avoid pickle issues class NebiusLLM(LLM): """Nebius LLM wrapper with proper response validation.""" model: str = Field(..., description="The model name to use") api_key: str = Field(..., description="API key for Nebius") base_url: str = "https://api.studio.nebius.com/v1" max_tokens: int = 2048 temperature: float = 0.7 top_p: float = 0.95 top_k: int = 50 def __init__(self, **data): super().__init__(**data) if not self.model: raise ValueError("Model name cannot be empty") @property def _llm_type(self) -> str: return "nebius" def _call( self, prompt: str, stop: Optional[List[str]] = None, run_manager: Optional[Any] = None, **kwargs: Any, ) -> str: """Call the Nebius API and return the response.""" try: from openai import OpenAI debug_print(f"Nebius API call: model={self.model}, max_tokens={self.max_tokens}") # Create OpenAI client with Nebius base URL client = OpenAI(base_url=self.base_url, api_key=self.api_key) completion = client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=self.temperature, top_p=self.top_p, max_tokens=self.max_tokens ) # Extract the text content with proper validation if completion.choices and len(completion.choices) > 0: choice = completion.choices[0] # Handle different response formats if hasattr(choice.message, 'content') and choice.message.content: text = choice.message.content elif hasattr(choice, 'text'): text = choice.text else: debug_print(f"WARNING: Unexpected response format: {choice}") text = str(choice.message) if hasattr(choice, 'message') else str(choice) # Validate that we got actual text if text is None: debug_print(f"WARNING: Received None text from API response") return "I apologize, but I received an empty response. Please try rephrasing your question." if not isinstance(text, str): text = str(text) if not text.strip(): debug_print(f"WARNING: Received empty text from API") return "I apologize, but I received an empty response. Please try rephrasing your question." return text.strip() else: debug_print(f"WARNING: No choices in API response") return "I apologize, but I didn't receive a valid response. Please try again." except ImportError: error_msg = "OpenAI package is required for Nebius models. Please install it with: pip install openai" debug_print(error_msg) return error_msg except Exception as e: error_msg = f"Error calling Nebius API: {str(e)}" debug_print(error_msg) return f"API error: {str(e)}" @property def _identifying_params(self) -> Dict[str, Any]: """Return identifying parameters.""" return { "model": self.model, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k, "max_tokens": self.max_tokens, } # Add batch processing helper functions def generate_parameter_values(min_val, max_val, num_values): """Generate evenly spaced values between min and max""" if num_values == 1: return [min_val] step = (max_val - min_val) / (num_values - 1) return [min_val + (step * i) for i in range(num_values)] def process_batch_query(query, model_choice, max_tokens, param_configs, slider_values, job_id, use_history=True): """Process a batch of queries with different parameter combinations""" results = [] # Update model if it has changed if hasattr(rag_chain, 'llm_choice') and rag_chain.llm_choice != model_choice: rag_chain.update_llm_pipeline(model_choice, rag_chain.temperature, rag_chain.top_p, rag_chain.top_k, rag_chain.prompt_template, rag_chain.bm25_weight, rag_chain.max_tokens) debug_print(f"Model updated to {model_choice}") # Generate all parameter combinations temp_values = [slider_values['temperature']] if param_configs['temperature'] == "Constant" else generate_parameter_values(0.1, 1.0, int(param_configs['temperature'].split()[2])) top_p_values = [slider_values['top_p']] if param_configs['top_p'] == "Constant" else generate_parameter_values(0.1, 0.99, int(param_configs['top_p'].split()[2])) top_k_values = [slider_values['top_k']] if param_configs['top_k'] == "Constant" else generate_parameter_values(1, 100, int(param_configs['top_k'].split()[2])) bm25_values = [slider_values['bm25']] if param_configs['bm25'] == "Constant" else generate_parameter_values(0.0, 1.0, int(param_configs['bm25'].split()[2])) total_combinations = len(temp_values) * len(top_p_values) * len(top_k_values) * len(bm25_values) current = 0 for temp in temp_values: for top_p in top_p_values: for top_k in top_k_values: for bm25 in bm25_values: current += 1 try: # Update parameters rag_chain.temperature = temp rag_chain.top_p = top_p rag_chain.top_k = top_k rag_chain.bm25_weight = bm25 rag_chain.faiss_weight = 1.0 - bm25 # Update ensemble retriever rag_chain.ensemble_retriever = EnsembleRetriever( retrievers=[rag_chain.bm25_retriever, rag_chain.faiss_retriever], weights=[rag_chain.bm25_weight, rag_chain.faiss_weight] ) # Process query response = rag_chain.elevated_rag_chain.invoke({"question": query}) # Store response in history if enabled if use_history: trimmed_response = response[:1000] + ("..." if len(response) > 1000 else "") rag_chain.conversation_history.append({"query": query, "response": trimmed_response}) # Format result result = { "Parameters": f"Temp: {temp:.2f}, Top-p: {top_p:.2f}, Top-k: {top_k}, BM25: {bm25:.2f}", "Response": response, "Progress": f"Query {current}/{total_combinations}" } results.append(result) except Exception as e: results.append({ "Parameters": f"Temp: {temp:.2f}, Top-p: {top_p:.2f}, Top-k: {top_k}, BM25: {bm25:.2f}", "Response": f"Error: {str(e)}", "Progress": f"Query {current}/{total_combinations}" }) # Format results with CSV file links - UPDATED TO PASS ADDITIONAL PARAMETERS formatted_results, csv_path = format_batch_result_files( results, job_id, embedding_model=getattr(rag_chain, 'embedding_model', 'unknown'), llm_model=model_choice, param_variations=param_configs ) return ( formatted_results, csv_path, f"Job ID: {job_id}", f"Input tokens: {count_tokens(query)}", f"Output tokens: {sum(count_tokens(r['Response']) for r in results)}" ) def process_batch_query_async(query, model_choice, max_tokens, param_configs, slider_values, use_history): """Asynchronous version of batch query processing""" global last_job_id if not query: return "Please enter a non-empty query", None, "", "Input tokens: 0", "Output tokens: 0", "", "", get_job_list() if not hasattr(rag_chain, 'elevated_rag_chain') or not rag_chain.raw_data: return "Please load files first.", None, "", "Input tokens: 0", "Output tokens: 0", "", "", get_job_list() job_id = str(uuid.uuid4()) debug_print(f"Starting async batch job {job_id} for query: {query}") # Get slider values slider_values = { 'temperature': slider_values['temperature'], 'top_p': slider_values['top_p'], 'top_k': slider_values['top_k'], 'bm25': slider_values['bm25'] } # Start background thread threading.Thread( target=process_in_background, args=(job_id, process_batch_query, [query, model_choice, max_tokens, param_configs, slider_values, job_id, use_history]) ).start() jobs[job_id] = { "status": "processing", "type": "batch_query", "start_time": time.time(), "query": query, "model": model_choice, "param_configs": param_configs } last_job_id = job_id return ( f"Batch query submitted and processing in the background (Job ID: {job_id}).\n\n" f"Use 'Check Job Status' tab with this ID to get results.", None, # No CSV file initially "", # Empty context initially f"Input tokens: {count_tokens(query)}", "Output tokens: pending", job_id, # Return job_id to update the job_id_input component query, # Return query to update the job_query_display component get_job_list() # Return updated job list ) def submit_batch_query_async(query, model_choice, max_tokens, temp_config, top_p_config, top_k_config, bm25_config, temp_slider, top_p_slider, top_k_slider, bm25_slider, use_history): """Handle batch query submission with async processing""" if not query: return "Please enter a non-empty query", None, "", "Input tokens: 0", "Output tokens: 0", "", "", get_job_list() if not hasattr(rag_chain, 'elevated_rag_chain') or not rag_chain.raw_data: return "Please load files first.", None, "", "Input tokens: 0", "Output tokens: 0", "", "", get_job_list() # Get slider values slider_values = { 'temperature': temp_slider, 'top_p': top_p_slider, 'top_k': top_k_slider, 'bm25': bm25_slider } param_configs = { 'temperature': temp_config, 'top_p': top_p_config, 'top_k': top_k_config, 'bm25': bm25_config } return process_batch_query_async(query, model_choice, max_tokens, param_configs, slider_values, use_history) def submit_batch_query(query, model_choice, max_tokens, temp_config, top_p_config, top_k_config, bm25_config, temp_slider, top_p_slider, top_k_slider, bm25_slider): """Handle batch query submission""" if not query: return "Please enter a non-empty query", "", "Input tokens: 0", "Output tokens: 0" if not hasattr(rag_chain, 'elevated_rag_chain') or not rag_chain.raw_data: return "Please load files first.", "", "Input tokens: 0", "Output tokens: 0" # Get slider values slider_values = { 'temperature': temp_slider, 'top_p': top_p_slider, 'top_k': top_k_slider, 'bm25': bm25_slider } try: results = process_batch_query(query, model_choice, max_tokens, {'temperature': temp_config, 'top_p': top_p_config, 'top_k': top_k_config, 'bm25': bm25_config}, slider_values) # Format results for display formatted_results = "### Batch Query Results\n\n" for result in results: formatted_results += f"#### {result['Parameters']}\n" formatted_results += f"**Progress:** {result['Progress']}\n\n" formatted_results += f"{result['Response']}\n\n" formatted_results += "---\n\n" return formatted_results, "", f"Input tokens: {count_tokens(query)}", f"Output tokens: {sum(count_tokens(r['Response']) for r in results)}" except Exception as e: return f"Error processing batch query: {str(e)}", "", "Input tokens: 0", "Output tokens: 0" def get_mime_type(file_path): return mimetypes.guess_type(file_path)[0] or 'application/octet-stream' print("Pydantic Version: ") print(pydantic.__version__) # Add Mistral imports with fallback handling slider_max_tokens = None try: from mistralai import Mistral MISTRAL_AVAILABLE = True debug_print = lambda msg: print(f"[{datetime.datetime.now().isoformat()}] {msg}") debug_print("Loaded latest Mistral client library") except ImportError: MISTRAL_AVAILABLE = False debug_print = lambda msg: print(f"[{datetime.datetime.now().isoformat()}] {msg}") debug_print("Mistral client library not found. Install with: pip install mistralai") def debug_print(message: str): print(f"[{datetime.datetime.now().isoformat()}] {message}", flush=True) def word_count(text: str) -> int: return len(text.split()) # Initialize a tokenizer for token counting (using gpt2 as a generic fallback) def initialize_tokenizer(): try: return AutoTokenizer.from_pretrained("gpt2") except Exception as e: debug_print("Failed to initialize tokenizer: " + str(e)) return None global_tokenizer = initialize_tokenizer() def count_tokens(text: str) -> int: if global_tokenizer: try: return len(global_tokenizer.encode(text)) except Exception as e: return len(text.split()) return len(text.split()) # Add NebiusEmbedding class for Nebius platform embedding models class NebiusEmbedding(Embeddings): """Custom embedding class for Nebius platform models""" def __init__(self, model_name: str, api_key: str = None): super().__init__() self.model_name = model_name self.api_key = api_key or os.environ.get("NEBIUS_API_KEY") if not self.api_key: raise ValueError("Please set the NEBIUS_API_KEY environment variable to use Nebius embedding models.") try: from openai import OpenAI self.client = OpenAI( base_url="https://api.studio.nebius.com/v1/", api_key=self.api_key ) except ImportError: raise ImportError("openai package is required for Nebius embedding models.") def embed_documents(self, texts: List[str]) -> List[List[float]]: """Embed a list of documents""" try: response = self.client.embeddings.create( model=self.model_name, input=texts ) return [data.embedding for data in response.data] except Exception as e: debug_print(f"Error embedding documents with Nebius: {str(e)}") raise e def embed_query(self, text: str) -> List[float]: """Embed a single query""" try: response = self.client.embeddings.create( model=self.model_name, input=[text] ) return response.data[0].embedding except Exception as e: debug_print(f"Error embedding query with Nebius: {str(e)}") raise e # Add these imports at the top of your file import uuid import threading import queue from typing import Dict, Any, Tuple, Optional import time # Global storage for jobs and results jobs = {} # Stores job status and results results_queue = queue.Queue() # Thread-safe queue for completed jobs processing_lock = threading.Lock() # Prevent simultaneous processing of the same job # Add a global variable to store the last job ID last_job_id = None # Add these missing async processing functions def process_in_background(job_id, function, args): """Process a function in the background and store its result""" try: debug_print(f"Processing job {job_id} in background") result = function(*args) results_queue.put((job_id, result)) debug_print(f"Job {job_id} completed and added to results queue") except Exception as e: error_msg = f"Error processing job {job_id}: {str(e)}" debug_print(error_msg) results_queue.put((job_id, (error_msg, None, "", "Input tokens: 0", "Output tokens: 0"))) def load_pdfs_async(file_links, prompt_template, bm25_weight, embedding_model): """Asynchronous version of load_pdfs_updated to prevent timeouts""" global last_job_id if not file_links: return "Please enter non-empty URLs", "", "Model used: N/A", "", "", get_job_list(), "" job_id = str(uuid.uuid4()) debug_print(f"Starting async job {job_id} for file loading") # Start background thread threading.Thread( target=process_in_background, args=(job_id, load_pdfs_updated, [file_links, prompt_template, bm25_weight, embedding_model]) ).start() job_query = f"Loading files: {file_links.split()[0]}..." if file_links else "No files" jobs[job_id] = { "status": "processing", "type": "load_files", "start_time": time.time(), "query": job_query } last_job_id = job_id init_message = "Vector database initialized using the files.\nThe above parameters were used in the initialization of the RAG chain." return ( f"Files submitted and processing in the background (Job ID: {job_id}).\n\n" f"Use 'Check Job Status' tab with this ID to get results.", f"Job ID: {job_id}", f"Embedding model: {embedding_model}", job_id, # Return job_id to update the job_id_input component job_query, # Return job_query to update the job_query_display component get_job_list(), # Return updated job list init_message # Return initialization message ) def submit_query_async(query, model_choice, max_tokens_slider, temperature, top_p, top_k, bm25_weight, use_history): """Submit a query asynchronously""" try: if not query: return "Please enter a non-empty query", "", "Input tokens: 0", "Output tokens: 0" # Update model if it has changed if hasattr(rag_chain, 'llm_choice') and rag_chain.llm_choice != model_choice: rag_chain.update_llm_pipeline(model_choice, temperature, top_p, top_k, rag_chain.prompt_template, bm25_weight, max_tokens_slider) debug_print(f"Model updated to {model_choice}") # Update BM25 weight and recreate ensemble retriever if needed if hasattr(rag_chain, 'bm25_weight') and rag_chain.bm25_weight != bm25_weight: rag_chain.bm25_weight = bm25_weight rag_chain.faiss_weight = 1.0 - bm25_weight rag_chain.ensemble_retriever = EnsembleRetriever( retrievers=[rag_chain.bm25_retriever, rag_chain.faiss_retriever], weights=[rag_chain.bm25_weight, rag_chain.faiss_weight] ) debug_print(f"Updated ensemble retriever with BM25 weight: {bm25_weight}") # Clear conversation history if checkbox is unchecked if not use_history: rag_chain.conversation_history = [] debug_print("Conversation history cleared") result = rag_chain.chain({"question": query}) response = result["answer"] context = rag_chain.get_current_context() # Format the response formatted_response = format_response(response) # Get token counts input_tokens = count_tokens(query + context) output_tokens = count_tokens(response) return ( formatted_response, context, f"Input tokens: {input_tokens}", f"Output tokens: {output_tokens}" ) except Exception as e: error_msg = f"Error processing query: {str(e)}" debug_print(error_msg) return error_msg, "", "Input tokens: 0", "Output tokens: 0" def update_ui_with_last_job_id(): # This function doesn't need to do anything anymore # We'll update the UI directly in the functions that call this pass # Function to display all jobs as a clickable list def get_job_list(): job_list_md = "### Submitted Jobs\n\n" if not jobs: return "No jobs found. Submit a query or load files to create jobs." # Sort jobs by start time (newest first) sorted_jobs = sorted( [(job_id, job_info) for job_id, job_info in jobs.items()], key=lambda x: x[1].get("start_time", 0), reverse=True ) for job_id, job_info in sorted_jobs: status = job_info.get("status", "unknown") job_type = job_info.get("type", "unknown") query = job_info.get("query", "") start_time = job_info.get("start_time", 0) time_str = datetime.datetime.fromtimestamp(start_time).strftime("%Y-%m-%d %H:%M:%S") # Create a shortened query preview query_preview = query[:30] + "..." if query and len(query) > 30 else query or "N/A" # Add color and icons based on status if status == "processing": # Red color with processing icon for processing jobs status_formatted = f"β³ {status}" elif status == "completed": # Green color with checkmark for completed jobs status_formatted = f"β {status}" else: # Default formatting for unknown status status_formatted = f"β {status}" # Create clickable links using Markdown if job_type == "query": job_list_md += f"- [{job_id}](javascript:void) - {time_str} - {status_formatted} - Query: {query_preview}\n" else: job_list_md += f"- [{job_id}](javascript:void) - {time_str} - {status_formatted} - File Load Job\n" return job_list_md # Function to handle job list clicks def job_selected(job_id): if job_id in jobs: return job_id, jobs[job_id].get("query", "No query for this job") return job_id, "Job not found" # Function to refresh the job list def refresh_job_list(): return get_job_list() # Function to sync model dropdown boxes def sync_model_dropdown(value): return value # Function to check job status def check_job_status(job_id): """Check the status of a job and return its results""" if not job_id: return "Please enter a job ID", None, "", "", "", "" # Process any completed jobs in the queue try: while not results_queue.empty(): completed_id, result = results_queue.get_nowait() if completed_id in jobs: jobs[completed_id]["status"] = "completed" jobs[completed_id]["result"] = result jobs[completed_id]["end_time"] = time.time() debug_print(f"Job {completed_id} completed and stored in jobs dictionary") except queue.Empty: pass if job_id not in jobs: return "Job not found", None, "", "", "", "" job = jobs[job_id] job_query = job.get("query", "No query for this job") # If job is still processing if job["status"] == "processing": elapsed = time.time() - job["start_time"] return ( f"Job is still processing... (elapsed time: {elapsed:.1f}s)", None, "", "", "", job_query ) # If job is completed if job["status"] == "completed": result = job["result"] processing_time = job["end_time"] - job["start_time"] if job.get("type") == "load_files": return ( f"{result[0]}\n\nProcessing time: {processing_time:.1f}s", None, result[1], "", "", job_query ) else: # query job return ( f"{result[0]}\n\nProcessing time: {processing_time:.1f}s", result[1], # CSV file path result[2], result[3], result[4], job_query ) # Fallback for unknown status return f"Job status: {job['status']}", None, "", "", "", job_query # Function to clean up old jobs def cleanup_old_jobs(): current_time = time.time() to_delete = [] for job_id, job in jobs.items(): # Keep completed jobs for 24 hours, processing jobs for 48 hours if job["status"] == "completed" and (current_time - job.get("end_time", 0)) > 86400: to_delete.append(job_id) elif job["status"] == "processing" and (current_time - job.get("start_time", 0)) > 172800: to_delete.append(job_id) for job_id in to_delete: del jobs[job_id] debug_print(f"Cleaned up {len(to_delete)} old jobs. {len(jobs)} jobs remaining.") return f"Cleaned up {len(to_delete)} old jobs", "", "" # Improve the truncate_prompt function to be more aggressive with limiting context def truncate_prompt(prompt: str, max_tokens: int = 4096) -> str: """Truncate prompt to fit within token limit, preserving the most recent/relevant parts.""" if not prompt: return "" if global_tokenizer: try: tokens = global_tokenizer.encode(prompt) if len(tokens) > max_tokens: # For prompts, we often want to keep the beginning instructions and the end context # So we'll keep the first 20% and the last 80% of the max tokens beginning_tokens = int(max_tokens * 0.2) ending_tokens = max_tokens - beginning_tokens new_tokens = tokens[:beginning_tokens] + tokens[-(ending_tokens):] return global_tokenizer.decode(new_tokens) except Exception as e: debug_print(f"Truncation error: {str(e)}") # Fallback to word-based truncation words = prompt.split() if len(words) > max_tokens: beginning_words = int(max_tokens * 0.2) ending_words = max_tokens - beginning_words return " ".join(words[:beginning_words] + words[-(ending_words):]) return prompt default_prompt = """\ {conversation_history} Use the following context to provide a detailed technical answer to the user's question. Do not include an introduction like "Based on the provided documents, ...". Just answer the question. Context: {context} User's question: {question} """ # #If you don't know the answer, please respond with "I don't know". def load_txt_from_url(url: str) -> Document: response = requests.get(url) if response.status_code == 200: text = response.text.strip() if not text: raise ValueError(f"TXT file at {url} is empty.") return Document(page_content=text, metadata={"source": url}) else: raise Exception(f"Failed to load {url} with status {response.status_code}") from pdfminer.high_level import extract_text from langchain_core.documents import Document def get_confirm_token(response): for key, value in response.cookies.items(): if key.startswith("download_warning"): return value return None def download_file_from_google_drive(file_id, destination): """ Download a file from Google Drive handling large file confirmation. """ URL = "https://docs.google.com/uc?export=download&confirm=1" session = requests.Session() response = session.get(URL, params={"id": file_id}, stream=True) token = get_confirm_token(response) if token: params = {"id": file_id, "confirm": token} response = session.get(URL, params=params, stream=True) save_response_content(response, destination) def save_response_content(response, destination): CHUNK_SIZE = 32768 with open(destination, "wb") as f: for chunk in response.iter_content(CHUNK_SIZE): if chunk: f.write(chunk) def extract_file_id(drive_link: str) -> str: # Check for /d/ format match = re.search(r"/d/([a-zA-Z0-9_-]+)", drive_link) if match: return match.group(1) # Check for open?id= format match = re.search(r"open\?id=([a-zA-Z0-9_-]+)", drive_link) if match: return match.group(1) raise ValueError("Could not extract file ID from the provided Google Drive link.") def load_txt_from_google_drive(link: str) -> Document: """ Load text from a Google Drive shared link """ file_id = extract_file_id(link) # Create direct download link download_url = f"https://drive.google.com/uc?export=download&id={file_id}" # Request the file content response = requests.get(download_url) if response.status_code != 200: raise ValueError(f"Failed to download file from Google Drive. Status code: {response.status_code}") # Create a Document object content = response.text if not content.strip(): raise ValueError(f"TXT file from Google Drive is empty.") metadata = {"source": link} return Document(page_content=content, metadata=metadata) def load_pdf_from_google_drive(link: str) -> list: """ Load a PDF document from a Google Drive link using pdfminer to extract text. Returns a list of LangChain Document objects. """ file_id = extract_file_id(link) debug_print(f"Extracted file ID: {file_id}") with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_path = temp_file.name try: download_file_from_google_drive(file_id, temp_path) debug_print(f"File downloaded to: {temp_path}") try: full_text = extract_text(temp_path) if not full_text.strip(): raise ValueError("Extracted text is empty. The PDF might be image-based.") debug_print("Extracted preview text from PDF:") debug_print(full_text[:1000]) # Preview first 1000 characters document = Document(page_content=full_text, metadata={"source": link}) return [document] except Exception as e: debug_print(f"Could not extract text from PDF: {e}") return [] finally: if os.path.exists(temp_path): os.remove(temp_path) def load_file_from_google_drive(link: str) -> list: """ Load a document from a Google Drive link, detecting whether it's a PDF or TXT file. Returns a list of LangChain Document objects. """ file_id = extract_file_id(link) # Create direct download link download_url = f"https://drive.google.com/uc?export=download&id={file_id}" # First, try to read a small portion of the file to determine its type try: # Use a streaming request to read just the first part of the file response = requests.get(download_url, stream=True) if response.status_code != 200: raise ValueError(f"Failed to download file from Google Drive. Status code: {response.status_code}") # Read just the first 1024 bytes to check file signature file_start = next(response.iter_content(1024)) response.close() # Close the stream # Convert bytes to string for pattern matching file_start_str = file_start.decode('utf-8', errors='ignore') # Check for PDF signature (%PDF-) at the beginning of the file if file_start_str.startswith('%PDF-') or b'%PDF-' in file_start: debug_print(f"Detected PDF file by content signature from Google Drive: {link}") return load_pdf_from_google_drive(link) else: # If not a PDF, try as text debug_print(f"No PDF signature found, treating as TXT file from Google Drive: {link}") # Since we already downloaded part of the file, get the full content response = requests.get(download_url) if response.status_code != 200: raise ValueError(f"Failed to download complete file from Google Drive. Status code: {response.status_code}") content = response.text if not content.strip(): raise ValueError(f"TXT file from Google Drive is empty.") doc = Document(page_content=content, metadata={"source": link}) return [doc] except UnicodeDecodeError: # If we get a decode error, it's likely a binary file like PDF debug_print(f"Got decode error, likely a binary file. Treating as PDF from Google Drive: {link}") return load_pdf_from_google_drive(link) except Exception as e: debug_print(f"Error detecting file type: {e}") # Fall back to trying both formats debug_print("Falling back to trying both formats for Google Drive file") try: return load_pdf_from_google_drive(link) except Exception as pdf_error: debug_print(f"Failed to load as PDF: {pdf_error}") try: doc = load_txt_from_google_drive(link) return [doc] except Exception as txt_error: debug_print(f"Failed to load as TXT: {txt_error}") raise ValueError(f"Could not load file from Google Drive as either PDF or TXT: {link}") class ElevatedRagChain: def __init__(self, llm_choice: str = "Meta-Llama-3", prompt_template: str = default_prompt, bm25_weight: float = 0.6, temperature: float = 0.5, top_p: float = 0.95, top_k: int = 50, embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2", max_tokens: int = 3000) -> None: debug_print(f"Initializing ElevatedRagChain with model: {llm_choice}") self.embedding_model = embedding_model self.embed_func = self._create_embedding_function(embedding_model) self.bm25_weight = bm25_weight self.faiss_weight = 1.0 - bm25_weight self.top_k = top_k self.llm_choice = llm_choice self.temperature = temperature self.top_p = top_p self.max_tokens = max_tokens self.prompt_template = prompt_template self.context = "" self.conversation_history: List[Dict[str, str]] = [] self.raw_data = None self.split_data = None self.elevated_rag_chain = None def _create_embedding_function(self, embedding_model: str): """Create the appropriate embedding function based on the model choice""" debug_print(f"Creating embedding function for: {embedding_model}") # Map display names to actual model names model_mapping = { # sentence-transformers Models (Free) "π€ sentence-transformers/all-MiniLM-L6-v2 (384 dim, fast)": "sentence-transformers/all-MiniLM-L6-v2", "π€ sentence-transformers/all-mpnet-base-v2 (768 dim, high-quality)": "sentence-transformers/all-mpnet-base-v2", "π€ sentence-transformers/all-distilroberta-v1 (768 dim, balanced)": "sentence-transformers/all-distilroberta-v1", "π€ sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (384 dim, multilingual)": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", "π€ sentence-transformers/paraphrase-multilingual-mpnet-base-v2 (768 dim, multilingual)": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", # HuggingFace Models (Free) "π€ BAAI/bge-small-en-v1.5 (384 dim, efficient)": "BAAI/bge-small-en-v1.5", "π€ BAAI/bge-base-en-v1.5 (768 dim, excellent)": "BAAI/bge-base-en-v1.5", "π€ BAAI/bge-large-en-v1.5 (1024 dim, powerful)": "BAAI/bge-large-en-v1.5", "π€ intfloat/e5-base-v2 (768 dim, general-purpose)": "intfloat/e5-base-v2", "π€ intfloat/e5-large-v2 (1024 dim, advanced)": "intfloat/e5-large-v2", # Nebius Models (Cost) "π¦ Qwen/Qwen3-Embedding-8B (1024 dim, advanced)": "Qwen/Qwen3-Embedding-8B", "π¦ BAAI/bge-en-icl (1024 dim, instruction-tuned)": "BAAI/bge-en-icl", "π¦ BAAI/bge-multilingual-gemma2 (1024 dim, multilingual)": "BAAI/bge-multilingual-gemma2" } # Get the actual model name actual_model = model_mapping.get(embedding_model, embedding_model) # Check if it's a Nebius model if any(nebius_model in actual_model for nebius_model in [ "Qwen/Qwen3-Embedding-8B", "BAAI/bge-en-icl", "BAAI/bge-multilingual-gemma2" ]): try: return NebiusEmbedding(model_name=actual_model) except Exception as e: debug_print(f"Failed to create Nebius embedding: {e}") debug_print("Falling back to default HuggingFace embedding") return HuggingFaceEmbeddings( model_name="sentence-transformers/all-MiniLM-L6-v2", model_kwargs={"device": "cpu"} ) else: # Default to HuggingFace embeddings for all other models return HuggingFaceEmbeddings( model_name=actual_model, model_kwargs={"device": "cpu"} ) # Instance method to capture context and conversation history def capture_context(self, result): self.context = "\n".join([str(doc) for doc in result["context"]]) result["context"] = self.context history_text = ( "\n".join([f"Q: {conv['query']}\nA: {conv['response']}" for conv in self.conversation_history]) if self.conversation_history else "" ) result["conversation_history"] = history_text return result # Instance method to extract question from input data def extract_question(self, input_data): return input_data["question"] # Improve error handling in the ElevatedRagChain class def calculate_safe_max_tokens(self, backend_model, prompt_text="", max_tokens_override=None): """Calculate safe max_tokens based on model context limits and input length.""" # Model context limits (total tokens including input + output) - from Nebius documentation nebius_context_limits = { "openai/gpt-oss-120b": 131000, "openai/gpt-oss-20b": 131000, "google/gemma-3-27b-it": 131000, "deepseek-ai/DeepSeek-R1-0528": 164000, "deepseek-ai/DeepSeek-V3": 128000, "llama-3.1-70b-instruct": 128000, "Meta-Llama-3.1-405B-Instruct": 128000, "Qwen/Qwen3-235B-A22B": 262000, "Qwen/Qwen3-32B": 41000, # Correct limit: 41K context "NousResearch/Hermes-4-405B": 128000, "zai-org/GLM-4.5-Air": 128000 } context_limit = nebius_context_limits.get(backend_model, 32768) # Better token estimation def estimate_tokens(text): """Rough token estimation: 1 token β 3.5-4 characters for most models.""" if not text: return 0 # More conservative estimate return int(len(str(text)) / 3.5) # Estimate input tokens from various sources input_tokens = 0 # Add tokens from retrieved documents if hasattr(self, 'retrieved_docs') and self.retrieved_docs: docs_text = ' '.join([doc.page_content for doc in self.retrieved_docs]) input_tokens += estimate_tokens(docs_text) # Add tokens from prompt/query input_tokens += estimate_tokens(prompt_text) # Add tokens from system prompts and formatting (rough estimate) input_tokens += 500 # Buffer for system messages, formatting, etc. # Calculate safe max_tokens available_tokens = context_limit - input_tokens # Apply user override if provided, but cap it at available tokens if max_tokens_override: requested_tokens = min(max_tokens_override, available_tokens - 100) # 100 token safety buffer else: # Default to 25% of available tokens, capped at reasonable limits requested_tokens = min( int(available_tokens * 0.25), # 25% of available space 8192 # Reasonable upper limit for generation ) # Ensure minimum viable response length safe_max_tokens = max(512, requested_tokens) debug_print(f"Token calculation for {backend_model}:") debug_print(f" Context limit: {context_limit}") debug_print(f" Estimated input tokens: {input_tokens}") debug_print(f" Available tokens: {available_tokens}") debug_print(f" Safe max_tokens: {safe_max_tokens}") if safe_max_tokens <= 512: raise ValueError(f"Input too long for model {backend_model}. Input: {input_tokens} tokens, Context limit: {context_limit}") return safe_max_tokens def create_llm_pipeline(self, max_tokens_override=None): from langchain.llms.base import LLM # Import LLM here so it's always defined from typing import Optional, List, Any, Dict from pydantic import PrivateAttr # Check for Nebius models FIRST (before any normalization) debug_print(f"Checking for Nebius model: {self.llm_choice}") print(f"DEBUG: Checking for Nebius model: {self.llm_choice}") if self.llm_choice in ["π¦ GPT OSS 120b (Nebius)", "π¦ GPT OSS 20b (Nebius)", "π¦ Google Gemma 3 27b-Instruct (Nebius)", "π¦ DeepSeek-R1-0528 (Nebius)", "π¦ DeepSeek-V3 (Nebius)", "π¦ Meta-Llama-3.1-70B-Instruct (Nebius)", "π¦ Meta-Llama-3.1-405B-Instruct (Nebius)", "π¦ Qwen3-235B-A22B (Nebius)", "π¦ Qwen3-32B (Nebius)", "π¦ Hermes 4 405B (Nebius)", "π¦ GLM-4.5 AIR (Nebius)"]: debug_print(f"Found Nebius model: {self.llm_choice}") print(f"DEBUG: Found Nebius model: {self.llm_choice}") if not OPENAI_AVAILABLE: raise ImportError("openai package is required for NEBIUS models.") # Map display names to backend names nebius_model_mapping = { "π¦ GPT OSS 120b (Nebius)": "openai/gpt-oss-120b", "π¦ GPT OSS 20b (Nebius)": "openai/gpt-oss-20b", "π¦ Google Gemma 3 27b-Instruct (Nebius)": "google/gemma-3-27b-it", "π¦ DeepSeek-R1-0528 (Nebius)": "deepseek-ai/DeepSeek-R1-0528", "π¦ DeepSeek-V3 (Nebius)": "deepseek-ai/DeepSeek-V3", "π¦ Meta-Llama-3.1-70B-Instruct (Nebius)": "llama-3.1-70b-instruct", "π¦ Meta-Llama-3.1-405B-Instruct (Nebius)": "Meta-Llama-3.1-405B-Instruct", "π¦ Qwen3-235B-A22B (Nebius)": "Qwen/Qwen3-235B-A22B", "π¦ Qwen3-32B (Nebius)": "Qwen/Qwen3-32B", "π¦ Hermes 4 405B (Nebius)": "NousResearch/Hermes-4-405B", "π¦ GLM-4.5 AIR (Nebius)": "zai-org/GLM-4.5-Air" } # Set appropriate token limits for Nebius models # These are MAXIMUM GENERATION tokens, not total context length nebius_token_limits = { "openai/gpt-oss-120b": 8192, # Conservative limit "openai/gpt-oss-20b": 8192, # Conservative limit "google/gemma-3-27b-it": 4096, # Conservative for 8K context "deepseek-ai/DeepSeek-R1-0528": 8192, "deepseek-ai/DeepSeek-V3": 16384, "meta-llama/Meta-Llama-3.1-70B-Instruct": 32768, "meta-llama/Meta-Llama-3.1-405B-Instruct": 32768, "Qwen/Qwen3-235B-A22B": 8192, "Qwen/Qwen3-32B": 8192, # Reduced from 32768 - model has 40K total context "NousResearch/Hermes-4-405B": 32768, "zai-org/GLM-4.5-Air": 16384 } # Model context limits (total tokens including input + output) - from Nebius documentation nebius_context_limits = { "openai/gpt-oss-120b": 131000, "openai/gpt-oss-20b": 131000, "google/gemma-3-27b-it": 131000, "deepseek-ai/DeepSeek-R1-0528": 164000, "deepseek-ai/DeepSeek-V3": 128000, "meta-llama/Meta-Llama-3.1-70B-Instruct": 128000, "meta-llama/Meta-Llama-3.1-405B-Instruct": 128000, "Qwen/Qwen3-235B-A22B": 262000, "Qwen/Qwen3-32B": 41000, # Correct limit: 41K context "NousResearch/Hermes-4-405B": 128000, "zai-org/GLM-4.5-Air": 128000 } backend_model = nebius_model_mapping[self.llm_choice] # Calculate safe max_tokens based on context limit context_limit = nebius_context_limits.get(backend_model, 32768) # Calculate safe max_tokens dynamically try: # Get current prompt if available current_prompt = getattr(self, 'current_query', '') max_tokens = self.calculate_safe_max_tokens( backend_model, current_prompt, max_tokens_override ) except ValueError as e: debug_print(f"Token calculation error: {str(e)}") # Fallback to model-specific token limits max_tokens = nebius_token_limits.get(backend_model, 2048) debug_print(f"Creating Nebius LLM for model: {backend_model} with max_tokens: {max_tokens}") print(f"DEBUG: Creating Nebius LLM for model: {backend_model} with max_tokens: {max_tokens}") try: api_key = NEBIUS_API_KEY or os.environ.get("NEBIUS_API_KEY") if not api_key: raise ValueError("Please set the NEBIUS_API_KEY either in the code or as an environment variable.") nebius_llm = NebiusLLM( model=backend_model, api_key=api_key, temperature=self.temperature, top_p=self.top_p, top_k=self.top_k, max_tokens=max_tokens ) debug_print("Nebius API pipeline created successfully.") print(f"DEBUG: Nebius API pipeline created successfully for {self.llm_choice}") return nebius_llm except Exception as e: error_msg = f"Failed to create Nebius LLM: {str(e)}" debug_print(error_msg) print(f"DEBUG: {error_msg}") raise ValueError(error_msg) # Extract the model name without the flag emoji prefix (for non-Nebius models) clean_llm_choice = self.llm_choice.split(" ", 1)[-1] if " " in self.llm_choice else self.llm_choice normalized = clean_llm_choice.lower() print(f"Normalized model name: {normalized}") # Model configurations from the second file model_token_limits = { "gpt-3.5": 16385, "gpt-4o": 128000, "gpt-4o-mini": 128000, "meta-llama-3": 4096, "mistral-api": 128000, "o1-mini": 128000, "o3-mini": 128000 } model_map = { "gpt-3.5": "gpt-3.5-turbo", "gpt-4o": "gpt-4o", "gpt-4o mini": "gpt-4o-mini", "o1-mini": "gpt-4o-mini", "o3-mini": "gpt-4o-mini", "mistral": "mistral-small-latest", "mistral-api": "mistral-small-latest", "meta-llama-3": "meta-llama/Meta-Llama-3-8B-Instruct", "remote meta-llama-3": "meta-llama/Meta-Llama-3-8B-Instruct" } model_pricing = { "gpt-3.5": {"USD": {"input": 0.0000005, "output": 0.0000015}, "RON": {"input": 0.0000023, "output": 0.0000069}}, "gpt-4o": {"USD": {"input": 0.0000025, "output": 0.00001}, "RON": {"input": 0.0000115, "output": 0.000046}}, "gpt-4o-mini": {"USD": {"input": 0.00000015, "output": 0.0000006}, "RON": {"input": 0.0000007, "output": 0.0000028}}, "o1-mini": {"USD": {"input": 0.0000011, "output": 0.0000044}, "RON": {"input": 0.0000051, "output": 0.0000204}}, "o3-mini": {"USD": {"input": 0.0000011, "output": 0.0000044}, "RON": {"input": 0.0000051, "output": 0.0000204}}, "meta-llama-3": {"USD": {"input": 0.00, "output": 0.00}, "RON": {"input": 0.00, "output": 0.00}}, "mistral": {"USD": {"input": 0.00, "output": 0.00}, "RON": {"input": 0.00, "output": 0.00}}, "mistral-api": {"USD": {"input": 0.00, "output": 0.00}, "RON": {"input": 0.00, "output": 0.00}} } pricing_info = "" # Find the matching model model_key = None for key in model_map: if key.lower() in normalized: model_key = key break if not model_key: raise ValueError(f"Unsupported model: {normalized}") model = model_map[model_key] max_tokens = self.max_tokens if max_tokens_override is not None: max_tokens = min(max_tokens_override, max_tokens) pricing_info = model_pricing.get(model_key, {"USD": {"input": 0.00, "output": 0.00}, "RON": {"input": 0.00, "output": 0.00}}) try: # OpenAI models (GPT-3.5, GPT-4o, GPT-4o mini, o1-mini, o3-mini) if any(model in normalized for model in ["gpt-3.5", "gpt-4o", "o1-mini", "o3-mini"]): debug_print(f"Creating OpenAI API pipeline for {normalized}...") openai_api_key = os.environ.get("OPENAI_API_KEY") if not openai_api_key: raise ValueError("Please set the OPENAI_API_KEY environment variable to use OpenAI API.") import openai class OpenAILLM(LLM): model_name: str = model llm_choice: str = model max_context_tokens: int = max_tokens pricing: dict = pricing_info temperature: float = 0.7 top_p: float = 0.95 top_k: int = 50 @property def _llm_type(self) -> str: return "openai_llm" def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: try: openai.api_key = openai_api_key print(f" tokens: {max_tokens}") response = openai.ChatCompletion.create( model=self.model_name, messages=[{"role": "user", "content": prompt}], temperature=self.temperature, top_p=self.top_p, max_tokens=max_tokens ) return response["choices"][0]["message"]["content"] except Exception as e: debug_print(f"OpenAI API error: {str(e)}") return f"Error generating response: {str(e)}" @property def _identifying_params(self) -> dict: return { "model": self.model_name, "max_tokens": self.max_context_tokens, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k } debug_print(f"OpenAI {model} pipeline created successfully.") return OpenAILLM() # Meta-Llama-3 model (but not Nebius models) elif ("meta-llama" in normalized or "llama" in normalized) and "nebius" not in normalized: debug_print("Creating remote Meta-Llama-3 pipeline via Hugging Face Inference API...") from huggingface_hub import InferenceClient repo_id = "meta-llama/Meta-Llama-3-8B-Instruct" hf_api_token = os.environ.get("HF_API_TOKEN") if not hf_api_token: raise ValueError("Please set the HF_API_TOKEN environment variable to use remote inference.") client = InferenceClient(token=hf_api_token, timeout=120) def remote_generate(prompt: str) -> str: max_retries = 3 backoff = 2 # start with 2 seconds for attempt in range(max_retries): try: debug_print(f"Remote generation attempt {attempt+1} tokens: {self.max_tokens}") response = client.text_generation( prompt, model=repo_id, temperature=self.temperature, top_p=self.top_p, max_tokens= max_tokens # Reduced token count for speed ) return response except Exception as e: debug_print(f"Attempt {attempt+1} failed with error: {e}") if attempt == max_retries - 1: raise time.sleep(backoff) backoff *= 2 # exponential backoff return "Failed to generate response after multiple attempts." class RemoteLLM(LLM): model_name: str = repo_id llm_choice: str = repo_id max_context_tokens: int = max_tokens pricing: dict = pricing_info @property def _llm_type(self) -> str: return "remote_llm" def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: return remote_generate(prompt) @property def _identifying_params(self) -> dict: return {"model": self.model_name, "max_tokens": self.max_context_tokens} debug_print("Remote Meta-Llama-3 pipeline created successfully.") return RemoteLLM() # Mistral API model elif "mistral" in normalized: debug_print("Creating Mistral API pipeline...") mistral_api_key = os.environ.get("MISTRAL_API_KEY") if not mistral_api_key: raise ValueError("Please set the MISTRAL_API_KEY environment variable to use Mistral API.") try: from mistralai import Mistral debug_print("Mistral library imported successfully") except ImportError: raise ImportError("Mistral client library not installed. Please install with 'pip install mistralai'.") class MistralLLM(LLM): temperature: float = 0.7 top_p: float = 0.95 model_name: str = model llm_choice: str = model pricing: dict = pricing_info _client: Any = PrivateAttr(default=None) def __init__(self, api_key: str, temperature: float = 0.7, top_p: float = 0.95, **kwargs: Any): try: super().__init__(**kwargs) # Bypass Pydantic's __setattr__ to assign to _client object.__setattr__(self, '_client', Mistral(api_key=api_key)) self.temperature = temperature self.top_p = top_p except Exception as e: debug_print(f"Init Mistral failed with error: {e}") @property def _llm_type(self) -> str: return "mistral_llm" def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: try: debug_print(f"Calling Mistral API... tokens: {max_tokens}") response = self._client.chat.complete( model=self.model_name, messages=[{"role": "user", "content": prompt}], temperature=self.temperature, top_p=self.top_p, max_tokens= max_tokens ) return response.choices[0].message.content except Exception as e: debug_print(f"Mistral API error: {str(e)}") return f"Error generating response: {str(e)}" @property def _identifying_params(self) -> dict: return {"model": self.model_name, "max_tokens": max_tokens} debug_print("Creating Mistral LLM instance") mistral_llm = MistralLLM(api_key=mistral_api_key, temperature=self.temperature, top_p=self.top_p) debug_print("Mistral API pipeline created successfully.") return mistral_llm else: raise ValueError(f"Unsupported model choice: {self.llm_choice}") except Exception as e: debug_print(f"Error creating LLM pipeline: {str(e)}") # Return a dummy LLM that explains the error class ErrorLLM(LLM): @property def _llm_type(self) -> str: return "error_llm" def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: return f"Error initializing LLM: \n\nPlease check your environment variables and try again." @property def _identifying_params(self) -> dict: return {"model": "error"} return ErrorLLM() def update_llm_pipeline(self, new_model_choice: str, temperature: float, top_p: float, top_k: int, prompt_template: str, bm25_weight: float, max_tokens: int = 3000): debug_print(f"Updating chain with new model: {new_model_choice}") self.llm_choice = new_model_choice self.temperature = temperature self.top_p = top_p self.top_k = top_k self.max_tokens = max_tokens self.prompt_template = prompt_template self.bm25_weight = bm25_weight self.faiss_weight = 1.0 - bm25_weight self.llm = self.create_llm_pipeline() def format_response(response: str) -> str: input_tokens = count_tokens(self.context + self.prompt_template) output_tokens = count_tokens(response) formatted = f"β Response:\n\n" formatted += f"Model: {self.llm_choice}\n" formatted += f"Model Parameters:\n" formatted += f"- Temperature: {self.temperature}\n" formatted += f"- Top-p: {self.top_p}\n" formatted += f"- Top-k: {self.top_k}\n" formatted += f"- BM25 Weight: {self.bm25_weight}\n\n" formatted += f"{response}\n\n---\n" formatted += f"- **Input tokens:** {input_tokens}\n" formatted += f"- **Output tokens:** {output_tokens}\n" formatted += f"- **Generated using:** {self.llm_choice}\n" formatted += f"\n**Conversation History:** {len(self.conversation_history)} conversation(s) considered.\n" return formatted base_runnable = RunnableParallel({ "context": RunnableLambda(self.extract_question) | self.ensemble_retriever, "question": RunnableLambda(self.extract_question) }) | self.capture_context self.elevated_rag_chain = base_runnable | self.rag_prompt | self.llm | format_response debug_print("Chain updated successfully with new LLM pipeline.") def add_pdfs_to_vectore_store(self, file_links: List[str]) -> None: debug_print(f"Processing files using {self.llm_choice}") self.raw_data = [] for link in file_links: if "drive.google.com" in link and ("file/d" in link or "open?id=" in link): debug_print(f"Loading Google Drive file: {link}") try: documents = load_file_from_google_drive(link) self.raw_data.extend(documents) debug_print(f"Successfully loaded {len(documents)} pages/documents from Google Drive") except Exception as e: debug_print(f"Error loading Google Drive file {link}: {e}") elif link.lower().endswith(".pdf"): debug_print(f"Loading PDF: {link}") loaded_docs = OnlinePDFLoader(link).load() if loaded_docs: self.raw_data.append(loaded_docs[0]) else: debug_print(f"No content found in PDF: {link}") elif link.lower().endswith(".txt") or link.lower().endswith(".utf-8"): debug_print(f"Loading TXT: {link}") try: self.raw_data.append(load_txt_from_url(link)) except Exception as e: debug_print(f"Error loading TXT file {link}: {e}") else: debug_print(f"File type not supported for URL: {link}") debug_print("Files loaded successfully.") debug_print("Starting text splitting...") self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=100) self.split_data = self.text_splitter.split_documents(self.raw_data) if not self.split_data: raise ValueError("Text splitting resulted in no chunks. Check the file contents.") debug_print(f"Text splitting completed. Number of chunks: {len(self.split_data)}") debug_print("Creating BM25 retriever...") self.bm25_retriever = BM25Retriever.from_documents(self.split_data) self.bm25_retriever.k = self.top_k debug_print("BM25 retriever created.") debug_print("Embedding chunks and creating FAISS vector store...") self.vector_store = FAISS.from_documents(self.split_data, self.embed_func) self.faiss_retriever = self.vector_store.as_retriever(search_kwargs={"k": self.top_k}) debug_print("FAISS vector store created successfully.") self.ensemble_retriever = EnsembleRetriever( retrievers=[self.bm25_retriever, self.faiss_retriever], weights=[self.bm25_weight, self.faiss_weight] ) base_runnable = RunnableParallel({ "context": RunnableLambda(self.extract_question) | self.ensemble_retriever, "question": RunnableLambda(self.extract_question) }) | self.capture_context # Ensure the prompt template is set self.rag_prompt = ChatPromptTemplate.from_template(self.prompt_template) if self.rag_prompt is None: raise ValueError("Prompt template could not be created from the given template.") prompt_runnable = RunnableLambda(lambda vars: self.rag_prompt.format(**vars)) self.str_output_parser = StrOutputParser() debug_print("Selecting LLM pipeline based on choice: " + self.llm_choice) self.llm = self.create_llm_pipeline() if self.llm is None: raise ValueError("LLM pipeline creation failed.") def format_response(response: str) -> str: input_tokens = count_tokens(self.context + self.prompt_template) output_tokens = count_tokens(response) formatted = f"β Response:\n\n" formatted += f"Model: {self.llm_choice}\n" formatted += f"Model Parameters:\n" formatted += f"- Temperature: {self.temperature}\n" formatted += f"- Top-p: {self.top_p}\n" formatted += f"- Top-k: {self.top_k}\n" formatted += f"- BM25 Weight: {self.bm25_weight}\n\n" formatted += f"{response}\n\n---\n" formatted += f"- **Input tokens:** {input_tokens}\n" formatted += f"- **Output tokens:** {output_tokens}\n" formatted += f"- **Generated using:** {self.llm_choice}\n" formatted += f"\n**Conversation History:** {len(self.conversation_history)} conversation(s) considered.\n" return formatted self.elevated_rag_chain = base_runnable | prompt_runnable | self.llm | format_response debug_print("Elevated RAG chain successfully built and ready to use.") def get_current_context(self) -> str: base_context = "\n".join([str(doc) for doc in self.split_data[:3]]) if self.split_data else "No context available." history_summary = "\n\n---\n**Recent Conversations (last 3):**\n" recent = self.conversation_history[-3:] if recent: for i, conv in enumerate(recent, 1): history_summary += f"**Conversation {i}:**\n- Query: {conv['query']}\n- Response: {conv['response']}\n" else: history_summary += "No conversation history." return base_context + history_summary # ---------------------------- # Gradio Interface Functions # ---------------------------- global rag_chain rag_chain = ElevatedRagChain() def load_pdfs_updated(file_links, prompt_template, bm25_weight, embedding_model): debug_print("Inside load_pdfs function.") if not file_links: debug_print("Please enter non-empty URLs") return "Please enter non-empty URLs", "Word count: N/A", "Model used: N/A", "Context: N/A" try: links = [link.strip() for link in file_links.split("\n") if link.strip()] global rag_chain if rag_chain.raw_data: # Files already loaded, just update parameters rag_chain.prompt_template = prompt_template rag_chain.bm25_weight = bm25_weight rag_chain.faiss_weight = 1.0 - bm25_weight context_display = rag_chain.get_current_context() response_msg = f"Files already loaded. Parameters updated." return ( response_msg, f"Word count: {word_count(rag_chain.context)}", f"Embedding model: {rag_chain.embedding_model}", f"Context:\n{context_display}" ) else: rag_chain = ElevatedRagChain( llm_choice="Mistral-API", # Default LLM choice prompt_template=prompt_template, bm25_weight=bm25_weight, temperature=0.5, # Default values top_p=0.95, top_k=50, embedding_model=embedding_model ) rag_chain.add_pdfs_to_vectore_store(links) context_display = rag_chain.get_current_context() response_msg = f"Files loaded successfully. Using embedding model: {embedding_model}" return ( response_msg, f"Word count: {word_count(rag_chain.context)}", f"Embedding model: {rag_chain.embedding_model}", f"Context:\n{context_display}" ) except Exception as e: error_msg = traceback.format_exc() debug_print("Could not load files. Error: " + error_msg) return ( "Error loading files: " + str(e), f"Word count: {word_count('')}", f"Model used: {rag_chain.llm_choice}", "Context: N/A" ) def update_model(new_model: str): global rag_chain if rag_chain and rag_chain.raw_data: rag_chain.update_llm_pipeline(new_model, rag_chain.temperature, rag_chain.top_p, rag_chain.top_k, rag_chain.prompt_template, rag_chain.bm25_weight, rag_chain.max_tokens) debug_print(f"Model updated to {rag_chain.llm_choice}") return f"Model updated to: {rag_chain.llm_choice}" else: return "No files loaded; please load files first." # Update submit_query_updated to better handle context limitation def submit_query_updated(query, temperature, top_p, top_k, bm25_weight, use_history=True): """Submit a query and return the response""" try: if not query: return "Please enter a non-empty query", "", "Input tokens: 0", "Output tokens: 0" # Update BM25 weight and recreate ensemble retriever if needed if hasattr(rag_chain, 'bm25_weight') and rag_chain.bm25_weight != bm25_weight: rag_chain.bm25_weight = bm25_weight rag_chain.faiss_weight = 1.0 - bm25_weight rag_chain.ensemble_retriever = EnsembleRetriever( retrievers=[rag_chain.bm25_retriever, rag_chain.faiss_retriever], weights=[rag_chain.bm25_weight, rag_chain.faiss_weight] ) debug_print(f"Updated ensemble retriever with BM25 weight: {bm25_weight}") # Clear conversation history if checkbox is unchecked if not use_history: rag_chain.conversation_history = [] debug_print("Conversation history cleared") result = rag_chain.chain({"question": query}) response = result["answer"] context = rag_chain.get_current_context() # Format the response formatted_response = format_response(response) # Get token counts input_tokens = count_tokens(query + context) output_tokens = count_tokens(response) return ( formatted_response, context, f"Input tokens: {input_tokens}", f"Output tokens: {output_tokens}" ) except Exception as e: error_msg = f"Error processing query: {str(e)}" debug_print(error_msg) return error_msg, "", "Input tokens: 0", "Output tokens: 0" def format_response(response: str) -> str: """Format the response to include model info and main answer""" try: # Split response into components parts = response.split("\n\n") # Extract main answer (usually the first part) main_answer = parts[0].strip() # Extract model info if present model_info = "" for part in parts: if "Model:" in part: model_info = part.strip() break # Format the response formatted = [] if model_info: formatted.append(model_info) formatted.append("\nAnswer:") formatted.append(main_answer) return "\n".join(formatted) except Exception as e: debug_print(f"Error formatting response: {str(e)}") return response def reset_app_updated(): global rag_chain # Properly clean up the existing vector database components if hasattr(rag_chain, 'vector_store'): try: del rag_chain.vector_store except: pass if hasattr(rag_chain, 'faiss_retriever'): try: del rag_chain.faiss_retriever except: pass if hasattr(rag_chain, 'bm25_retriever'): try: del rag_chain.bm25_retriever except: pass if hasattr(rag_chain, 'ensemble_retriever'): try: del rag_chain.ensemble_retriever except: pass # Clear data references if hasattr(rag_chain, 'raw_data'): rag_chain.raw_data = None if hasattr(rag_chain, 'split_data'): rag_chain.split_data = None if hasattr(rag_chain, 'context'): rag_chain.context = "" if hasattr(rag_chain, 'conversation_history'): rag_chain.conversation_history = [] # Clear other components if hasattr(rag_chain, 'text_splitter'): try: del rag_chain.text_splitter except: pass if hasattr(rag_chain, 'elevated_rag_chain'): try: del rag_chain.elevated_rag_chain except: pass # Create a new instance rag_chain = ElevatedRagChain() # Force garbage collection to free memory gc.collect() debug_print("App reset successfully. Vector database and all components cleaned up.") return ( "App reset successfully. Vector database and all components cleaned up. You can now load new files", "", "Model used: Not selected" ) # ---------------------------- # Gradio Interface Setup # ---------------------------- custom_css = """ textarea { overflow-y: scroll !important; max-height: 200px; } """ # Function to add dots and reset def add_dots_and_reset(): if not hasattr(add_dots_and_reset, "dots"): add_dots_and_reset.dots = "" # Initialize the attribute # Add a dot add_dots_and_reset.dots += "." # Reset after 5 dots if len(add_dots_and_reset.dots) > 5: add_dots_and_reset.dots = "" print(f"Current dots: {add_dots_and_reset.dots}") # Debugging print return add_dots_and_reset.dots # Define a dummy function to simulate data retrieval def run_query(max_value): # Simulate a data retrieval or processing function return [[i, i**2] for i in range(1, max_value + 1)] # Function to call both refresh_job_list and check_job_status using the last job ID def periodic_update(is_checked): interval = 2 if is_checked else None debug_print(f"Auto-refresh checkbox is {'checked' if is_checked else 'unchecked'}, every={interval}") if is_checked: global last_job_id job_list_md = refresh_job_list() job_status = check_job_status(last_job_id) if last_job_id else ("No job ID available", "", "", "", "") query_results = run_query(10) # Use a fixed value or another logic if needed context_info = rag_chain.get_current_context() if rag_chain else "No context available." return job_list_md, job_status[0], query_results, context_info else: # Return empty values to stop updates return "", "", [], "" # Define a function to determine the interval based on the checkbox state def get_interval(is_checked): return 2 if is_checked else None # CSV file management functions (copied exactly from psyllm.py) def list_all_csv_files(): csv_files = sorted(glob.glob("*.csv"), key=os.path.getmtime, reverse=True) zip_files = sorted(glob.glob("*.zip"), key=os.path.getmtime, reverse=True) all_files = csv_files + zip_files if not all_files: return "No CSV or ZIP files found.", [], [], [] # Gather file info: name, date/time, size file_infos = [] for f in all_files: stat = os.stat(f) dt = datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S') size_kb = stat.st_size / 1024 file_infos.append({ "name": os.path.basename(f), "path": os.path.abspath(f), "datetime": dt, "size_kb": f"{size_kb:.1f} KB" }) # HTML table with columns: Name, Date/Time, Size html_links = '
| File | Date/Time | Size | 
|---|---|---|
| {info["name"]} | ' \ f'{info["datetime"]} | {info["size_kb"]} |