testtest123 commited on
Commit
9748ae9
·
1 Parent(s): 73905db

Refactor LLM architecture to SOLID principles, add Gemini Judge, and self-reflective RAGAS logic

Browse files
RAG_FULL_APPLICATION_BACKEND/app/config.py CHANGED
@@ -16,12 +16,9 @@ class Settings(BaseSettings):
16
  EMBED_TIMEOUT: int = 60
17
  EMBED_MAX_RETRIES: int = 3
18
 
19
- # LLM — Qwen3 & GLM-4.7-Flash
20
- QWEN3_MODEL_NAME: str = "Qwen/Qwen3-Demo"
21
- QWEN3_THINKING_BUDGET: int = 38
22
- GLM_4_7_API_KEY: str = ""
23
- GLM_BASE_URL: str = "https://api.z.ai/api/paas/v4/"
24
- GLM_MODEL_NAME: str = "glm-4.7-Flash"
25
  LLM_RESPONSE_TIMEOUT: int = 1080
26
  MAX_LLM_RETRIES: int = 3
27
  MAX_TIMEOUT_RETRIES: int = 10
 
16
  EMBED_TIMEOUT: int = 60
17
  EMBED_MAX_RETRIES: int = 3
18
 
19
+ # LLM — Tencent Hy3, Gemini & Qwen Omni
20
+ GEMINI_API_KEY: str = ""
21
+ GEMINI_MODEL_NAME: str = "gemini-3.1-flash-lite"
 
 
 
22
  LLM_RESPONSE_TIMEOUT: int = 1080
23
  MAX_LLM_RETRIES: int = 3
24
  MAX_TIMEOUT_RETRIES: int = 10
RAG_FULL_APPLICATION_BACKEND/app/services/llm_service.py CHANGED
@@ -1,237 +1,287 @@
1
  import os
2
  import threading
3
- import time
4
  import logging
5
- import re
6
  import html
7
- from gradio_client import Client
8
- from openai import OpenAI
 
9
  from ..config import settings
10
  from ..utils.json_utils import extract_json_block, repair_json
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
- class GLM47Service:
15
- def __init__(self):
16
- self.api_key = getattr(settings, "GLM_4_7_API_KEY", "") or os.getenv("GLM_4_7_API_KEY", "")
17
- self.base_url = getattr(settings, "GLM_BASE_URL", "https://api.z.ai/api/paas/v4/")
18
- self.model_name = getattr(settings, "GLM_MODEL_NAME", "glm-4.7-Flash")
19
- self._client = None
 
 
 
 
20
 
21
- @property
22
- def client(self):
23
- if not self._client:
24
- self._client = OpenAI(
25
- api_key=self.api_key,
26
- base_url=self.base_url
27
- )
28
- return self._client
29
 
30
- def _call_api(self, prompt: str, sys_prompt: str, result_box: list, error_box: list):
 
31
  try:
32
- default_sys = (
33
- "You are a highly capable AI assistant. Provide accurate, concise, and fact-based responses. "
34
- "Always format your responses in valid JSON when requested."
35
- )
36
- extra_body = {
37
- "thinking": {
38
- "type": "enabled",
39
- },
40
- }
41
- completion = self.client.chat.completions.create(
42
- model=self.model_name,
43
- messages=[
44
- {"role": "system", "content": sys_prompt or default_sys},
45
- {"role": "user", "content": prompt}
46
- ],
47
- stream=False,
48
- extra_body=extra_body
49
  )
50
- raw = completion.choices[0].message.content
51
- result_box[0] = raw
52
  except Exception as e:
53
  error_box[0] = e
 
 
 
 
 
 
54
 
55
  def generate(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> str:
56
- """
57
- Generate text response from GLM-4.7-Flash with 3 retries.
58
- """
59
- max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
60
- if retry_count >= max_retries:
61
- raise RuntimeError(f"Max GLM-4.7-Flash retries ({max_retries}) exceeded")
62
 
63
  rb, eb = [None], [None]
64
- t = threading.Thread(target=self._call_api, args=(prompt, sys_prompt, rb, eb), daemon=True)
65
  t.start()
66
- t.join(timeout=settings.LLM_RESPONSE_TIMEOUT)
67
 
68
  if t.is_alive():
69
- logger.warning(f"GLM-4.7-Flash timeout. Attempt {retry_count + 1}/{max_retries}")
70
  return self.generate(prompt, sys_prompt, retry_count + 1)
71
 
72
  if eb[0]:
73
- logger.error(f"GLM-4.7-Flash error: {eb[0]}. Attempt {retry_count + 1}/{max_retries}")
74
  time.sleep(2)
75
  return self.generate(prompt, sys_prompt, retry_count + 1)
76
 
77
  if rb[0] is None:
78
  return self.generate(prompt, sys_prompt, retry_count + 1)
79
 
80
- raw_text = rb[0].strip()
 
 
 
 
 
 
 
 
 
 
 
 
81
  json_str = extract_json_block(raw_text)
82
  data = repair_json(json_str)
83
- if data and isinstance(data, dict) and 'answer' in data:
84
- return str(data['answer']).strip()
85
- return raw_text
 
 
 
 
 
86
 
87
- def evaluate_json(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> dict:
88
- """
89
- Generate structured JSON response (used for RAGAS evaluation) with 3 retries.
90
- """
91
- max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
92
- if retry_count >= max_retries:
93
- raise RuntimeError(f"Max GLM-4.7-Flash evaluation retries ({max_retries}) exceeded")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  rb, eb = [None], [None]
96
- t = threading.Thread(target=self._call_api, args=(prompt, sys_prompt, rb, eb), daemon=True)
97
- t.start()
98
- t.join(timeout=settings.LLM_RESPONSE_TIMEOUT)
99
 
100
- if t.is_alive() or eb[0] or rb[0] is None:
101
- logger.warning(f"GLM-4.7-Flash JSON evaluation attempt {retry_count + 1} failed or timed out: {eb[0]}")
102
  time.sleep(2)
103
- return self.evaluate_json(prompt, sys_prompt, retry_count + 1)
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- raw_text = rb[0]
 
106
  json_str = extract_json_block(raw_text)
107
- parsed = repair_json(json_str)
108
- if parsed and isinstance(parsed, dict):
109
- return parsed
110
-
111
- # Fallback to direct json.loads
112
  try:
113
  return json.loads(json_str)
114
- except Exception as pe:
115
- logger.error(f"Failed to parse GLM evaluation JSON: {pe}")
116
- return self.evaluate_json(prompt, sys_prompt, retry_count + 1)
 
117
 
118
- class Qwen3Service:
119
  def __init__(self):
120
- self.model_name = getattr(settings, "QWEN3_MODEL_NAME", "zai-org/GLM-4.5-Space")
121
- self._client = None
 
122
 
123
- @property
124
- def client(self):
125
- if not self._client:
126
- self._client = Client(self.model_name)
127
- return self._client
128
-
129
- def _call(self, prompt: str, result_box: list, error_box: list):
130
  try:
131
- try:
132
- self.client.predict(api_name="/reset")
133
- except:
134
- pass
135
-
136
- sys_prompt = (
137
- "You are a highly capable RAG assistant. "
138
- "Provide accurate, concise, and fact-based responses. "
139
- "ALWAYS wrap your response in a JSON block with the following keys:\n"
140
- "{\n"
141
- " \"thinking\": \"Your internal reasoning process\",\n"
142
- " \"answer\": \"Your final formatted answer in markdown\"\n"
143
- "}\n"
144
- "Keep the 'thinking' brief and the 'answer' detailed."
145
- )
146
-
147
- result = self.client.predict(
148
- msg=prompt,
149
- sys_prompt=sys_prompt,
150
- thinking_enabled=True,
151
- temperature=0.1,
152
- api_name="/chat_wrapper_1"
153
  )
154
  result_box[0] = result
155
  except Exception as e:
156
  error_box[0] = e
 
 
 
 
 
 
157
 
158
- def generate(self, prompt: str, retry_count: int = 0) -> str:
159
- """
160
- Generate response from Qwen with 3 retries max.
161
- """
162
- max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
163
- if retry_count >= max_retries:
164
- raise RuntimeError(f"Max Qwen LLM retries ({max_retries}) exceeded")
165
 
166
  rb, eb = [None], [None]
167
- t = threading.Thread(target=self._call, args=(prompt, rb, eb), daemon=True)
168
  t.start()
169
- t.join(timeout=settings.LLM_RESPONSE_TIMEOUT)
170
 
171
  if t.is_alive():
172
- logger.warning(f"Qwen timeout. Attempt {retry_count + 1}/{max_retries}")
173
- return self.generate(prompt, retry_count + 1)
174
 
175
  if eb[0]:
176
- logger.error(f"Qwen error: {eb[0]}. Attempt {retry_count + 1}/{max_retries}")
177
  time.sleep(2)
178
- return self.generate(prompt, retry_count + 1)
179
 
180
  if rb[0] is None:
181
- return self.generate(prompt, retry_count + 1)
182
 
183
  try:
184
  res = rb[0]
185
- raw_text = ""
186
  if isinstance(res, (list, tuple)) and len(res) > 0:
187
- turn = res[0]
188
- if isinstance(turn, (list, tuple)) and len(turn) > 1:
189
- content_dict = turn[1]
190
- if isinstance(content_dict, dict) and 'content' in content_dict:
191
- raw_text = content_dict['content']
192
-
193
- if not raw_text:
194
- raw_text = str(res)
195
-
196
- json_str = extract_json_block(raw_text)
197
- data = repair_json(json_str)
198
-
199
- if data and isinstance(data, dict) and 'answer' in data:
200
- return str(data['answer']).strip()
201
-
202
- if raw_text:
203
- return raw_text.strip()
204
-
205
- return self.generate(prompt, retry_count + 1)
206
  except Exception as e:
207
- logger.error(f"Parse error for Qwen: {e}")
208
  return str(rb[0])
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  class LLMServiceDispatcher:
211
  def __init__(self):
212
- self.primary = Qwen3Service()
213
- self.backup = GLM47Service()
 
214
 
215
  @property
216
- def judge(self) -> GLM47Service:
217
- """GLM-4.7-Flash as RAGAS Judge"""
218
- return self.backup
219
 
220
- def generate(self, prompt: str) -> str:
221
  """
222
- Generates text using Primary (Qwen with 3 retries).
223
- Falls back to Backup (GLM-4.7-Flash with 3 retries) if primary fails.
 
224
  """
225
  try:
226
- logger.info("Attempting generation with Primary LLM (Qwen)...")
227
- return self.primary.generate(prompt)
228
  except Exception as e:
229
- logger.warning(f"Primary LLM failed: {e}. Falling back to Backup LLM (GLM-4.7-Flash)...")
230
  try:
231
- return self.backup.generate(prompt)
232
  except Exception as fe:
233
- logger.error(f"Backup LLM (GLM-4.7-Flash) also failed: {fe}")
234
- raise RuntimeError("All LLM services (Primary Qwen & Backup GLM-4.7-Flash) failed after retries")
 
 
 
 
235
 
236
  llm_service = LLMServiceDispatcher()
237
 
 
1
  import os
2
  import threading
3
+ import time, json
4
  import logging
5
+ import requests
6
  import html
7
+ import random
8
+ from abc import ABC, abstractmethod
9
+ from gradio_client import Client, handle_file
10
  from ..config import settings
11
  from ..utils.json_utils import extract_json_block, repair_json
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ class ILLMService(ABC):
16
+ @abstractmethod
17
+ def generate(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> str:
18
+ """Generate text response."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def evaluate_json(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> dict:
23
+ """Generate structured JSON response."""
24
+ pass
25
 
26
+ class TencentHy3Service(ILLMService):
27
+ def __init__(self):
28
+ self.model_name = "tencent/Hy3"
29
+ self.timeout = getattr(settings, "LLM_RESPONSE_TIMEOUT", 1080)
30
+ self.max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
 
 
 
31
 
32
+ def _call(self, prompt: str, sys_prompt: str, result_box: list, error_box: list):
33
+ client = None
34
  try:
35
+ client = Client(self.model_name)
36
+ result2 = client.predict(
37
+ message=prompt,
38
+ system_prompt=sys_prompt or "",
39
+ history=None,
40
+ think_level="high",
41
+ temperature=None,
42
+ max_tokens=0,
43
+ top_p=0,
44
+ functions_json_str="",
45
+ api_name="/chat"
 
 
 
 
 
 
46
  )
47
+ result_box[0] = result2
 
48
  except Exception as e:
49
  error_box[0] = e
50
+ finally:
51
+ if client:
52
+ try:
53
+ client.close()
54
+ except:
55
+ pass
56
 
57
  def generate(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> str:
58
+ if retry_count >= self.max_retries:
59
+ raise RuntimeError(f"Max Hy3 retries ({self.max_retries}) exceeded")
 
 
 
 
60
 
61
  rb, eb = [None], [None]
62
+ t = threading.Thread(target=self._call, args=(prompt, sys_prompt, rb, eb), daemon=True)
63
  t.start()
64
+ t.join(timeout=self.timeout)
65
 
66
  if t.is_alive():
67
+ logger.warning(f"Hy3 timeout. Attempt {retry_count + 1}/{self.max_retries}")
68
  return self.generate(prompt, sys_prompt, retry_count + 1)
69
 
70
  if eb[0]:
71
+ logger.error(f"Hy3 error: {eb[0]}. Attempt {retry_count + 1}/{self.max_retries}")
72
  time.sleep(2)
73
  return self.generate(prompt, sys_prompt, retry_count + 1)
74
 
75
  if rb[0] is None:
76
  return self.generate(prompt, sys_prompt, retry_count + 1)
77
 
78
+ try:
79
+ res = rb[0]
80
+ if isinstance(res, (list, tuple)) and len(res) > 0:
81
+ response_text = res[0]
82
+ else:
83
+ response_text = str(res)
84
+ return response_text.strip()
85
+ except Exception as e:
86
+ logger.error(f"Parse error for Hy3: {e}")
87
+ return str(rb[0])
88
+
89
+ def evaluate_json(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> dict:
90
+ raw_text = self.generate(prompt, sys_prompt, retry_count)
91
  json_str = extract_json_block(raw_text)
92
  data = repair_json(json_str)
93
+ if data and isinstance(data, dict):
94
+ return data
95
+ try:
96
+ return json.loads(json_str)
97
+ except Exception:
98
+ if retry_count < self.max_retries:
99
+ return self.evaluate_json(prompt, sys_prompt, retry_count + 1)
100
+ raise ValueError("Hy3 failed to return valid JSON.")
101
 
102
+ class GeminiService(ILLMService):
103
+ def __init__(self):
104
+ self.api_key = getattr(settings, "GEMINI_API_KEY", "") or os.getenv("GEMINI_API_KEY", "")
105
+ self.model_name = getattr(settings, "GEMINI_MODEL_NAME", "gemini-3.1-flash-lite")
106
+ self.base_url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent"
107
+ self.timeout = getattr(settings, "LLM_RESPONSE_TIMEOUT", 1080)
108
+ self.max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
109
+
110
+ def _call(self, prompt: str, sys_prompt: str, result_box: list, error_box: list):
111
+ try:
112
+ headers = {'Content-Type': 'application/json'}
113
+ url = f"{self.base_url}?key={self.api_key}"
114
+
115
+ system_instruction = {"parts": [{"text": sys_prompt}]} if sys_prompt else None
116
+
117
+ payload = {
118
+ "contents": [
119
+ {
120
+ "parts": [
121
+ {"text": prompt}
122
+ ]
123
+ }
124
+ ],
125
+ "generationConfig": {
126
+ "temperature": 0.7,
127
+ }
128
+ }
129
+ if system_instruction:
130
+ payload["systemInstruction"] = system_instruction
131
+
132
+ res = requests.post(url, headers=headers, json=payload, timeout=self.timeout)
133
+ if res.status_code != 200:
134
+ error_box[0] = f"HTTP {res.status_code}: {res.text}"
135
+ else:
136
+ result_box[0] = res.json()
137
+ except Exception as e:
138
+ error_box[0] = e
139
+
140
+ def generate(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> str:
141
+ if retry_count >= self.max_retries:
142
+ raise RuntimeError(f"Max Gemini retries ({self.max_retries}) exceeded")
143
 
144
  rb, eb = [None], [None]
145
+ self._call(prompt, sys_prompt, rb, eb)
 
 
146
 
147
+ if eb[0]:
148
+ logger.error(f"Gemini error: {eb[0]}. Attempt {retry_count + 1}/{self.max_retries}")
149
  time.sleep(2)
150
+ return self.generate(prompt, sys_prompt, retry_count + 1)
151
+
152
+ if rb[0] is None:
153
+ return self.generate(prompt, sys_prompt, retry_count + 1)
154
+
155
+ try:
156
+ data = rb[0]
157
+ text = data['candidates'][0]['content']['parts'][0]['text']
158
+ return text.strip()
159
+ except Exception as e:
160
+ logger.error(f"Parse error for Gemini: {e}")
161
+ return str(rb[0])
162
 
163
+ def evaluate_json(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> dict:
164
+ raw_text = self.generate(prompt, sys_prompt, retry_count)
165
  json_str = extract_json_block(raw_text)
166
+ data = repair_json(json_str)
167
+ if data and isinstance(data, dict):
168
+ return data
 
 
169
  try:
170
  return json.loads(json_str)
171
+ except Exception:
172
+ if retry_count < self.max_retries:
173
+ return self.evaluate_json(prompt, sys_prompt, retry_count + 1)
174
+ raise ValueError("Gemini failed to return valid JSON.")
175
 
176
+ class QwenOmniService(ILLMService):
177
  def __init__(self):
178
+ self.model_name = "Qwen/Qwen3.5-Omni-Offline-Demo"
179
+ self.timeout = getattr(settings, "LLM_RESPONSE_TIMEOUT", 1080)
180
+ self.max_retries = getattr(settings, "MAX_LLM_RETRIES", 3)
181
 
182
+ def _call(self, prompt: str, sys_prompt: str, result_box: list, error_box: list):
183
+ client = None
 
 
 
 
 
184
  try:
185
+ client = Client(self.model_name)
186
+ client.predict(api_name="/clear_history_offline")
187
+ result = client.predict(
188
+ text=prompt,
189
+ audio=None,
190
+ image=None,
191
+ video=None,
192
+ history=[],
193
+ system_prompt=sys_prompt or "You are a helpful expert. Return accurate responses.",
194
+ temperature=0.7,
195
+ top_p=0.8,
196
+ top_k=20,
197
+ api_name="/chat_predict"
 
 
 
 
 
 
 
 
 
198
  )
199
  result_box[0] = result
200
  except Exception as e:
201
  error_box[0] = e
202
+ finally:
203
+ if client:
204
+ try:
205
+ client.close()
206
+ except:
207
+ pass
208
 
209
+ def generate(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> str:
210
+ if retry_count >= self.max_retries:
211
+ raise RuntimeError(f"Max Qwen Omni retries ({self.max_retries}) exceeded")
 
 
 
 
212
 
213
  rb, eb = [None], [None]
214
+ t = threading.Thread(target=self._call, args=(prompt, sys_prompt, rb, eb), daemon=True)
215
  t.start()
216
+ t.join(timeout=self.timeout)
217
 
218
  if t.is_alive():
219
+ logger.warning(f"Qwen Omni timeout. Attempt {retry_count + 1}/{self.max_retries}")
220
+ return self.generate(prompt, sys_prompt, retry_count + 1)
221
 
222
  if eb[0]:
223
+ logger.error(f"Qwen Omni error: {eb[0]}. Attempt {retry_count + 1}/{self.max_retries}")
224
  time.sleep(2)
225
+ return self.generate(prompt, sys_prompt, retry_count + 1)
226
 
227
  if rb[0] is None:
228
+ return self.generate(prompt, sys_prompt, retry_count + 1)
229
 
230
  try:
231
  res = rb[0]
 
232
  if isinstance(res, (list, tuple)) and len(res) > 0:
233
+ response_text = res[0]
234
+ else:
235
+ response_text = str(res)
236
+ return response_text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  except Exception as e:
238
+ logger.error(f"Parse error for Qwen Omni: {e}")
239
  return str(rb[0])
240
 
241
+ def evaluate_json(self, prompt: str, sys_prompt: str = None, retry_count: int = 0) -> dict:
242
+ raw_text = self.generate(prompt, sys_prompt, retry_count)
243
+ json_str = extract_json_block(raw_text)
244
+ data = repair_json(json_str)
245
+ if data and isinstance(data, dict):
246
+ return data
247
+ try:
248
+ return json.loads(json_str)
249
+ except Exception:
250
+ if retry_count < self.max_retries:
251
+ return self.evaluate_json(prompt, sys_prompt, retry_count + 1)
252
+ raise ValueError("Qwen Omni failed to return valid JSON.")
253
+
254
  class LLMServiceDispatcher:
255
  def __init__(self):
256
+ self.primary = TencentHy3Service()
257
+ self.secondary = GeminiService()
258
+ self.tertiary = QwenOmniService()
259
 
260
  @property
261
+ def judge(self) -> ILLMService:
262
+ """Gemini 3.1 Flash Lite as RAGAS Judge (to conserve rate limits on Primary if needed, or because Gemini is better at JSON)"""
263
+ return self.secondary
264
 
265
+ def generate(self, prompt: str, sys_prompt: str = None) -> str:
266
  """
267
+ Generates text using Primary (Tencent Hy3).
268
+ Falls back to Secondary (Gemini) if primary fails.
269
+ Falls back to Tertiary (Qwen Omni) if secondary fails.
270
  """
271
  try:
272
+ logger.info("Attempting generation with Primary LLM (Tencent Hy3)...")
273
+ return self.primary.generate(prompt, sys_prompt)
274
  except Exception as e:
275
+ logger.warning(f"Primary Tencent Hy3 failed: {e}. Falling back to Gemini...")
276
  try:
277
+ return self.secondary.generate(prompt, sys_prompt)
278
  except Exception as fe:
279
+ logger.warning(f"Secondary Gemini also failed: {fe}. Falling back to Qwen Omni...")
280
+ try:
281
+ return self.tertiary.generate(prompt, sys_prompt)
282
+ except Exception as te:
283
+ logger.error(f"Tertiary Qwen Omni also failed: {te}")
284
+ raise RuntimeError("All LLM services failed after retries")
285
 
286
  llm_service = LLMServiceDispatcher()
287
 
RAG_FULL_APPLICATION_BACKEND/app/techniques/ragas_eval.py CHANGED
@@ -125,9 +125,32 @@ Format your output EXACTLY as this JSON structure:
125
  return await self.underlying.retrieve(query, document_id, top_k=top_k, **kwargs)
126
 
127
  async def generate(self, query: str, chunks: List[Dict[str, Any]]) -> str:
128
- base_answer = await self.underlying.generate(query, chunks)
129
- # Evaluate single query answer with GLM-4.7-Flash judge
 
130
  contexts = [c["text"] for c in chunks]
131
- scores = await self.evaluate_item(query, base_answer, contexts, ground_truth="")
132
- return f"{base_answer}\n\n---\n**RAGAs Quality Score (GLM-4.7-Flash Judge)**:\n- Faithfulness: `{scores['faithfulness']:.2f}`\n- Relevancy: `{scores['answer_relevancy']:.2f}`\n- Precision: `{scores['context_precision']:.2f}`\n- Recall: `{scores['context_recall']:.2f}`"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
 
125
  return await self.underlying.retrieve(query, document_id, top_k=top_k, **kwargs)
126
 
127
  async def generate(self, query: str, chunks: List[Dict[str, Any]]) -> str:
128
+ max_retries = 2
129
+ base_answer = ""
130
+ scores = {}
131
  contexts = [c["text"] for c in chunks]
132
+ attempt = 0
133
+
134
+ for attempt in range(max_retries + 1):
135
+ if attempt > 0:
136
+ await self.emit("GENERATE", "#F59E0B", f"Self-Correction (Attempt {attempt}): Regenerating answer due to low score...")
137
+ feedback_prompt = (
138
+ f"Your previous answer was evaluated and scored low on these metrics:\n"
139
+ f"Faithfulness: {scores.get('faithfulness', 0)}\n"
140
+ f"Relevancy: {scores.get('answer_relevancy', 0)}\n"
141
+ f"Please try again. Ensure the answer is faithful to the context and highly relevant to the query."
142
+ )
143
+ new_query = f"{query}\n\n[FEEDBACK FROM PREVIOUS ATTEMPT]: {feedback_prompt}"
144
+ base_answer = await self.underlying.generate(new_query, chunks)
145
+ else:
146
+ base_answer = await self.underlying.generate(query, chunks)
147
+
148
+ # Evaluate single query answer with Gemini judge
149
+ scores = await self.evaluate_item(query, base_answer, contexts, ground_truth="")
150
+
151
+ # Check if scores are acceptable
152
+ if scores["faithfulness"] >= 0.8 and scores["answer_relevancy"] >= 0.8:
153
+ break
154
+
155
+ return f"{base_answer}\n\n---\n**RAGAs Quality Score (Gemini Judge)**:\n- Faithfulness: `{scores['faithfulness']:.2f}`\n- Relevancy: `{scores['answer_relevancy']:.2f}`\n- Precision: `{scores['context_precision']:.2f}`\n- Recall: `{scores['context_recall']:.2f}`\n- *Self-Correction Retries: {attempt}*"
156