Amrita P commited on
Commit
4e69779
·
1 Parent(s): 56f3ac6

change to grok

Browse files
.env.example CHANGED
@@ -1 +1 @@
1
- GEMINI_API_KEY=your_key_here
 
1
+ GROQ_API_KEY=your_key_here
app_gradio.py CHANGED
@@ -98,7 +98,6 @@ def answer_question(question: str, history: list[dict], expand_query: bool):
98
  index=index,
99
  k=5,
100
  expand_query=expand_query,
101
- generator=generator,
102
  )
103
  results = search_resp.chunks
104
 
 
98
  index=index,
99
  k=5,
100
  expand_query=expand_query,
 
101
  )
102
  results = search_resp.chunks
103
 
evaluation/judge.py CHANGED
@@ -1,18 +1,18 @@
1
- """LLM-as-judge scoring via Gemini: faithfulness and answer relevance."""
2
 
3
  import logging
4
  import os
5
  import re
6
  import time
7
 
8
- import google.generativeai as genai
9
  from dotenv import load_dotenv
10
 
11
  load_dotenv()
12
  logger = logging.getLogger(__name__)
13
 
14
- _JUDGE_MODEL = "gemini-flash-latest"
15
- _ENV_KEY = "GEMINI_API_KEY"
16
 
17
  _FAITHFULNESS_PROMPT = """\
18
  You are a grader assessing whether a RAG system's answer is supported by the context.
@@ -56,31 +56,32 @@ The answer addresses the main question but omits one important detail.\
56
 
57
 
58
  class LLMJudge:
59
- """Thin Gemini wrapper for judge scoring with built-in rate limiting."""
60
 
61
  def __init__(self, rate_limit_delay: float = 1.0) -> None:
62
  api_key = os.getenv(_ENV_KEY)
63
  if not api_key:
64
  raise EnvironmentError(
65
- f"Gemini API key not found. Set {_ENV_KEY} or add it to .env."
66
  )
67
- genai.configure(api_key=api_key)
68
- self._model = genai.GenerativeModel(
69
- _JUDGE_MODEL,
70
- generation_config=genai.GenerationConfig(temperature=0.0, max_output_tokens=128),
71
- )
72
  self._delay = rate_limit_delay
73
  self._last_call_at: float = 0.0
74
 
75
  def score(self, prompt: str) -> tuple[float, str]:
76
- """Call Gemini and return (score 0–1, explanation). Returns (0.0, error) on failure."""
77
  wait = self._delay - (time.monotonic() - self._last_call_at)
78
  if wait > 0:
79
  time.sleep(wait)
80
  try:
81
- response = self._model.generate_content(prompt)
 
 
 
 
 
82
  self._last_call_at = time.monotonic()
83
- return _parse_response(response.text)
84
  except Exception as exc:
85
  self._last_call_at = time.monotonic()
86
  logger.warning("Judge call failed: %s", exc)
 
1
+ """LLM-as-judge scoring via Groq: faithfulness and answer relevance."""
2
 
3
  import logging
4
  import os
5
  import re
6
  import time
7
 
8
+ from groq import Groq
9
  from dotenv import load_dotenv
10
 
11
  load_dotenv()
12
  logger = logging.getLogger(__name__)
13
 
14
+ _JUDGE_MODEL = "llama-3.3-70b-versatile"
15
+ _ENV_KEY = "GROQ_API_KEY"
16
 
17
  _FAITHFULNESS_PROMPT = """\
18
  You are a grader assessing whether a RAG system's answer is supported by the context.
 
56
 
57
 
58
  class LLMJudge:
59
+ """Thin Groq wrapper for judge scoring with built-in rate limiting."""
60
 
61
  def __init__(self, rate_limit_delay: float = 1.0) -> None:
62
  api_key = os.getenv(_ENV_KEY)
63
  if not api_key:
64
  raise EnvironmentError(
65
+ f"Groq API key not found. Set {_ENV_KEY} or add it to .env."
66
  )
67
+ self._client = Groq(api_key=api_key)
 
 
 
 
68
  self._delay = rate_limit_delay
69
  self._last_call_at: float = 0.0
70
 
71
  def score(self, prompt: str) -> tuple[float, str]:
72
+ """Call Groq and return (score 0–1, explanation). Returns (0.0, error) on failure."""
73
  wait = self._delay - (time.monotonic() - self._last_call_at)
74
  if wait > 0:
75
  time.sleep(wait)
76
  try:
