CodeCommunity commited on
Commit
3cce99c
·
verified ·
1 Parent(s): 32a4542

Update app/core/model_loader.py

Browse files
Files changed (1) hide show
  1. app/core/model_loader.py +65 -102
app/core/model_loader.py CHANGED
@@ -1,22 +1,17 @@
1
- # app/core/model_loader.py
2
  import os
3
  import json
4
  import logging
5
  import threading
6
  import time
7
  from datetime import datetime
8
- import google.generativeai as genai
9
- from google.generativeai.types import HarmCategory, HarmBlockThreshold
10
  from dotenv import load_dotenv
 
11
 
12
- # Force load .env immediately upon module import
13
  load_dotenv()
14
 
15
  logger = logging.getLogger(__name__)
16
-
17
  STATS_FILE = "usage_stats.json"
18
 
19
-
20
  class LLMSingleton:
21
  _instance = None
22
  _instance_lock = threading.Lock()
@@ -33,45 +28,21 @@ class LLMSingleton:
33
  if self._instance is not None:
34
  raise Exception("Singleton instance already exists!")
35
 
36
- self.api_key = os.getenv("GEMINI_API_KEY")
37
  if not self.api_key:
38
- logger.warning(
39
- "⚠️ GEMINI_API_KEY not found in environment variables. AI features will fail."
40
- )
41
  else:
42
- genai.configure(api_key=self.api_key)
43
-
44
- # Gemini Flash Latest: Stable alias (likely 1.5 Flash)
45
- # Explicitly pinning to 1.5-flash to guarantee the 1500 RPD free tier
46
- self.model_name = "gemini-flash-latest"
47
- self.generation_config = {
48
- "temperature": 0.3,
49
- "top_p": 0.95,
50
- "top_k": 64,
51
- "max_output_tokens": 8192,
52
- "response_mime_type": "application/json",
53
- }
54
- self.safety_settings = {
55
- HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
56
- HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
57
- HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
58
- HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
59
- }
60
 
61
- # Guards self.stats (read/modify/write) and the JSON file writes.
62
- # Needed now that multiple review batches can call generate()
63
- # concurrently from different threads.
64
  self._stats_lock = threading.Lock()
65
-
66
- # Guards RPM window tracking for the same reason.
67
  self._rpm_lock = threading.Lock()
68
 
69
- # Load persistent stats
70
  self.stats = self._load_stats()
71
  self._check_daily_reset()
72
 
73
- # RPM Tracking
74
- self.rpm_limit = 15
75
  self.minute_window_start = time.time()
76
  self.requests_this_minute = 0
77
 
@@ -91,16 +62,12 @@ class LLMSingleton:
91
  try:
92
  with open(STATS_FILE, "r") as f:
93
  data = json.load(f)
94
- # Merge defaults for backward compatibility
95
  return {**default_stats, **data}
96
  except Exception as e:
97
  logger.error(f"Failed to load stats: {e}")
98
  return default_stats
99
 
100
  def _save_stats(self):
101
- """
102
- Caller must hold self._stats_lock before calling this.
103
- """
104
  try:
105
  with open(STATS_FILE, "w") as f:
106
  json.dump(self.stats, f, indent=2)
@@ -117,9 +84,6 @@ class LLMSingleton:
117
  self._save_stats()
118
 
119
  def _check_rpm_window(self):
120
- """
121
- Resets the minute counter if 60 seconds have passed.
122
- """
123
  with self._rpm_lock:
124
  now = time.time()
125
  if now - self.minute_window_start >= 60:
@@ -129,81 +93,55 @@ class LLMSingleton:
129
  def get_usage_stats(self):
130
  self._check_daily_reset()
131
  self._check_rpm_window()
132
-
133
  with self._stats_lock:
134
  stats = self.stats.copy()
135
-
136
  with self._rpm_lock:
137
  requests_this_minute = self.requests_this_minute
138
 
139
- # Limits for Gemini 3 Flash (Free Tier)
140
- daily_limit = 500
141
-
142
  stats["limits"] = {
143
  "requests_per_minute": self.rpm_limit,
144
  "requests_per_day": daily_limit,
145
  }
