Spaces:
Sleeping
Sleeping
| """ | |
| Real LLM-based generator using Groq or Google Gemini API. | |
| This ACTUALLY generates responses (unlike SimpleGenerator which just extracts text). | |
| """ | |
| import os | |
| from typing import List, Dict, Optional | |
| import streamlit as st | |
| try: | |
| from groq import Groq | |
| GROQ_AVAILABLE = True | |
| except ImportError: | |
| GROQ_AVAILABLE = False | |
| try: | |
| import google.generativeai as genai | |
| GEMINI_AVAILABLE = True | |
| except ImportError: | |
| GEMINI_AVAILABLE = False | |
| class LLMGenerator: | |
| """ | |
| Actual LLM-based response generation using Groq (Llama-3-70B) or Gemini. | |
| This is what NotebookLM uses - real AI generation, not text extraction. | |
| """ | |
| def __init__(self, provider: str = "groq", api_key: Optional[str] = None): | |
| """ | |
| Initialize LLM generator. | |
| Args: | |
| provider: "groq" or "gemini" | |
| api_key: API key (if None, reads from environment or asks user) | |
| """ | |
| self.provider = provider | |
| self.client = None | |
| self.ready = False | |
| # Get API key | |
| if api_key: | |
| self.api_key = api_key | |
| elif provider == "groq": | |
| self.api_key = os.getenv("GROQ_API_KEY", "") | |
| elif provider == "gemini": | |
| self.api_key = os.getenv("GEMINI_API_KEY", "") | |
| else: | |
| self.api_key = "" | |
| # Initialize client | |
| self._initialize_client() | |
| def _initialize_client(self): | |
| """Initialize the LLM client.""" | |
| if not self.api_key: | |
| return | |
| try: | |
| if self.provider == "groq" and GROQ_AVAILABLE: | |
| # Initialize Groq client with explicit parameters | |
| # Avoid potential proxies kwarg issue by not passing extra config | |
| import os | |
| os.environ["GROQ_API_KEY"] = self.api_key | |
| self.client = Groq() # Will read from environment | |
| self.ready = True | |
| elif self.provider == "gemini" and GEMINI_AVAILABLE: | |
| genai.configure(api_key=self.api_key) | |
| self.client = genai.GenerativeModel('gemini-2.5-flash') | |
| self.ready = True | |
| except Exception as e: | |
| print(f"Failed to initialize {self.provider}: {e}") | |
| self.ready = False | |
| def set_api_key(self, api_key: str): | |
| """Update API key and reinitialize.""" | |
| self.api_key = api_key | |
| self._initialize_client() | |
| def generate_response( | |
| self, | |
| prompt: str, | |
| context: str = "", | |
| use_case: str = "explanation", | |
| metadatas: List[Dict] = None, | |
| temperature: float = 0.7, | |
| max_tokens: int = 1500, | |
| **kwargs | |
| ) -> str: | |
| """ | |
| Generate response using actual LLM (NotebookLM-style). | |
| Args: | |
| prompt: User's question | |
| context: Retrieved context from documents | |
| use_case: Response type (explanation, summary, qa, notes) | |
| metadatas: Metadata for citations | |
| temperature: LLM temperature (0.0-1.0) | |
| max_tokens: Maximum response length | |
| Returns: | |
| Generated response with inline citations | |
| """ | |
| if not self.ready: | |
| return ( | |
| "⚠️ **LLM not configured.** Please add your API key in the sidebar.\n\n" | |
| "Get a free key:\n" | |
| "- **Groq** (recommended, very fast): https://console.groq.com/keys\n" | |
| "- **Gemini** (Google): https://makersuite.google.com/app/apikey" | |
| ) | |
| if not context: | |
| return ( | |
| "I don't have enough information from your uploaded documents to answer this question. " | |
| "Please upload relevant study materials first." | |
| ) | |
| # Build NotebookLM-style system prompt with strict source grounding | |
| system_prompt = self._build_system_prompt(use_case) | |
| # Build user message with context | |
| user_message = self._build_user_message(prompt, context, metadatas) | |
| try: | |
| # Generate with LLM | |
| if self.provider == "groq": | |
| response = self._generate_groq(system_prompt, user_message, temperature, max_tokens) | |
| elif self.provider == "gemini": | |
| response = self._generate_gemini(system_prompt, user_message, temperature, max_tokens) | |
| else: | |
| return "Error: Unknown provider" | |
| return response | |
| except Exception as e: | |
| return f"Error generating response: {str(e)}\n\nPlease check your API key and try again." | |
| def _build_system_prompt(self, use_case: str) -> str: | |
| """Build specialized system prompt based on use case.""" | |
| base_prompt = ( | |
| "You are an expert academic assistant for students, acting like a highly intelligent study buddy. " | |
| "⚠️ CRITICAL RULE: You MUST ONLY use information from the provided context below. " | |
| "DO NOT use your training knowledge. DO NOT infer beyond what's explicitly stated. " | |
| "If the context doesn't contain adequate information to answer the question, you MUST respond: " | |
| "'I cannot find sufficient information about this in the uploaded documents. Please upload materials covering this topic or rephrase your question.'\n\n" | |
| "⚠️ GROUNDING REQUIREMENT: Every statement must be traceable to the provided context. " | |
| "If you cannot find it in the context below, DO NOT answer from general knowledge.\n\n" | |
| "✨ FORMATTING RULES (NotebookLM Style):\n" | |
| "- Use clean, hierarchical Markdown (### Headers, **Bold** terms).\n" | |
| "- Break down long paragraphs into easily readable bullet points.\n" | |
| "- Be direct and concise. Avoid conversational fluff like 'Certainly!' or 'Here is the answer'.\n" | |
| "- If applicable to the prompt, always try to extract a **Real-World Example** from the text to aid understanding.\n\n" | |
| ) | |
| if use_case == "explanation": | |
| base_prompt += ( | |
| "**Your task:** Explain the concept in a clear, step-by-step manner suitable for students.\n" | |
| "1. Start with a concise, one-sentence definition.\n" | |
| "2. Break down the core mechanics or components using bullet points.\n" | |
| "3. Provide an example (only if found in the text).\n" | |
| "4. Add a 'Key Takeaway' at the end.\n" | |
| ) | |
| elif use_case == "summary": | |
| base_prompt += ( | |
| "**Your task:** Create a highly structured summary.\n" | |
| "- Start with a brief high-level overview (2 sentences max).\n" | |
| "- Use '### Key Themes' and list the main points as bulleted items.\n" | |
| "- Keep each point concise but factually dense.\n" | |
| ) | |
| elif use_case == "qa": | |
| base_prompt += ( | |
| "**Your task:** Answer the question directly and comprehensively.\n" | |
| "- Provide the direct answer immediately in the first sentence.\n" | |
| "- Use numbered lists or bullet points to provide supporting details from the context.\n" | |
| "- Use **bold** for key facts, numbers, and formulas.\n" | |
| ) | |
| elif use_case == "notes": | |
| base_prompt += ( | |
| "**Your task:** Create comprehensive, structured study notes.\n" | |
| "- Use clear section headers (###).\n" | |
| "- Organize information hierarchically (using nested bullet points).\n" | |
| "- Explicitly highlight **Definitions**, **Formulas**, and **Important Dates/Names**.\n" | |
| ) | |
| base_prompt += ( | |
| "\n**Citation Rules:**\n" | |
| "- You MUST cite your source at the end of every major claim or paragraph using numbered brackets like **[1]**, **[2]** based on the Source number provided in the context.\n" | |
| "- If a claim comes from multiple sources, use **[1, 2]**.\n" | |
| "- Do NOT use the document filename in the citation, ONLY the number.\n" | |
| "- Do NOT make up information - stick strictly to the provided context.\n" | |
| ) | |
| return base_prompt | |
| def _build_user_message(self, prompt: str, context: str, metadatas: List[Dict] = None) -> str: | |
| """Build user message with context and question.""" | |
| # Extract source names from metadata | |
| sources = [] | |
| if metadatas: | |
| for meta in metadatas: | |
| filename = meta.get('filename', 'Unknown') | |
| clean_name = filename.replace('.pdf', '').replace('.docx', '').replace('.txt', '') | |
| if clean_name not in sources: | |
| sources.append(clean_name) | |
| message = "**Available Sources (USE ONLY THESE):**\n" | |
| for source in sources[:5]: # Show up to 5 sources | |
| message += f"- {source}\n" | |
| message += f"\n**===== START OF CONTEXT (ANSWER ONLY FROM THIS) =====**\n\n{context}\n\n" | |
| message += f"**===== END OF CONTEXT =====**\n\n" | |
| message += f"**Student's Question:** {prompt}\n\n" | |
| message += "**Instructions:** Answer ONLY using the context between the markers above. If the context doesn't contain the answer, say you don't have that information. Cite sources in brackets." | |
| return message | |
| def _generate_groq(self, system_prompt: str, user_message: str, temperature: float, max_tokens: int) -> str: | |
| """Generate using Groq API (Llama-3.3-70B).""" | |
| completion = self.client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", # Latest 70B model (Dec 2024) | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_message} | |
| ], | |
| temperature=temperature, | |
| max_tokens=max_tokens, | |
| top_p=0.95, | |
| stream=False | |
| ) | |
| return completion.choices[0].message.content | |
| def _generate_gemini(self, system_prompt: str, user_message: str, temperature: float, max_tokens: int) -> str: | |
| """Generate using Google Gemini API.""" | |
| full_prompt = f"{system_prompt}\n\n{user_message}" | |
| response = self.client.generate_content( | |
| full_prompt, | |
| generation_config=genai.GenerationConfig( | |
| temperature=temperature, | |
| max_output_tokens=max_tokens, | |
| top_p=0.95 | |
| ) | |
| ) | |
| return response.text | |
| def is_ready(self) -> bool: | |
| """Check if LLM is ready to generate.""" | |
| return self.ready | |
| def get_provider(self) -> str: | |
| """Get current provider name.""" | |
| if self.provider == "groq": | |
| return "Groq (Llama-3.3-70B)" | |
| elif self.provider == "gemini": | |
| return "Google Gemini 2.5 Flash" | |
| return "Unknown" | |
| def generate(self, prompt: str, temperature: float = 0.3, max_tokens: int = 1500) -> str: | |
| """ | |
| Simple wrapper for backend compatibility. | |
| Generates response from a complete prompt that already includes context. | |
| Args: | |
| prompt: Complete prompt with context already embedded | |
| temperature: LLM temperature (0.0-1.0) | |
| max_tokens: Maximum response length | |
| Returns: | |
| Generated response | |
| """ | |
| if not self.ready: | |
| return ( | |
| "⚠️ **LLM not configured.** Please add your API key.\n\n" | |
| "Get a free key:\n" | |
| "- **Groq** (recommended, very fast): https://console.groq.com/keys\n" | |
| "- **Gemini** (Google): https://makersuite.google.com/app/apikey" | |
| ) | |
| try: | |
| if self.provider == "groq": | |
| return self._generate_groq( | |
| system_prompt="You are a helpful AI assistant.", | |
| user_message=prompt, | |
| temperature=temperature, | |
| max_tokens=max_tokens | |
| ) | |
| elif self.provider == "gemini": | |
| return self._generate_gemini( | |
| system_prompt="You are a helpful AI assistant.", | |
| user_message=prompt, | |
| temperature=temperature, | |
| max_tokens=max_tokens | |
| ) | |
| except Exception as e: | |
| return f"Error generating response: {str(e)}" | |