Phoenix1410 commited on
Commit
b97909e
·
verified ·
1 Parent(s): 9625290

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +55 -57
main.py CHANGED
@@ -160,19 +160,20 @@ except Exception as e:
160
  qg_tokenizer = None
161
  qg_model = None
162
 
163
- # 5. THE ANALYST (Groq Reasoning Engine)
164
  if not GROQ_API_KEY:
165
  print("[WARNING] GROQ_API_KEY not found in environment.", flush=True)
166
 
167
- DEFAULT_MODEL = os.getenv("GROQ_MODEL", "openai/gpt-oss-120b")
168
  analyst = ChatGroq(
169
  temperature=0,
170
  model_name=DEFAULT_MODEL,
171
  groq_api_key=GROQ_API_KEY,
172
- request_timeout=35.0,
 
173
  max_retries=2
174
  )
175
- print(f"[OK] Analyst Model Configured: {DEFAULT_MODEL}", flush=True)
176
 
177
  # --- HELPER FUNCTIONS ---
178
 
@@ -223,9 +224,9 @@ Detection Reason: {source_str}
223
  User's Constraint Rule: {user_rule if user_rule else "None"}
224
 
225
  Task:
226
- 1. Summarize this clause in plain English.
227
- 2. If the user provided a rule, EXPLICITLY check if this clause violates it.
228
- 3. If it is risky, suggest a safer rewrite."""
229
 
230
  messages = [
231
  SystemMessage(content=system_msg),
@@ -252,7 +253,7 @@ Task:
252
 
253
  async def generate_dynamic_questions_groq(text: str, max_items: int = 5) -> list[dict]:
254
  """
255
- Extracts factual queries from Party A that focus strictly on observable anchor points
256
  (time, location, movements, doors, confrontations) that Party B (the accused) can speak to.
257
  """
258
  system_prompt = """You are a senior forensic legal interrogator.
@@ -261,18 +262,18 @@ Your goal is to extract factual queries from Party A's deposition that the OTHER
261
  CRITICAL RULES:
262
  1. NEVER frame questions around Party A's private internal actions (e.g., do NOT ask "Why did Party A go to buy groceries?" or "What was Party A thinking?").
263
  2. Focus on SHARED EXTERNAL EVENTS that Party B / Accused was either present for or accused of:
264
- - What time did the incident or encounter take place?
265
  - What specific area of the premises (e.g. register, electronics aisle, exit door) is involved?
266
  - What specific actions, movements, or interactions are alleged regarding the accused?
267
- - What exit or vehicle was allegedly used?
268
  3. For each query, quote Party A's exact sentence answering it as 'party_a_fact'.
269
  4. Respond ONLY with a valid JSON object matching:
270
  {
271
  "queries": [
272
  {
273
  "id": 0,
274
- "question": "What time did the encounter or incident take place near the register area?",
275
- "party_a_fact": "On March 14, I arrived at the store around 6:15 PM and noticed the accused near the registers."
276
  }
277
  ]
278
  }"""
@@ -353,8 +354,7 @@ def generate_dynamic_questions_local_t5(text: str, max_items: int = 5) -> list[d
353
 
354
  def extract_party_b_local_fallback(factual_pairs: list[dict], party_b_text: str) -> dict[int, str]:
355
  """
356
- Local CPU Fallback: If Groq returns empty/unmentioned for everything, uses Scout (Sentence-BERT)
357
- to find the most relevant factual sentence in Party B's testimony.
358
  """
359
  b_sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', party_b_text) if len(s.strip()) > 15]
360
  if not b_sentences or not scout:
@@ -389,14 +389,14 @@ async def extract_party_b_answers(factual_pairs: list[dict], party_b_text: str)
389
  system_prompt = """You are an objective forensic legal cross-examiner.
390
  You are examining Party B's deposition against factual queries arising from Party A's account.
391
 
392
- CRITICAL IDENTITY AND RULES:
393
  1. Party B IS the accused / respondent. Party B speaks using "I", "me", or "my".