146
- stats["remaining_daily_requests"] = max(
147
- 0, daily_limit - stats["daily_requests_count"]
148
- )
149
  stats["remaining_rpm"] = max(0, self.rpm_limit - requests_this_minute)
150
-
151
  return stats
152
 
153
  def track_local_usage(self, input_chars: int = 0):
154
- """
155
- Track usage of local models (like CodeBERT).
156
- """
157
  with self._stats_lock:
158
  self.stats["local_model_requests"] += 1
159
  self.stats["input_tokens"] += input_chars // 4
160
  self._save_stats()
161
 
162
  def _reserve_request_slot(self) -> bool:
163
- """
164
- Atomically checks quota and increments counters for one outgoing
165
- request. Returns False if the daily quota is exceeded (caller should
166
- raise before touching the network). This closes the old race where
167
- two threads could both pass the quota check before either one
168
- incremented daily_requests_count.
169
- """
170
  with self._stats_lock:
171
- if self.stats["daily_requests_count"] >= 500:
172
  return False
173
  self.stats["total_requests"] += 1
174
  self.stats["daily_requests_count"] += 1
175
  self._save_stats()
176
-
177
  with self._rpm_lock:
178
  self.requests_this_minute += 1
179
-
180
  return True
181
 
182
- def generate(self, prompt: str, max_tokens: int = 8192) -> str:
183
  import random
184
 
185
  self._check_daily_reset()
186
  self._check_rpm_window()
187
 
188
- if not self.api_key:
189
- logger.error("Cannot generate: Missing GEMINI_API_KEY")
190
  raise RuntimeError("MISSING_API_KEY")
191
 
192
  if not self._reserve_request_slot():
193
- logger.error("❌ Daily Quota Exceeded (500 requests). Request blocked.")
194
  raise RuntimeError("QUOTA_EXCEEDED")
195
 
196
- # Log the prompt source (first 50 chars) to identify the caller
197
- logger.info(f"🤖 Generating with Gemini. Prompt start: {prompt[:50]}...")
198
-
199
- model = genai.GenerativeModel(
200
- model_name=self.model_name,
201
- generation_config=self.generation_config,
202
- safety_settings=self.safety_settings,
203
- )
204
 
205
  retries = 0
206
- max_retries = 5
207
  base_delay = 2
208
 
209
  while retries <= max_retries:
@@ -212,24 +150,39 @@ class LLMSingleton:
212
  self.stats["input_tokens"] += len(prompt) // 4
213
  self._save_stats()
214
 