77
+ response = self._client.chat.completions.create(
78
+ model=_JUDGE_MODEL,
79
+ messages=[{"role": "user", "content": prompt}],
80
+ temperature=0.0,
81
+ max_tokens=128,
82
+ )
83
  self._last_call_at = time.monotonic()
84
+ return _parse_response(response.choices[0].message.content)
85
  except Exception as exc:
86
  self._last_call_at = time.monotonic()
87
  logger.warning("Judge call failed: %s", exc)
generation/generator.py CHANGED
@@ -3,7 +3,7 @@ import logging
3
  from dataclasses import dataclass, field
4
  from typing import Iterator
5
 
6
- import google.generativeai as genai
7
  from dotenv import load_dotenv
8
 
9
  from retrieval.index import SearchResult
@@ -12,8 +12,8 @@ load_dotenv()
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- _MODEL_NAME = "gemini-flash-latest"
16
- _ENV_KEY = "GEMINI_API_KEY"
17
 
18
  _SYSTEM_PROMPT = """\
19
  You are a precise document Q&A assistant. Follow these rules without exception:
@@ -45,11 +45,11 @@ class Answer:
45
 
46
 
47
  class Generator:
48
- """Wraps Gemini 1.5 Flash for grounded, citation-aware answer generation.
49
 
50
- The API key is read from the GEMINI_API_KEY environment variable (or a
51
  .env file in the working directory). A low temperature keeps answers
52
- factual; max_output_tokens caps runaway responses.
53
  """
54
 
55
  def __init__(
@@ -61,22 +61,14 @@ class Generator:
61
  api_key = os.getenv(_ENV_KEY)
62
  if not api_key:
63
  raise EnvironmentError(
64
- f"Gemini API key not found. Set the {_ENV_KEY} environment variable "
65
  "or add it to a .env file."
66
  )
67
 
68
  self._low_threshold = low_confidence_threshold
69
  self._medium_threshold = medium_confidence_threshold
70
-
71
- genai.configure(api_key=api_key)
72
- self._model = genai.GenerativeModel(
73
- model_name,
74
- system_instruction=_SYSTEM_PROMPT,
75
- generation_config=genai.GenerationConfig(
76
- temperature=0.1,
77
- max_output_tokens=2048,
78
- ),
79
- )
80
  logger.info("Generator initialised with model: %s", model_name)
81
 
82
  def _confidence_level(self, max_score: float) -> str:
@@ -92,7 +84,7 @@ class Generator:
92
  results: list[SearchResult],
93
  max_score: float = 1.0,
94
  ) -> Answer:
95
- """Build a grounded prompt from retrieved chunks and call Gemini.
96
 
97
  Args:
98
  question: The user's question string.
@@ -100,11 +92,11 @@ class Generator:
100
  Each result's metadata must contain 'text', 'source',
101
  and 'page_num' keys (set by IngestionPipeline).
102
  max_score: Highest retrieval score from the search step, used to
103
- gate the Gemini call and set confidence metadata.
104
 
105
  Returns:
106
  Answer with generated text, source metadata, and confidence fields.
107
- When max_score is below the low threshold, Gemini is not called.
108
  """
109
  if not results:
110
  return Answer(
@@ -118,7 +110,7 @@ class Generator:
118
  level = self._confidence_level(max_score)
119
 
120
  if level == "low":
121
- logger.debug("Skipping Gemini call: max_score=%.4f below low threshold %.2f", max_score, self._low_threshold)
122
  return Answer(
123
  question=question,
124
  answer=_LOW_CONFIDENCE_MESSAGE,
@@ -134,9 +126,17 @@ class Generator:
134
  "ANSWER:"
135
  )
136
 
137
- logger.debug("Sending prompt to Gemini (%d chars)", len(prompt))
138
- response = self._model.generate_content(prompt)
139
- answer_text = response.text.strip()
 
 
 
 
 
 
 
 
140
  if level == "medium":
141
  answer_text += _MEDIUM_CONFIDENCE_NOTE
142
 
@@ -154,19 +154,19 @@ class Generator:
154
  results: list[SearchResult],
155
  max_score: float = 1.0,
156
  ) -> Iterator[str]:
157
- """Stream the answer token-by-token using Gemini's streaming API.
158
 
159
  Args:
160
  question: The user's question string.
161
  results: Ranked SearchResult list (same as generate_answer).
162
- max_score: Highest retrieval score; gates the Gemini call and
163
  appends a warning note for medium-confidence answers.
164
 
165
  Yields:
166
- Text chunks as they arrive from Gemini.
167
 
168
  Raises:
