nyayasetu / src /court /opposing.py
CaffeinatedCoding's picture
Upload folder using huggingface_hub
9b38acd verified
"""
Opposing Counsel Agent.
Adversarial. Strategic. Never helps the user.
Three trap types:
1. Admission trap — phrase a statement to elicit a damaging concession
2. Precedent trap — cite a case that superficially sounds helpful but supports opposition
3. Internal inconsistency trap — catch the user contradicting themselves
The opposing counsel reads everything the user has researched via the case brief.
This is what makes the simulation genuinely adversarial.
Difficulty levels change the aggressiveness:
- moot: measured, educational, somewhat forgiving
- standard: sharp, strategic, exploits weaknesses
- adversarial: ruthless, traps constantly, gives no quarter
"""
import logging
import re
from typing import Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
# ── Difficulty-specific personality modifiers ──────────────────
DIFFICULTY_MODIFIERS = {
"moot": """
You are firm but educational in your opposition. While you argue against the user,
you allow them to recover from weak arguments without immediately exploiting every gap.
Your goal is to challenge, not to destroy.""",
"standard": """
You are sharp and strategic. You exploit weaknesses directly.
You set traps when opportunities arise.
You cite contradictions when you spot them.
You are a formidable opponent but a realistic one.""",
"adversarial": """
You are ruthless. You exploit every weakness immediately.
You set traps constantly. You never let a concession pass unexploited.
You cite every contradiction. You are what a top SC senior advocate
looks like at their most aggressive. The user will have to fight for every inch.""",
}
OPPOSING_SYSTEM_PROMPT = """You are opposing counsel in an Indian Supreme Court moot court simulation.
YOUR ROLE:
You argue AGAINST the user's position. You work FOR the opposing party.
Your job is to WIN — find the angle, exploit the weakness, set the trap.
YOUR PERSONALITY:
- You are a senior advocate with 20+ years at the Supreme Court bar
- Sharp, prepared, slightly aggressive
- You have read every case the user researched (their Case Brief is your preparation)
- You know their weaknesses better than they do
- You NEVER inadvertently help the user. Every sentence advances your client's case.
YOUR LANGUAGE:
- Address the bench as "My Lords" or "Hon'ble Court"
- Address user as "My learned friend" with a slightly dismissive edge
- Use phrases like "With respect, my learned friend's submission is misconceived...",
"The settled position in law, as Your Lordships are aware, is...",
"My learned friend has conveniently overlooked..."
- Cite cases with their full citation when possible: "(2017) 10 SCC 1"
YOUR THREE WEAPONS:
1. DIRECT COUNTER: State the opposing legal position clearly with authority
2. CITATION COUNTER: Cite a case that directly contradicts the user's position
3. TRAP: Set one of the three trap types when the opportunity arises
TRAP TYPES (use strategically, not every turn):
- ADMISSION TRAP: Make a statement that sounds reasonable but forces a damaging concession if agreed to
Example: "My Lords, surely my learned friend would not dispute that the right in question is subject to reasonable restrictions?"
- PRECEDENT TRAP: Cite a case that sounds helpful to the user but actually supports you when read carefully
Example: Cite Puttaswamy but focus on the proportionality test which the user's case fails
- INCONSISTENCY TRAP: If user has contradicted themselves across rounds, call it out explicitly
Example: "My Lords, in Round 2 my learned friend submitted X. Now my learned friend submits Y. These positions are irreconcilable."
RESPONSE LENGTH:
Keep your counter-argument to 4-6 sentences. Courtroom arguments are precise, not lengthy.
End with either a direct statement OR a trap question — not both.
IMPORTANT: This is a simulation. You are not providing legal advice."""
def build_opposing_prompt(
session: Dict,
user_argument: str,
retrieved_context: str,
trap_opportunity: Optional[str] = None,
) -> List[Dict]:
"""
Build the messages list for opposing counsel LLM call.
The opposing counsel sees:
- Full case brief (preserved throughout)
- All documents filed (critical!)
- All concessions made (critical!)
- Full recent transcript
- User's latest argument
- Retrieved precedents to use against user
- Any detected trap opportunities
- Trap history
"""
difficulty = session.get("difficulty", "standard")
difficulty_modifier = DIFFICULTY_MODIFIERS.get(difficulty, DIFFICULTY_MODIFIERS["standard"])
# The retrieved_context now contains EVERYTHING from _build_full_context
# including case brief, documents, concessions, and precedents
# This is the complete informational context
trap_instruction = ""
if trap_opportunity:
trap_instruction = f"\n🎯 TRAP OPPORTUNITY DETECTED: {trap_opportunity}\nConsider exploiting this in your response."
user_content = f"""COMPLETE SESSION CONTEXT (use all of this):
{retrieved_context[:4000]}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USER'S LATEST ARGUMENT (respond to this specifically):
{user_argument}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CRITICAL REMINDERS:
1. You have full access to all documents filed by the user
2. You have the complete history of the user's arguments
3. You have all concessions made — exploit them mercilessly
4. You have seen all trap attempts before — use what worked
5. You know their case brief — you know their weaknesses
Round {session.get('current_round', 1)} of {session.get('max_rounds', 5)}.
{trap_instruction}
{difficulty_modifier}
Now respond as opposing counsel. Counter this argument."""
return [
{"role": "system", "content": OPPOSING_SYSTEM_PROMPT},
{"role": "user", "content": user_content}
]
def build_cross_examination_prompt(
session: Dict,
question_number: int,
retrieved_context: str,
) -> List[Dict]:
"""
Build prompt for cross-examination phase.
Opposing counsel asks pointed questions, not arguments.
"""
system = f"""You are opposing counsel conducting cross-examination in an Indian Supreme Court moot court.
This is Question {question_number} of 3 in your cross-examination.
YOUR OBJECTIVE:
Ask ONE precise question that:
1. Forces the user to admit a weakness in their case, OR
2. Challenges the factual basis of their position, OR
3. Sets up an admission you can use in your closing argument
CROSS-EXAMINATION RULES:
- Ask only closed questions (yes/no or specific fact questions)
- Never ask open-ended questions that let them explain freely
- Build from previous answers — each question should box them in further
- Your question must be specific, not general
Format: One sentence question only. No preamble.
{DIFFICULTY_MODIFIERS.get(session.get('difficulty', 'standard'), '')}"""
case_brief = session.get("case_brief", "")
concessions = _format_concessions(session.get("concessions", []))
transcript = _get_recent_transcript(session, last_n=8)
user_content = f"""CASE BRIEF:
{case_brief[:800]}
RECENT TRANSCRIPT:
{transcript}
{concessions}
RELEVANT AUTHORITIES:
{retrieved_context[:1000] if retrieved_context else "Use general legal knowledge."}
This is cross-examination Question {question_number} of 3.
Ask your most damaging question for this position in the examination."""
return [
{"role": "system", "content": system},
{"role": "user", "content": user_content}
]
def build_opposing_closing_prompt(session: Dict) -> List[Dict]:
"""Build prompt for opposing counsel's closing argument."""
system = """You are opposing counsel delivering your closing argument.
This is your opportunity to:
1. Summarise the strongest 3 arguments you made
2. Point out what the user FAILED to establish
3. Highlight every concession they made
4. Tell the court why they should rule in your client's favour
Length: 6-8 sentences. Formal. Decisive. Leave no doubt.
End with: "For these reasons, we respectfully submit that [the petition/the appeal] be [dismissed/allowed]." """
case_brief = session.get("case_brief", "")
concessions = _format_concessions(session.get("concessions", []))
transcript_summary = _get_full_transcript_summary(session)
user_content = f"""CASE BRIEF:
{case_brief[:800]}
COMPLETE TRANSCRIPT SUMMARY:
{transcript_summary[:2000]}
{concessions}
Deliver your closing argument."""
return [
{"role": "system", "content": system},
{"role": "user", "content": user_content}
]
def detect_trap_opportunity_llm(
user_argument: str,
session: Dict,
) -> Optional[Tuple[str, str]]:
"""
Use LLM to detect trap opportunities semantically.
More sophisticated than keyword matching — understands legal logic.
Falls back to keyword detection if LLM fails.
Returns (trap_type, description) or None.
"""
try:
from src.llm import call_llm_raw
import json
import re
# Build minimal context for trap analysis
case_brief = session.get("case_brief", "")[:500]
concessions = session.get("concessions", [])
user_args = session.get("user_arguments", [])
concessions_text = ""
if concessions:
concessions_text = "Previous concessions by user: " + "; ".join(
[c.get("exact_quote", "")[:60] for c in concessions[-3:]]
)
previous_args_text = ""
if len(user_args) >= 2:
previous_args_text = "User argued in Round 1: " + user_args[0].get("text", "")[:150]
system_prompt = """You are a legal trap detector for a moot court simulation.
Analyze if the user's argument contains a trap opportunity for opposing counsel.
Return ONLY valid JSON:
{
"trap_found": true/false,
"trap_type": "admission_trap|precedent_trap|inconsistency_trap|none",
"description": "brief description of the trap"
}
Trap types:
- admission_trap: User made an absolute claim (e.g., "no exceptions", "unlimited", "cannot be restricted")
- precedent_trap: User cited a case that actually supports opposition when read carefully
- inconsistency_trap: User contradicted their own previous argument
"""
user_prompt = f"""Case brief: {case_brief}
{concessions_text}
{previous_args_text}
User's latest argument: {user_argument}
Detect trap opportunity. Return ONLY JSON."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = call_llm_raw(messages)
# Extract JSON from response
match = re.search(r'\{.*\}', response, re.DOTALL)
if match:
data = json.loads(match.group())
if data.get("trap_found"):
trap_type = data.get("trap_type", "admission_trap")
if trap_type != "none":
return (trap_type, data.get("description", "Trap detected"))
except Exception as e:
logger.debug(f"LLM trap detection failed, falling back to keyword: {e}")
# Fall back to keyword-based detection
return detect_trap_opportunity(user_argument, session.get("user_arguments", []), session)
def detect_trap_opportunity(
user_argument: str,
previous_arguments: List[Dict],
session: Dict,
) -> Optional[Tuple[str, str]]:
"""
Analyse user's argument to detect trap opportunities (keyword-based fallback).
Returns (trap_type, description) or None.
"""
arg_lower = user_argument.lower()
# ── Check for admission trap opportunities ─────────────────
# User makes absolute claims that can be challenged
absolute_markers = [
("absolute right", "admission_trap", "User claims absolute right — trap: agree no right is absolute"),
("always", "admission_trap", "User uses 'always' — trap: get them to admit exceptions exist"),
("cannot be restricted", "admission_trap", "User claims right cannot be restricted — trap: Article 19(2) reasonable restrictions"),
("unlimited", "admission_trap", "User claims unlimited right — trap: all rights have limits"),
("no exception", "admission_trap", "User claims no exception — trap: every rule has exceptions"),
]
for marker, trap_type, description in absolute_markers:
if marker in arg_lower:
return (trap_type, description)
# ── Check for internal inconsistency ──────────────────────
if len(previous_arguments) >= 2:
inconsistency = _detect_inconsistencies(previous_arguments + [{
"round": session.get("current_round", 0),
"text": user_argument,
"key_claims": [],
}])
if inconsistency:
return ("inconsistency_trap", inconsistency)
return None
def _detect_inconsistencies(user_arguments: List[Dict]) -> Optional[str]:
"""
Simple rule-based inconsistency detector.
Checks for contradictory claims across rounds.
"""
if len(user_arguments) < 2:
return None
# Pairs of contradictory markers
contradiction_pairs = [
(["not guilty", "innocent", "no offence"], ["committed", "did take", "admitted"]),
(["no consent required", "no permission needed"], ["consent was given", "permission was obtained"]),
(["fundamental right", "absolute"], ["subject to restriction", "can be limited"]),
(["no notice", "without notice"], ["notice was given", "was informed"]),
(["private party", "private company"], ["government", "state", "public authority"]),
]
all_texts = [a["text"].lower() for a in user_arguments]
for positive_markers, negative_markers in contradiction_pairs:
found_positive_round = None
found_negative_round = None
for i, text in enumerate(all_texts):
if any(m in text for m in positive_markers):
found_positive_round = i
if any(m in text for m in negative_markers):
found_negative_round = i
if found_positive_round is not None and found_negative_round is not None:
if found_positive_round != found_negative_round:
pos_arg = user_arguments[found_positive_round]
neg_arg = user_arguments[found_negative_round]
return (
f"Round {pos_arg['round']}: user argued position A. "
f"Round {neg_arg['round']}: user argued contradictory position B. "
f"These cannot coexist."
)
return None
def _format_concessions(concessions: List[Dict]) -> str:
if not concessions:
return ""
lines = ["CONCESSIONS ALREADY MADE (exploit these):"]
for c in concessions:
lines.append(f" Round {c['round_number']}: \"{c['exact_quote'][:100]}\"")
return "\n".join(lines)
def _get_recent_transcript(session: Dict, last_n: int = 4) -> str:
transcript = session.get("transcript", [])
recent = transcript[-last_n:] if len(transcript) > last_n else transcript
lines = []
for entry in recent:
lines.append(f"{entry['role_label'].upper()}: {entry['content'][:250]}")
lines.append("")
return "\n".join(lines) if lines else "No transcript yet."
def _get_full_transcript_summary(session: Dict) -> str:
transcript = session.get("transcript", [])
if not transcript:
return "No transcript."
rounds = {}
for entry in transcript:
r = entry.get("round_number", 0)
if r not in rounds:
rounds[r] = []
rounds[r].append(f"{entry['role_label']}: {entry['content'][:200]}")
lines = []
for round_num in sorted(rounds.keys()):
lines.append(f"--- Round {round_num} ---")
lines.extend(rounds[round_num])
lines.append("")
return "\n".join(lines)