File size: 5,615 Bytes
2db8ee1
 
 
a529aa1
2db8ee1
 
 
7af9025
e296091
 
7af9025
e296091
 
 
 
 
 
7af9025
1d2f776
2db8ee1
 
a529aa1
e296091
a529aa1
2db8ee1
 
 
 
e296091
08292d0
 
 
 
 
 
 
2db8ee1
 
 
 
 
e296091
a529aa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e296091
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2db8ee1
e296091
2db8ee1
 
d8c0ee6
e296091
d8c0ee6
e296091
 
d8c0ee6
a529aa1
 
 
 
 
 
e296091
 
 
a529aa1
e296091
 
 
 
 
 
08292d0
d8c0ee6
e296091
 
 
d8c0ee6
a529aa1
 
 
 
 
 
e296091
 
 
a529aa1
e296091
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8c0ee6
e296091
 
 
 
 
 
 
 
d8c0ee6
e296091
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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