169
- Exception: Re-raises any Gemini API error so the caller can handle it.
170
  """
171
  if not results:
172
  yield "No relevant context was retrieved to answer this question."
@@ -175,7 +175,7 @@ class Generator:
175
  level = self._confidence_level(max_score)
176
 
177
  if level == "low":
178
- logger.debug("Skipping Gemini call: max_score=%.4f below low threshold %.2f", max_score, self._low_threshold)
179
  yield _LOW_CONFIDENCE_MESSAGE
180
  return
181
 
@@ -186,11 +186,20 @@ class Generator:
186
  "ANSWER:"
187
  )
188
 
189
- logger.debug("Sending streaming prompt to Gemini (%d chars)", len(prompt))
190
- response = self._model.generate_content(prompt, stream=True)
191
- for chunk in response:
192
- if chunk.text:
193
- yield chunk.text
 
 
 
 
 
 
 
 
 
194
 
195
  if level == "medium":
196
  yield _MEDIUM_CONFIDENCE_NOTE
 
3
  from dataclasses import dataclass, field
4
  from typing import Iterator
5
 
6
+ from groq import Groq
7
  from dotenv import load_dotenv
8
 
9
  from retrieval.index import SearchResult
 
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ _MODEL_NAME = "llama-3.3-70b-versatile"
16
+ _ENV_KEY = "GROQ_API_KEY"
17
 
18
  _SYSTEM_PROMPT = """\
19
  You are a precise document Q&A assistant. Follow these rules without exception:
 
45
 
46
 
47
  class Generator:
48
+ """Wraps Groq (llama-3.3-70b-versatile) for grounded, citation-aware answer generation.
49
 
50
+ The API key is read from the GROQ_API_KEY environment variable (or a
51
  .env file in the working directory). A low temperature keeps answers
52
+ factual; max_tokens caps runaway responses.
53
  """
54
 
55
  def __init__(
 
61
  api_key = os.getenv(_ENV_KEY)
62
  if not api_key:
63
  raise EnvironmentError(
64
+ f"Groq API key not found. Set the {_ENV_KEY} environment variable "
65
  "or add it to a .env file."
66
  )
67
 
68
  self._low_threshold = low_confidence_threshold
69
  self._medium_threshold = medium_confidence_threshold
70
+ self._model_name = model_name
71
+ self._client = Groq(api_key=api_key)
 
 
 
 
 
 
 
 
72
  logger.info("Generator initialised with model: %s", model_name)
73
 
74
  def _confidence_level(self, max_score: float) -> str:
 
84
  results: list[SearchResult],
85
  max_score: float = 1.0,
86
  ) -> Answer:
87
+ """Build a grounded prompt from retrieved chunks and call Groq.
88
 
89
  Args:
90
  question: The user's question string.
 
92
  Each result's metadata must contain 'text', 'source',
93
  and 'page_num' keys (set by IngestionPipeline).
94
  max_score: Highest retrieval score from the search step, used to
95
+ gate the API call and set confidence metadata.
96
 
97
  Returns:
98
  Answer with generated text, source metadata, and confidence fields.
99
+ When max_score is below the low threshold, the LLM is not called.
100
  """
101
  if not results:
102
  return Answer(
 
110
  level = self._confidence_level(max_score)
111
 
112
  if level == "low":
113
+ logger.debug("Skipping LLM call: max_score=%.4f below low threshold %.2f", max_score, self._low_threshold)
114
  return Answer(
115
  question=question,
116
  answer=_LOW_CONFIDENCE_MESSAGE,
 
126
  "ANSWER:"
127
  )
128
 
129
+ logger.debug("Sending prompt to Groq (%d chars)", len(prompt))
130
+ response = self._client.chat.completions.create(
131
+ model=self._model_name,
132
+ messages=[
133
+ {"role": "system", "content": _SYSTEM_PROMPT},
134
+ {"role": "user", "content": prompt},
135
+ ],
136
+ temperature=0.1,
137
+ max_tokens=2048,
138
+ )
139
+ answer_text = response.choices[0].message.content.strip()
140
  if level == "medium":
141
  answer_text += _MEDIUM_CONFIDENCE_NOTE
142
 
 
154
  results: list[SearchResult],
155
  max_score: float = 1.0,
156
  ) -> Iterator[str]:
157
+ """Stream the answer token-by-token using Groq's streaming API.
158
 
159
  Args:
160
  question: The user's question string.
161
  results: Ranked SearchResult list (same as generate_answer).
162
+ max_score: Highest retrieval score; gates the API call and
163
  appends a warning note for medium-confidence answers.
164
 
165
  Yields:
166
+ Text chunks as they arrive from Groq.
167
 
168
  Raises:
169
+ Exception: Re-raises any Groq API error so the caller can handle it.
170
  """
