VectorMind / backend /rag /generator.py
Ash-211's picture
Send actual images to Gemini for multimodal RAG
a529aa1
Raw
History Blame Contribute Delete
5.62 kB
import os
from dotenv import load_dotenv
import google.genai as genai
from PIL import Image
load_dotenv()
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
# --- Groq Fallback ---
_groq_client = None
def _get_groq_client():
global _groq_client
if _groq_client is None:
from groq import Groq
_groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
return _groq_client
GROQ_MODEL = "llama-3.3-70b-versatile"
def _build_prompt(query, text_contexts, image_contexts):
"""Build the text-only prompt (shared by Gemini and Groq)."""
text_block = "\n\n".join(
[f"[Text Source {i+1}]\n{ctx['content']}"
for i, ctx in enumerate(text_contexts)]
)
return f"""You are VectorMind, a smart and conversational AI assistant. The user has uploaded documents, and the relevant excerpts are provided below as context.
Your job is to be genuinely helpful. You should:
- Answer questions using the provided context as your primary source of truth
- Analyze, interpret, summarize, compare, evaluate, or give opinions about the content when asked
- Be conversational and natural — not robotic or overly cautious
- If the user asks you to do something with the document content (evaluate, critique, improve, etc.), do your best using what you have
- Only say you don't have enough information if the context truly has nothing relevant
- Cite your sources naturally, like (Text Source 1) or (Image Source 2)
TEXT CONTEXT:
{text_block}
QUESTION:
{query}
"""
def _load_images(image_contexts):
images = []
for i, ctx in enumerate(image_contexts):
path = ctx.get("image_path", "")
if os.path.isfile(path):
try:
img = Image.open(path)
images.append((f"[Image Source {i+1}] from page {ctx.get('page', '?')}", img))
except Exception:
pass
return images
def generate_title(user_message):
"""Generate a short title for a conversation based on the first user message."""
prompt = f"Generate a concise 3-6 word title for a conversation that starts with this message. Return ONLY the title, no quotes or punctuation.\n\nMessage: {user_message}\n\nTitle:"
try:
response = client.models.generate_content(model="gemini-2.5-flash", contents=prompt)
return response.text.strip().strip('"').strip("'")[:50]
except Exception:
# Fallback to Groq for title generation
try:
groq = _get_groq_client()
response = groq.chat.completions.create(
model=GROQ_MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=20,
)
return response.choices[0].message.content.strip().strip('"').strip("'")[:50]
except Exception:
return "New Chat"
def decompose_query(user_query):
prompt = f"""
Break the following user question into smaller, independent search queries.
Question:
{user_query}
Return 2–4 focused sub-queries related to the input documents, if the query can be broken down, else return a singular query.
Do NOT answer the question.
Only return the search queries.
"""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt
)
return [q.strip("- ").strip() for q in response.text.split("\n") if q.strip()]
def generate_answer(query, text_contexts, image_contexts):
prompt = _build_prompt(query, text_contexts, image_contexts)
contents = [prompt]
loaded_images = _load_images(image_contexts)
for label, img in loaded_images:
contents.append(img)
contents.append(label)
try:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=contents
)
return response.text
except Exception as e:
if _is_rate_limit(e):
return _groq_generate(prompt)
raise
def generate_answer_stream(query, text_contexts, image_contexts):
"""Streaming version — tries Gemini first, falls back to Groq on rate limit."""
prompt = _build_prompt(query, text_contexts, image_contexts)
contents = [prompt]
loaded_images = _load_images(image_contexts)
for label, img in loaded_images:
contents.append(img)
contents.append(label)
try:
response = client.models.generate_content_stream(
model="gemini-2.5-flash",
contents=contents,
)
for chunk in response:
if chunk.text:
yield chunk.text
except Exception as e:
if _is_rate_limit(e):
yield from _groq_generate_stream(prompt)
else:
raise
def _is_rate_limit(error):
"""Check if the error is a rate limit / quota error."""
error_str = str(error).lower()
return any(kw in error_str for kw in ["429", "quota", "rate limit", "resource exhausted"])
def _groq_generate(prompt):
"""Non-streaming Groq fallback."""
groq = _get_groq_client()
response = groq.chat.completions.create(
model=GROQ_MODEL,
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
def _groq_generate_stream(prompt):
"""Streaming Groq fallback."""
groq = _get_groq_client()
stream = groq.chat.completions.create(
model=GROQ_MODEL,
messages=[{"role": "user", "content": prompt}],
stream=True,
)
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
yield content