215
- response = model.generate_content(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  with self._stats_lock:
218
  self.stats["successful_requests"] += 1
219
- if response.text:
220
- self.stats["output_tokens"] += len(response.text) // 4
221
  self._save_stats()
222
 
223
- return response.text.strip()
 
224
  except Exception as e:
225
  error_str = str(e)
226
- if "429" in error_str or "quota" in error_str.lower():
227
  with self._stats_lock:
228
  self.stats["rate_limit_hits"] += 1
229
  self._save_stats()
230
- wait_time = (base_delay * (2**retries)) + random.uniform(0, 1)
231
  logger.warning(
232
- f"⚠️ Rate limit hit. Retrying in {wait_time:.2f}s... (Attempt {retries+1}/{max_retries})"
233
  )
234
  time.sleep(wait_time)
235
  retries += 1
@@ -237,7 +190,7 @@ class LLMSingleton:
237
  with self._stats_lock:
238
  self.stats["errors"] += 1
239
  self._save_stats()
240
- logger.error(f"Gemini generation failed: {e}")
241
  raise RuntimeError(f"GENERATION_FAILED: {e}")
242
 
243
  with self._stats_lock:
@@ -247,33 +200,43 @@ class LLMSingleton:
247
  raise RuntimeError("RATE_LIMIT_EXCEEDED")
248
 
249
  def generate_text(self, prompt: str) -> str:
250
- """
251
- Helper for non-JSON text generation (like Guides).
252
- """
253
  self._check_daily_reset()
254
  self._check_rpm_window()
255
 
256
- if not self.api_key:
257
  return "Error: Missing API Key."
258
 
259
  if not self._reserve_request_slot():
260
  return "Error: Daily Quota Exceeded."
261
 
262
  try:
263
- # Override config for text
264
- config = self.generation_config.copy()
265
- config["response_mime_type"] = "text/plain"
266
-
267
- model = genai.GenerativeModel(
268
- model_name=self.model_name,
269
- generation_config=config,
270
- safety_settings=self.safety_settings,
 
 
 
 
 
 
 
271
  )
272
- response = model.generate_content(prompt)
273
- return response.text
 
 
 
 
 
 
 
274
  except Exception as e:
275
- logger.error(f"Gemini text generation failed: {e}")
276
  return f"Error generating content: {str(e)}"
277
 
278
-
279
  llm_engine = LLMSingleton.get_instance()
 
 
1
  import os
2
  import json
3
  import logging
4
  import threading
5
  import time
6
  from datetime import datetime
 
 
7
  from dotenv import load_dotenv
8
+ from groq import Groq
9
 
 
10
  load_dotenv()
11
 
12
  logger = logging.getLogger(__name__)
 
13
  STATS_FILE = "usage_stats.json"
14
 
 
15
  class LLMSingleton:
16
  _instance = None
17
  _instance_lock = threading.Lock()
 
28
  if self._instance is not None:
29
  raise Exception("Singleton instance already exists!")
30
 
31
+ self.api_key = os.getenv("GROQ_API_KEY")
32
  if not self.api_key:
33
+ logger.warning("⚠️ GROQ_API_KEY not found in environment variables. AI features will fail.")
34
+ self.client = None
 
35
  else:
36
+ self.client = Groq(api_key=self.api_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ self.model_name = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
 
 
39
  self._stats_lock = threading.Lock()
 
 
40
  self._rpm_lock = threading.Lock()
41
 
 
42
  self.stats = self._load_stats()
43
  self._check_daily_reset()
44
 
45
+ self.rpm_limit = 30
 
46
  self.minute_window_start = time.time()
47
  self.requests_this_minute = 0
48
 
 
62
  try:
63
  with open(STATS_FILE, "r") as f:
64
  data = json.load(f)
 
65
  return {**default_stats, **data}
66
  except Exception as e:
67
  logger.error(f"Failed to load stats: {e}")
68
  return default_stats
69
 
70
  def _save_stats(self):
 
 
 
71
  try:
72
  with open(STATS_FILE, "w") as f:
73
  json.dump(self.stats, f, indent=2)
 
84
  self._save_stats()
85
 
86
  def _check_rpm_window(self):
 
 
 
87
  with self._rpm_lock:
88
  now = time.time()
89
  if now - self.minute_window_start >= 60:
 
93
  def get_usage_stats(self):
94
  self._check_daily_reset()
95
  self._check_rpm_window()
 
96
  with self._stats_lock:
97
  stats = self.stats.copy()
 
98
  with self._rpm_lock:
99
  requests_this_minute = self.requests_this_minute
100
 
101
+ daily_limit = 1000
 
 
102
  stats["limits"] = {
103
  "requests_per_minute": self.rpm_limit,
104
  "requests_per_day": daily_limit,
105
  }
106
+ stats["remaining_daily_requests"] = max(0, daily_limit - stats["daily_requests_count"])
 
 
107
  stats["remaining_rpm"] = max(0, self.rpm_limit - requests_this_minute)
 
108
  return stats
109
 
110
  def track_local_usage(self, input_chars: int = 0):
 
 
 
111
  with self._stats_lock:
112
  self.stats["local_model_requests"] += 1
113
  self.stats["input_tokens"] += input_chars // 4
114
  self._save_stats()
115
 
116
  def _reserve_request_slot(self) -> bool:
 
 
 
 
 
 
 
117
  with self._stats_lock:
118
+ if self.stats["daily_requests_count"] >= 1000:
119
  return False
120
  self.stats["total_requests"] += 1
121
  self.stats["daily_requests_count"] += 1
122
  self._save_stats()
 
123
  with self._rpm_lock:
124
  self.requests_this_minute += 1
 
125
  return True
126
 
127
+ def generate(self, prompt: str, max_tokens: int = 2048) -> str:
128
  import random
129
 
130
  self._check_daily_reset()
131
  self._check_rpm_window()
132
 
133
+ if not self.api_key or not self.client:
134
+ logger.error("Cannot generate: Missing GROQ_API_KEY")
135
  raise RuntimeError("MISSING_API_KEY")
136
 
137
  if not self._reserve_request_slot():
138
+ logger.error("❌ Daily Quota Exceeded. Request blocked.")
139
  raise RuntimeError("QUOTA_EXCEEDED")
140
 
141
+ logger.info(f"🤖 Generating with Groq ({self.model_name}). Prompt start: {prompt[:50]}...")
 
 
 
 
 
 
 
142
 
143
  retries = 0
144
+ max_retries = 3
145
  base_delay = 2
146
 
147
  while retries <= max_retries:
 
150
  self.stats["input_tokens"] += len(prompt) // 4
151
  self._save_stats()
152
 
153
+ chat_completion = self.client.chat.completions.create(
154
+ model=self.model_name,
155
+ messages=[
156
+ {
157
+ "role": "system",
158
+ "content": "You are a senior Android code reviewer. You MUST return a valid JSON object matching the requested schema strictly.",
159
+ },
160
+ {"role": "user", "content": prompt},
161
+ ],
162
+ response_format={"type": "json_object"},
163
+ max_tokens=max_tokens,
164
+ temperature=0.2,
165
+ )
166
+
167
+ response_text = chat_completion.choices[0].message.content or ""
168
 
169
  with self._stats_lock:
170
  self.stats["successful_requests"] += 1
171
+ if response_text:
172
+ self.stats["output_tokens"] += len(response_text) // 4
173
  self._save_stats()
174
 
175
+ return response_text.strip()
176
+
177
  except Exception as e:
178
  error_str = str(e)
179
+ if "429" in error_str or "rate_limit_exceeded" in error_str.lower():
180
  with self._stats_lock:
181
  self.stats["rate_limit_hits"] += 1
182
  self._save_stats()
183
+ wait_time = (base_delay * (2**retries)) + random.uniform(0.5, 1.5)
184
  logger.warning(
185
+ f"⚠️ Groq rate limit hit. Retrying in {wait_time:.2f}s... (Attempt {retries + 1}/{max_retries})"
186
  )
187
  time.sleep(wait_time)
188
  retries += 1
 
190
  with self._stats_lock:
191
  self.stats["errors"] += 1
192
  self._save_stats()
193
+ logger.error(f"Groq generation failed: {e}")
194
  raise RuntimeError(f"GENERATION_FAILED: {e}")
195
 
196
  with self._stats_lock:
 
200
  raise RuntimeError("RATE_LIMIT_EXCEEDED")
201
 
202
  def generate_text(self, prompt: str) -> str:
 
 
 
203
  self._check_daily_reset()
204
  self._check_rpm_window()
205
 
206
+ if not self.api_key or not self.client:
207
  return "Error: Missing API Key."
208
 
209
  if not self._reserve_request_slot():
210
  return "Error: Daily Quota Exceeded."
211
 
212
  try:
213
+ with self._stats_lock:
214
+ self.stats["input_tokens"] += len(prompt) // 4
215
+ self._save_stats()
216
+
217
+ chat_completion = self.client.chat.completions.create(
218
+ model=self.model_name,
219
+ messages=[
220
+ {
221
+ "role": "system",
222
+ "content": "You are GitGud AI, an expert software architect.",
223
+ },
224
+ {"role": "user", "content": prompt},
225
+ ],
226
+ max_tokens=2048,
227
+ temperature=0.3,
228
  )
229
+
230
+ response_text = chat_completion.choices[0].message.content or ""
231
+
232
+ with self._stats_lock:
233
+ self.stats["successful_requests"] += 1
234
+ self.stats["output_tokens"] += len(response_text) // 4
235
+ self._save_stats()
236
+
237
+ return response_text
238
  except Exception as e:
239
+ logger.error(f"Groq text generation failed: {e}")
240
  return f"Error generating content: {str(e)}"
241
 
 
242
  llm_engine = LLMSingleton.get_instance()