394
- 2. If a query touches upon a time, location, or event that Party B discusses:
395
- - State Party B's version of that fact (e.g., "Party B states they were in the pharmacy aisle at 6:30 PM, not near the registers.").
396
- - If Party B denies the occurrence or offers an alibi, report that denial concisely.
397
- 3. DO NOT BE OVERLY RIGID. If the query asks about the time or location of the incident, and Party B mentions their own arrival, departure, or location during that period, THAT IS PARTY B'S ACCOUNT. Do not say "Not mentioned."
398
  4. ONLY write "Not mentioned." if Party B's entire deposition has zero information related to that timeframe, location, or interaction.
399
- 5. Respond ONLY with a valid JSON object matching:
400
  {
401
  "answers": [
402
  {"id": 0, "answer": "Party B's statement, denial, or counter-account"}
@@ -414,16 +414,12 @@ CRITICAL IDENTITY AND RULES:
414
  response = await analyst.ainvoke(messages)
415
  raw_content = response.content.strip()
416
 
417
- # 1. Clean reasoning tags & markdown
418
  raw_content = re.sub(r"<think>.*?</think>", "", raw_content, flags=re.DOTALL).strip()
419
  if "```" in raw_content:
420
  raw_content = re.sub(r"```(?:json)?", "", raw_content).replace("```", "").strip()
421
 
422
- print(f"[DEBUG Party B Output]: {raw_content[:300]}...", flush=True)
423
-
424
  results_map = {}
425
 
426
- # 2. Parse JSON
427
  json_match = re.search(r"(\{|\[).*(\}|\])", raw_content, re.DOTALL)
428
  if json_match:
429
  try:
@@ -436,7 +432,6 @@ CRITICAL IDENTITY AND RULES:
436
  id_digits = re.findall(r"\d+", str(raw_id))
437
  matched_id = int(id_digits[0]) if id_digits else idx
438
 
439
- # Handle 1-based indexing if LLM counted 1..N
440
  if matched_id not in [p["id"] for p in factual_pairs] and idx < len(factual_pairs):
441
  matched_id = factual_pairs[idx]["id"]
442
 
@@ -444,7 +439,6 @@ CRITICAL IDENTITY AND RULES:
444
  except Exception as json_err:
445
  print(f"[PARTY B JSON PARSE NOTICE]: {json_err}", flush=True)
446
 
447
- # 3. Regex Fallback if JSON parsing missed any fields
448
  if not results_map:
449
  answer_matches = re.findall(r'"answer"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"', raw_content)
450
  if answer_matches:
@@ -452,10 +446,9 @@ CRITICAL IDENTITY AND RULES:
452
  target_id = factual_pairs[idx]["id"]
453
  results_map[target_id] = ans_text.replace('\\"', '"').strip()
454
 
455
- # Check if the LLM returned "Not mentioned." for all items despite testimony being present
456
  all_unmentioned = all("not mentioned" in results_map.get(item["id"], "").lower() for item in factual_pairs)
457
  if all_unmentioned:
458
- print("[NOTICE] LLM returned 'Not mentioned.' for all items. Engaging Scout semantic matcher...", flush=True)
459
  return await asyncio.to_thread(extract_party_b_local_fallback, factual_pairs, party_b_text)
460
 
461
  for item in factual_pairs:
@@ -465,9 +458,8 @@ CRITICAL IDENTITY AND RULES:
465
  return results_map
466
 
467
  except Exception as api_err:
468
- print(f"[ERROR in Party B Extraction]: {api_err}. Engaging Scout semantic search fallback...", flush=True)
469
 
470
- # Local fallback if Groq call failed or timed out
471
  return await asyncio.to_thread(extract_party_b_local_fallback, factual_pairs, party_b_text)
472
 
473
  # --- API ENDPOINTS ---
@@ -481,6 +473,7 @@ async def health_check():
481
  diagnostic["key_present"] = bool(GROQ_API_KEY)
482
  diagnostic["key_length"] = len(GROQ_API_KEY) if GROQ_API_KEY else 0
483
  diagnostic["key_format_valid"] = (GROQ_API_KEY.startswith("gsk_") and len(GROQ_API_KEY) > 20) if GROQ_API_KEY else False
 
484
 
485
  try:
486
  ip = await asyncio.to_thread(socket.gethostbyname, "api.groq.com")
@@ -557,35 +550,41 @@ async def analyze_document(
557
  if sim_scores[idx] > 0.30:
558
  semantic_matches.add(int(idx))
559
 
 
 
 
 
 
 
 
 
560
  analysis_tasks = []
561
- for i, (clause, pred) in enumerate(zip(clauses, sniper_preds)):
562
  label_str = pred['label']
563
  risk_type = label_map.get(label_str, "Safe")
564
  is_sniper_risk = risk_type != "Safe"
565
- is_scout_match = i in semantic_matches
566
 
567
- if is_sniper_risk or is_scout_match:
568
- detection_source = []
569
- if is_sniper_risk:
570
- detection_source.append(f"Sniper Flagged ({risk_type})")
571
- if is_scout_match:
572
- detection_source.append("Scout Matched User Rule")
573
-
574
- source_str = " + ".join(detection_source)
575
- assigned_risk = risk_type if is_sniper_risk else "Potential Rule Violation"
576
-
577
- task = process_analyst_evaluation(
578
- clause=clause,
579
- user_rule=user_rule,
580
- source_str=source_str,
581
- index=i,
582
- pred_score=pred['score'],
583
- risk_type=assigned_risk
584
- )
585
- analysis_tasks.append(task)
586
 
587
- # Throttled execution to protect free tier rate limits
588
- sem = asyncio.Semaphore(4)
589
  async def bounded_task(t):
590
  async with sem:
591
  return await t
@@ -669,12 +668,11 @@ async def stream_compare_testimonies(
669
  emb_a = scout.encode([ans_a])
670
  emb_b = scout.encode([ans_b])
671
  sim = float(cosine_similarity(emb_a, emb_b)[0][0])
672
- if sim > 0.65:
 
673
  match_status = "Accounts Align"
674
- elif sim < 0.35:
675
- match_status = "Event details do not align"
676
  else:
677
- match_status = "Accounts Partially Align"
678
  except Exception:
679
  match_status = "Accounts Align" if ans_a.strip().lower() == ans_b.strip().lower() else "Event details do not align"
680
  else:
 
160
  qg_tokenizer = None
161
  qg_model = None
162
 
163
+ # 5. THE ANALYST (Fast 20B Tier with strict max_tokens & timeout to eliminate freezing)
164
  if not GROQ_API_KEY:
165
  print("[WARNING] GROQ_API_KEY not found in environment.", flush=True)
166
 
167
+ DEFAULT_MODEL = os.getenv("GROQ_MODEL", "openai/gpt-oss-20b")
168
  analyst = ChatGroq(
169
  temperature=0,
170
  model_name=DEFAULT_MODEL,
171
  groq_api_key=GROQ_API_KEY,
172
+ request_timeout=25.0,
173
+ max_tokens=300, # Prevents runaway responses & slashes response time
174
  max_retries=2
175
  )
176
+ print(f"[OK] Analyst Model Configured: {DEFAULT_MODEL} (High-Speed Tier)", flush=True)
177
 
178
  # --- HELPER FUNCTIONS ---
179
 
 
224
  User's Constraint Rule: {user_rule if user_rule else "None"}
225
 
226
  Task:
227
+ 1. Summarize this clause in 1-2 plain English sentences.
228
+ 2. If the user provided a rule, state whether this clause violates it.
229
+ 3. If risky, provide a short, balanced rewrite. Keep your entire response under 150 words."""
230
 
231
  messages = [
232
  SystemMessage(content=system_msg),
 
253
 
254
  async def generate_dynamic_questions_groq(text: str, max_items: int = 5) -> list[dict]:
255
  """
256
+ Extracts factual queries from Party A focusing strictly on observable anchor points
257
  (time, location, movements, doors, confrontations) that Party B (the accused) can speak to.
258
  """
259
  system_prompt = """You are a senior forensic legal interrogator.
 
262
  CRITICAL RULES:
263
  1. NEVER frame questions around Party A's private internal actions (e.g., do NOT ask "Why did Party A go to buy groceries?" or "What was Party A thinking?").
264
  2. Focus on SHARED EXTERNAL EVENTS that Party B / Accused was either present for or accused of:
265
+ - What time did the incident, arrival, or encounter take place?
266
  - What specific area of the premises (e.g. register, electronics aisle, exit door) is involved?
267
  - What specific actions, movements, or interactions are alleged regarding the accused?
268
+ - What exit or parking area was allegedly used?
269
  3. For each query, quote Party A's exact sentence answering it as 'party_a_fact'.
270
  4. Respond ONLY with a valid JSON object matching:
271
  {
272
  "queries": [
273
  {
274
  "id": 0,
275
+ "question": "At what time and through which door did the accused allegedly exit the store?",
276
+ "party_a_fact": "Security reviewed footage and pointed me toward the accused, who had exited through the side door near the pharmacy at approximately 6:35 PM."
277
  }
278
  ]
279
  }"""
 
354
 
355
  def extract_party_b_local_fallback(factual_pairs: list[dict], party_b_text: str) -> dict[int, str]:
356
  """
357
+ Local CPU Fallback: Uses Scout (Sentence-BERT) to find the most relevant sentence in Party B's text.
 
358
  """
359
  b_sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', party_b_text) if len(s.strip()) > 15]
360
  if not b_sentences or not scout:
 
389
  system_prompt = """You are an objective forensic legal cross-examiner.
390
  You are examining Party B's deposition against factual queries arising from Party A's account.
391
 
392
+ CRITICAL RULES:
393
  1. Party B IS the accused / respondent. Party B speaks using "I", "me", or "my".
394
+ 2. If a query touches upon a time, location, exit, or event that Party B discusses:
395
+ - State Party B's version of that fact (e.g., "Party B states they were at the pharmacy counter, not in the electronics aisle.").
396
+ - If Party B denies the occurrence or gives a different timeframe/door, report that counter-statement concisely.
397
+ 3. DO NOT BE OVERLY RIGID. If Party B mentions their arrival, departure, or location during that period, THAT IS PARTY B'S ACCOUNT. Do not say "Not mentioned."
398
  4. ONLY write "Not mentioned." if Party B's entire deposition has zero information related to that timeframe, location, or interaction.
399
+ 5. Respond ONLY with a valid JSON object:
400
  {
401
  "answers": [
402
  {"id": 0, "answer": "Party B's statement, denial, or counter-account"}
 
414
  response = await analyst.ainvoke(messages)
415
  raw_content = response.content.strip()
416
 
 
417
  raw_content = re.sub(r"<think>.*?</think>", "", raw_content, flags=re.DOTALL).strip()
418
  if "```" in raw_content:
419
  raw_content = re.sub(r"```(?:json)?", "", raw_content).replace("```", "").strip()
420
 
 
 
421
  results_map = {}
422
 
 
423
  json_match = re.search(r"(\{|\[).*(\}|\])", raw_content, re.DOTALL)
424
  if json_match:
425
  try:
 
432
  id_digits = re.findall(r"\d+", str(raw_id))
433
  matched_id = int(id_digits[0]) if id_digits else idx
434
 
 
435
  if matched_id not in [p["id"] for p in factual_pairs] and idx < len(factual_pairs):
436
  matched_id = factual_pairs[idx]["id"]
437
 
 
439
  except Exception as json_err:
440
  print(f"[PARTY B JSON PARSE NOTICE]: {json_err}", flush=True)
441
 
 
442
  if not results_map:
443
  answer_matches = re.findall(r'"answer"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"', raw_content)
444
  if answer_matches:
 
446
  target_id = factual_pairs[idx]["id"]
447
  results_map[target_id] = ans_text.replace('\\"', '"').strip()
448
 
 
449
  all_unmentioned = all("not mentioned" in results_map.get(item["id"], "").lower() for item in factual_pairs)
450
  if all_unmentioned:
451
+ print("[NOTICE] LLM returned 'Not mentioned.' for all items. Engaging Scout matcher...", flush=True)
452
  return await asyncio.to_thread(extract_party_b_local_fallback, factual_pairs, party_b_text)
453
 
454
  for item in factual_pairs:
 
458
  return results_map
459
 
460
  except Exception as api_err:
461
+ print(f"[ERROR in Party B Extraction]: {api_err}. Engaging Scout fallback...", flush=True)
462
 
 
463
  return await asyncio.to_thread(extract_party_b_local_fallback, factual_pairs, party_b_text)
464
 
465
  # --- API ENDPOINTS ---
 
473
  diagnostic["key_present"] = bool(GROQ_API_KEY)
474
  diagnostic["key_length"] = len(GROQ_API_KEY) if GROQ_API_KEY else 0
475
  diagnostic["key_format_valid"] = (GROQ_API_KEY.startswith("gsk_") and len(GROQ_API_KEY) > 20) if GROQ_API_KEY else False
476
+ diagnostic["active_model"] = DEFAULT_MODEL
477
 
478
  try:
479
  ip = await asyncio.to_thread(socket.gethostbyname, "api.groq.com")
 
550
  if sim_scores[idx] > 0.30:
551
  semantic_matches.add(int(idx))
552
 
553
+ # Priority queue: sort by model confidence and take top 8 highest-priority risks
554
+ flagged_indices = [
555
+ (i, clause, pred) for i, (clause, pred) in enumerate(zip(clauses, sniper_preds))
556
+ if label_map.get(pred['label'], "Safe") != "Safe" or i in semantic_matches
557
+ ]
558
+ flagged_indices.sort(key=lambda x: x[2]['score'], reverse=True)
559
+ top_flagged = flagged_indices[:8]
560
+
561
  analysis_tasks = []
562
+ for i, clause, pred in top_flagged:
563
  label_str = pred['label']
564
  risk_type = label_map.get(label_str, "Safe")
565
  is_sniper_risk = risk_type != "Safe"
 
566
 
567
+ detection_source = []
568
+ if is_sniper_risk:
569
+ detection_source.append(f"Sniper Flagged ({risk_type})")
570
+ if i in semantic_matches:
571
+ detection_source.append("Scout Matched User Rule")
572
+
573
+ source_str = " + ".join(detection_source)
574
+ assigned_risk = risk_type if is_sniper_risk else "Potential Rule Violation"
575
+
576
+ task = process_analyst_evaluation(
577
+ clause=clause,
578
+ user_rule=user_rule,
579
+ source_str=source_str,
580
+ index=i,
581
+ pred_score=pred['score'],
582
+ risk_type=assigned_risk
583
+ )
584
+ analysis_tasks.append(task)
 
585
 
586
+ # Throttled execution (max 3 concurrent) so Groq never rate-limits or freezes
587
+ sem = asyncio.Semaphore(3)
588
  async def bounded_task(t):
589
  async with sem:
590
  return await t
 
668
  emb_a = scout.encode([ans_a])
669
  emb_b = scout.encode([ans_b])
670
  sim = float(cosine_similarity(emb_a, emb_b)[0][0])
671
+ # High threshold: true factual alignment requires high topical and directional agreement
672
+ if sim > 0.70:
673
  match_status = "Accounts Align"
 
 
674
  else:
675
+ match_status = "Event details do not align"
676
  except Exception:
677
  match_status = "Accounts Align" if ans_a.strip().lower() == ans_b.strip().lower() else "Event details do not align"
678
  else: