Danialebrat commited on
Commit
18fc113
·
1 Parent(s): aa19464

- adding robust parsing strategy for LLM output

Browse files

- including token counter for all models for price analysis

Files changed (1) hide show
  1. Messaging_system/LLM.py +76 -33
Messaging_system/LLM.py CHANGED
@@ -114,6 +114,11 @@ class LLM:
114
  'total_tokens': response.usage.total_tokens
115
  }
116
 
 
 
 
 
 
117
  try:
118
  content = response.choices[0].message.content
119
 
@@ -132,10 +137,6 @@ class LLM:
132
  f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
133
  continue
134
 
135
- # validating the JSON
136
- self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
137
- self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
138
- self.Core.temp_token_counter += tokens['total_tokens']
139
  return output
140
 
141
  except json.JSONDecodeError:
@@ -172,6 +173,17 @@ class LLM:
172
  ))
173
 
174
  # output = json.loads(str(response.text))
 
 
 
 
 
 
 
 
 
 
 
175
  output = self.preprocess_and_parse_json(response.text)
176
 
177
  if 'message' not in output or 'header' not in output:
@@ -214,7 +226,7 @@ class LLM:
214
  {"role": "system", "content": instructions},
215
  {"role": "user", "content": prompt}
216
  ],
217
- reasoning_effort="medium",
218
  n=1,
219
  )
220
 
@@ -236,6 +248,11 @@ class LLM:
236
  'total_tokens': response.usage.total_tokens
237
  }
238
 
 
 
 
 
 
239
  try:
240
  content = response.choices[0].message.content
241
 
@@ -254,10 +271,6 @@ class LLM:
254
  f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
255
  continue
256
 
257
- # validating the JSON
258
- self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
259
- self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
260
- self.Core.temp_token_counter += tokens['total_tokens']
261
  return output
262
 
263
  except json.JSONDecodeError:
@@ -359,11 +372,15 @@ class LLM:
359
  response = message.content[0].text
360
 
361
  tokens = {
362
- 'prompt_tokens': 0,
363
- 'completion_tokens': 0,
364
- 'total_tokens': 0
365
  }
366
 
 
 
 
 
367
  try:
368
  output = self.preprocess_and_parse_json_claude(response)
369
  if output is None:
@@ -394,30 +411,56 @@ class LLM:
394
 
395
  def preprocess_and_parse_json(self, response: str):
396
  """
397
- Cleans an LLM response by removing <think> tags and extracting JSON
398
- from ```json ... ``` fences (or bare text if no fence is found),
399
- then returns the parsed object or None on failure.
400
  """
401
- # 1) Remove all <think>...</think> blocks
402
- cleaned = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL)
403
-
404
- # 2) Look for a ```json ... ``` fenced block
405
- fence_pattern = re.compile(r'```json(.*?)```', flags=re.DOTALL)
406
- fence_match = fence_pattern.search(cleaned)
407
- if fence_match:
408
- json_text = fence_match.group(1).strip()
409
- else:
410
- # No fence; assume whole cleaned text is JSON
411
- json_text = cleaned.strip()
412
 
413
- # 3) Attempt to parse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  try:
415
- return json.loads(json_text)
416
- except json.JSONDecodeError as e:
417
- print(f"Failed to parse JSON: {e}")
418
- # Optionally, log the offending text for debugging:
419
- # print("Offending text:", json_text)
420
- return None
 
 
 
 
 
 
 
421
 
422
  # ===============================================================
423
  # def preprocess_and_parse_json_claude(self, response: str):
 
114
  'total_tokens': response.usage.total_tokens
115
  }
116
 
117
+ # validating the JSON
118
+ self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
119
+ self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
120
+ self.Core.temp_token_counter += tokens['total_tokens']
121
+
122
  try:
123
  content = response.choices[0].message.content
124
 
 
137
  f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
138
  continue
139
 
 
 
 
 
140
  return output
141
 
142
  except json.JSONDecodeError:
 
173
  ))
174
 
175
  # output = json.loads(str(response.text))
176
+ tokens = {
177
+ 'prompt_tokens': response.usage_metadata.prompt_token_count,
178
+ 'completion_tokens': response.usage_metadata.candidates_token_count,
179
+ 'total_tokens': response.usage_metadata.total_token_count
180
+ }
181
+
182
+ # validating the JSON
183
+ self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
184
+ self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
185
+ self.Core.temp_token_counter += tokens['total_tokens']
186
+
187
  output = self.preprocess_and_parse_json(response.text)
188
 
189
  if 'message' not in output or 'header' not in output:
 
226
  {"role": "system", "content": instructions},
227
  {"role": "user", "content": prompt}
228
  ],
229
+ reasoning_effort="minimal",
230
  n=1,
231
  )
232
 
 
248
  'total_tokens': response.usage.total_tokens
249
  }
250
 
251
+ # validating the JSON
252
+ self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
253
+ self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
254
+ self.Core.temp_token_counter += tokens['total_tokens']
255
+
256
  try:
257
  content = response.choices[0].message.content
258
 
 
271
  f"'header' or 'message' is more than specified characters in response on attempt {attempt + 1}. Retrying...")
272
  continue
273
 
 
 
 
 
274
  return output
275
 
276
  except json.JSONDecodeError:
 
372
  response = message.content[0].text
373
 
374
  tokens = {
375
+ 'prompt_tokens': message.usage.input_tokens,
376
+ 'completion_tokens': message.usage.output_tokens,
377
+ 'total_tokens': message.usage.output_tokens + message.usage.input_tokens
378
  }
379
 
380
+ self.Core.total_tokens['prompt_tokens'] += tokens['prompt_tokens']
381
+ self.Core.total_tokens['completion_tokens'] += tokens['completion_tokens']
382
+ self.Core.temp_token_counter += tokens['total_tokens']
383
+
384
  try:
385
  output = self.preprocess_and_parse_json_claude(response)
386
  if output is None:
 
411
 
412
  def preprocess_and_parse_json(self, response: str):
413
  """
414
+ Remove <think> blocks, extract JSON (from ```json fences or first {...} block),
415
+ and parse. Includes a repair pass to handle common LLM issues like trailing commas.
 
416
  """
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ def extract_json(text: str) -> str:
419
+ # Remove <think>...</think>
420
+ text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
421
+
422
+ # Prefer fenced code if present
423
+ fence = re.search(r'```(?:json)?(.*?)```', text, flags=re.DOTALL | re.IGNORECASE)
424
+ if fence:
425
+ return fence.group(1).strip()
426
+
427
+ # Otherwise, grab the first {...} block
428
+ brace = re.search(r'\{.*\}', text, flags=re.DOTALL)
429
+ return brace.group(0).strip() if brace else text.strip()
430
+
431
+ def normalize_quotes(text: str) -> str:
432
+ return (text
433
+ .replace('\ufeff', '') # strip BOM if present
434
+ .replace('“', '"').replace('”', '"')
435
+ .replace('‘', "'").replace('’', "'"))
436
+
437
+ def strip_comments(text: str) -> str:
438
+ # Remove // line comments and /* block comments */
439
+ text = re.sub(r'//.*?$', '', text, flags=re.MULTILINE)
440
+ text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
441
+ return text
442
+
443
+ def remove_trailing_commas(text: str) -> str:
444
+ # Remove commas before } or ]
445
+ return re.sub(r',(\s*[}\]])', r'\1', text)
446
+
447
+ raw = extract_json(response)
448
+ raw = normalize_quotes(raw)
449
+
450
  try:
451
+ return json.loads(raw)
452
+ except json.JSONDecodeError:
453
+ # Repair pass
454
+ repaired = strip_comments(raw)
455
+ repaired = remove_trailing_commas(repaired)
456
+ repaired = repaired.strip()
457
+
458
+ try:
459
+ return json.loads(repaired)
460
+ except json.JSONDecodeError as e:
461
+ print(f"Failed to parse JSON: {e}")
462
+ # print('Offending text:', repaired)
463
+ return None
464
 
465
  # ===============================================================
466
  # def preprocess_and_parse_json_claude(self, response: str):