Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -160,19 +160,20 @@ except Exception as e:
|
|
| 160 |
qg_tokenizer = None
|
| 161 |
qg_model = None
|
| 162 |
|
| 163 |
-
# 5. THE ANALYST (
|
| 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-
|
| 168 |
analyst = ChatGroq(
|
| 169 |
temperature=0,
|
| 170 |
model_name=DEFAULT_MODEL,
|
| 171 |
groq_api_key=GROQ_API_KEY,
|
| 172 |
-
request_timeout=
|
|
|
|
| 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,
|
| 228 |
-
3. If
|
| 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
|
| 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
|
| 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": "
|
| 275 |
-
"party_a_fact": "
|
| 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:
|
| 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
|
| 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
|
| 396 |
-
- If Party B denies the occurrence or
|
| 397 |
-
3. DO NOT BE OVERLY RIGID. If
|
| 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,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
|
| 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
|
| 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,
|
| 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 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
analysis_tasks.append(task)
|
| 586 |
|
| 587 |
-
# Throttled execution
|
| 588 |
-
sem = asyncio.Semaphore(
|
| 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 |
-
|
|
|
|
| 673 |
match_status = "Accounts Align"
|
| 674 |
-
elif sim < 0.35:
|
| 675 |
-
match_status = "Event details do not align"
|
| 676 |
else:
|
| 677 |
-
match_status = "
|
| 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:
|