171
  if not results:
172
  yield "No relevant context was retrieved to answer this question."
 
175
  level = self._confidence_level(max_score)
176
 
177
  if level == "low":
178
+ logger.debug("Skipping LLM call: max_score=%.4f below low threshold %.2f", max_score, self._low_threshold)
179
  yield _LOW_CONFIDENCE_MESSAGE
180
  return
181
 
 
186
  "ANSWER:"
187
  )
188
 
189
+ logger.debug("Sending streaming prompt to Groq (%d chars)", len(prompt))
190
+ stream = self._client.chat.completions.create(
191
+ model=self._model_name,
192
+ messages=[
193
+ {"role": "system", "content": _SYSTEM_PROMPT},
194
+ {"role": "user", "content": prompt},
195
+ ],
196
+ temperature=0.1,
197
+ max_tokens=2048,
198
+ stream=True,
199
+ )
200
+ for chunk in stream:
201
+ if chunk.choices[0].delta.content:
202
+ yield chunk.choices[0].delta.content
203
 
204
  if level == "medium":
205
  yield _MEDIUM_CONFIDENCE_NOTE
requirements.txt CHANGED
@@ -16,8 +16,8 @@ sentence-transformers==3.3.1
16
  # Vector index
17
  faiss-cpu==1.9.0
18
 
19
- # LLM (Gemini)
20
- google-generativeai==0.8.3
21
 
22
  # Utilities
23
  numpy==1.26.4
 
16
  # Vector index
17
  faiss-cpu==1.9.0
18
 
19
+ # LLM (Groq)
20
+ groq
21
 
22
  # Utilities
23
  numpy==1.26.4
retrieval/searcher.py CHANGED
@@ -1,21 +1,17 @@
1
  from __future__ import annotations
2
 
3
  import logging
 
4
  import re
5
- from typing import TYPE_CHECKING
6
-
7
- import google.generativeai as genai
8
-
9
  from dataclasses import dataclass
10
 
 
 
11
  from ingestion.embedder import Embedder
12
  from retrieval.index import VectorIndex, SearchResult
13
  from retrieval.bm25_index import BM25Index
14
  from retrieval.reranker import Reranker
15
 
16
- if TYPE_CHECKING:
17
- from generation.generator import Generator
18
-
19
  logger = logging.getLogger(__name__)
20
 
21
 
@@ -27,6 +23,8 @@ class SearchResponse:
27
 
28
  _reranker = Reranker()
29
 
 
 
