notebook-backend / utils /llm_generator.py
mohhhhhit's picture
Update utils/llm_generator.py
2969175 verified
"""
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)}"