30
  _EXPANSION_PROMPT = (
31
  "Generate 3 alternative phrasings of this question that might match "
32
  "different parts of a document. Return only the rephrased questions, "
@@ -45,7 +43,6 @@ def search(
45
  bm25_index: BM25Index | None = None,
46
  alpha: float = 0.7,
47
  expand_query: bool = False,
48
- generator: Generator | None = None,
49
  use_reranking: bool = True,
50
  expansion_threshold: float = 0.45,
51
  ) -> SearchResponse:
@@ -101,29 +98,26 @@ def search(
101
  results = _single_search(query, embedder, index, bm25_index, candidates, alpha)
102
 
103
  if expand_query:
104
- if generator is None:
105
- logger.warning("expand_query=True but no generator provided — skipping expansion.")
 
 
 
 
106
  else:
107
- top_score = results[0].score if results else 0.0
108
- if top_score >= expansion_threshold:
109
- logger.debug(
110
- "Expansion skipped: top_score=%.4f >= threshold=%.4f",
111
- top_score, expansion_threshold,
112
- )
113
- else:
114
- logger.debug(
115
- "Expansion triggered: top_score=%.4f < threshold=%.4f",
116
- top_score, expansion_threshold,
117
- )
118
- expansions = _expand_query(query, generator)
119
- if expansions:
120
- logger.debug("Query expanded to %d variants: %s", len(expansions) + 1, [query] + expansions)
121
- expanded_results: list[list[SearchResult]] = [results] + [
122
- _single_search(q, embedder, index, bm25_index, candidates, alpha)
123
- for q in expansions
124
- ]
125
- results = _merge_by_best_score(expanded_results, candidates)
126
- expansion_used = True
127
 
128
  # Capture cosine similarity score before reranker overwrites scores with
129
  # cross-encoder values (range -10..+10), which would break confidence gating.
@@ -141,22 +135,26 @@ def search(
141
  # Query expansion
142
  # ---------------------------------------------------------------------------
143
 
144
- def _expand_query(query: str, generator: Generator) -> list[str]:
145
- """Ask Gemini for 3 alternative phrasings of the query.
146
 
147
- Uses a fresh GenerativeModel without the Q&A system instruction so the
148
- model isn't constrained to answer from context. Falls back to an empty
149
- list on any API error so the caller always gets at least the original query.
150
  """
 
 
 
 
151
  try:
152
- # Reuse the same underlying model name but without system_instruction.
153
- expansion_model = genai.GenerativeModel(
154
- generator._model.model_name,
155
- generation_config=genai.GenerationConfig(temperature=0.7, max_output_tokens=256),
156
- )
157
  prompt = _EXPANSION_PROMPT.format(question=query)
158
- response = expansion_model.generate_content(prompt)
159
- return _parse_expansion_lines(response.text)
 
 
 
 
 
160
  except Exception as exc:
161
  logger.warning("Query expansion failed (%s) — using original query only.", exc)
162
  return []
 
1
  from __future__ import annotations
2
 
3
  import logging
4
+ import os
5
  import re
 
 
 
 
6
  from dataclasses import dataclass
7
 
8
+ from groq import Groq
9
+
10
  from ingestion.embedder import Embedder
11
  from retrieval.index import VectorIndex, SearchResult
12
  from retrieval.bm25_index import BM25Index
13
  from retrieval.reranker import Reranker
14
 
 
 
 
15
  logger = logging.getLogger(__name__)
16
 
17
 
 
23
 
24
  _reranker = Reranker()
25
 
26
+ _GROQ_EXPANSION_MODEL = "llama-3.3-70b-versatile"
27
+
28
  _EXPANSION_PROMPT = (
29
  "Generate 3 alternative phrasings of this question that might match "
30
  "different parts of a document. Return only the rephrased questions, "
 
43
  bm25_index: BM25Index | None = None,
44
  alpha: float = 0.7,
45
  expand_query: bool = False,
 
46
  use_reranking: bool = True,
47
  expansion_threshold: float = 0.45,
48
  ) -> SearchResponse:
 
98
  results = _single_search(query, embedder, index, bm25_index, candidates, alpha)
99
 
100
  if expand_query:
101
+ top_score = results[0].score if results else 0.0
102
+ if top_score >= expansion_threshold:
103
+ logger.debug(
104
+ "Expansion skipped: top_score=%.4f >= threshold=%.4f",
105
+ top_score, expansion_threshold,
106
+ )
107
  else:
108
+ logger.debug(
109
+ "Expansion triggered: top_score=%.4f < threshold=%.4f",
110
+ top_score, expansion_threshold,
111
+ )
112
+ expansions = _expand_query(query)
113
+ if expansions:
114
+ logger.debug("Query expanded to %d variants: %s", len(expansions) + 1, [query] + expansions)
115
+ expanded_results: list[list[SearchResult]] = [results] + [
116
+ _single_search(q, embedder, index, bm25_index, candidates, alpha)
117
+ for q in expansions
118
+ ]
119
+ results = _merge_by_best_score(expanded_results, candidates)
120
+ expansion_used = True
 
 
 
 
 
 
 
121
 
122
  # Capture cosine similarity score before reranker overwrites scores with
123
  # cross-encoder values (range -10..+10), which would break confidence gating.
 
135
  # Query expansion
136
  # ---------------------------------------------------------------------------
137
 
138
+ def _expand_query(query: str) -> list[str]:
139
+ """Ask Groq for 3 alternative phrasings of the query.
140
 
141
+ Falls back to an empty list on any API error so the caller always gets
142
+ at least the original query.
 
143
  """
144
+ api_key = os.getenv("GROQ_API_KEY")
145
+ if not api_key:
146
+ logger.warning("GROQ_API_KEY not set — skipping query expansion.")
147
+ return []
148
  try:
149
+ client = Groq(api_key=api_key)
 
 
 
 
150
  prompt = _EXPANSION_PROMPT.format(question=query)
151
+ response = client.chat.completions.create(
152
+ model=_GROQ_EXPANSION_MODEL,
153
+ messages=[{"role": "user", "content": prompt}],
154
+ temperature=0.7,
155
+ max_tokens=256,
156
+ )
157
+ return _parse_expansion_lines(response.choices[0].message.content)
158
  except Exception as exc:
159
  logger.warning("Query expansion failed (%s) — using original query only.", exc)
160
  return []