Spaces:
Running
Running
File size: 8,474 Bytes
5d959d0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | """
Summariser Agent.
Only appears at the end of the session. Never speaks during the hearing.
Reads the entire transcript and produces the structured analysis document.
This is the most valuable output of the entire moot court system —
the thing that makes users want to come back and practice again.
The analysis must be:
- Brutally honest about what the user did wrong
- Specific about what they could have done differently
- Encouraging enough that they want to try again
- Precise enough to be actually useful for preparation
"""
import logging
from typing import Dict, List
logger = logging.getLogger(__name__)
SUMMARISER_SYSTEM_PROMPT = """You are a senior legal trainer analysing a moot court simulation.
YOUR ROLE:
You have watched the complete hearing. Now you produce a comprehensive, honest analysis
that will help the user improve their advocacy skills.
YOUR PERSONALITY:
- Clinical and precise. Not harsh, not gentle — just honest.
- Deeply knowledgeable about Indian law and courtroom procedure.
- You care about the user's development. Honest feedback serves them better than false praise.
- You name specific moments, not generalities.
YOUR ANALYSIS STRUCTURE:
Produce the analysis in this EXACT format using these EXACT section headers:
## OVERALL ASSESSMENT
[2 sentences: overall performance and predicted outcome]
## PERFORMANCE SCORE
[Single number X.X/10 with one sentence justification]
## OUTCOME PREDICTION
[ALLOWED / DISMISSED / PARTLY ALLOWED / REMANDED with one sentence reasoning]
## STRONGEST ARGUMENTS
[Number each argument. For each: what was argued, why it was effective, which precedent supported it]
## WEAKEST ARGUMENTS
[Number each. For each: what was argued, exactly why it failed, what should have been argued instead]
## CONCESSIONS ANALYSIS
[For each concession: exact quote, what it conceded legally, how opposing counsel could exploit it, how to avoid making this concession]
## TRAP ANALYSIS
[For each trap event: what the trap was, whether you fell in, what the correct response was]
## WHAT THE JUDGE WAS SIGNALLING
[Translate each judicial question into plain language: what weakness it was probing]
## MISSED OPPORTUNITIES
[Arguments you should have made but didn't, with specific precedents you should have cited]
## PREPARATION RECOMMENDATIONS
[3-5 specific, actionable recommendations for what to research and prepare before a real hearing]
## FULL TRANSCRIPT
[The complete verbatim transcript formatted as a court record]
Be specific. Name rounds, cite exact quotes, reference specific cases.
Generic feedback is useless. Specific feedback is gold."""
def build_summariser_prompt(session: Dict) -> List[Dict]:
"""
Build the messages list for the summariser LLM call.
The summariser gets everything:
- Complete transcript
- All concessions
- All trap events
- Case brief
- Retrieved precedents used
"""
transcript = _format_full_transcript(session)
concessions = _format_concessions_detailed(session.get("concessions", []))
trap_events = _format_trap_events(session.get("trap_events", []))
user_arguments = _format_user_arguments(session.get("user_arguments", []))
user_content = f"""COMPLETE SESSION DATA FOR ANALYSIS:
Case: {session.get('case_title', '')}
User argued as: {session.get('user_side', '').upper()}
Difficulty: {session.get('difficulty', 'standard')}
Rounds completed: {session.get('current_round', 0)} of {session.get('max_rounds', 5)}
CASE BRIEF:
{session.get('case_brief', 'No brief available.')[:800]}
LEGAL ISSUES:
{', '.join(session.get('legal_issues', []))}
COMPLETE TRANSCRIPT:
{transcript}
USER'S ARGUMENTS (extracted):
{user_arguments}
{concessions}
{trap_events}
Now produce the complete session analysis following the exact format specified."""
return [
{"role": "system", "content": SUMMARISER_SYSTEM_PROMPT},
{"role": "user", "content": user_content}
]
def parse_analysis(raw_analysis: str, session: Dict) -> Dict:
"""
Parse the summariser's raw output into a structured dict.
Used by the frontend to display individual sections.
"""
import re
sections = {
"overall_assessment": "",
"performance_score": 0.0,
"outcome_prediction": "unknown",
"strongest_arguments": [],
"weakest_arguments": [],
"concessions_analysis": [],
"trap_analysis": [],
"judge_signals": "",
"missed_opportunities": [],
"preparation_recommendations": [],
"full_transcript": "",
"raw_analysis": raw_analysis,
}
# Extract performance score
score_match = re.search(r'(\d+\.?\d*)\s*/\s*10', raw_analysis)
if score_match:
try:
sections["performance_score"] = float(score_match.group(1))
except Exception:
pass
# Extract outcome prediction
for outcome in ["ALLOWED", "DISMISSED", "PARTLY ALLOWED", "REMANDED"]:
if outcome in raw_analysis.upper():
sections["outcome_prediction"] = outcome.lower().replace(" ", "_")
break
# Extract sections by header
header_map = {
"OVERALL ASSESSMENT": "overall_assessment",
"WHAT THE JUDGE WAS SIGNALLING": "judge_signals",
"FULL TRANSCRIPT": "full_transcript",
}
for header, key in header_map.items():
pattern = rf'##\s+{header}\s*\n(.*?)(?=##|\Z)'
match = re.search(pattern, raw_analysis, re.DOTALL | re.IGNORECASE)
if match:
sections[key] = match.group(1).strip()
# Extract list sections
list_sections = {
"STRONGEST ARGUMENTS": "strongest_arguments",
"WEAKEST ARGUMENTS": "weakest_arguments",
"MISSED OPPORTUNITIES": "missed_opportunities",
"PREPARATION RECOMMENDATIONS": "preparation_recommendations",
}
for header, key in list_sections.items():
pattern = rf'##\s+{header}\s*\n(.*?)(?=##|\Z)'
match = re.search(pattern, raw_analysis, re.DOTALL | re.IGNORECASE)
if match:
content = match.group(1).strip()
# Split into numbered items
items = re.split(r'\n\d+\.', content)
sections[key] = [item.strip() for item in items if item.strip()]
return sections
def _format_full_transcript(session: Dict) -> str:
"""Format complete transcript for analysis."""
transcript = session.get("transcript", [])
if not transcript:
return "No transcript available."
lines = []
current_round = None
for entry in transcript:
round_num = entry.get("round_number", 0)
if round_num != current_round:
current_round = round_num
lines.append(f"\n--- ROUND {round_num} | {entry.get('phase', '').upper()} ---\n")
lines.append(f"{entry['role_label'].upper()}")
lines.append(entry["content"])
lines.append("")
return "\n".join(lines)
def _format_concessions_detailed(concessions: List[Dict]) -> str:
if not concessions:
return "CONCESSIONS: None recorded."
lines = ["CONCESSIONS MADE BY USER:"]
for i, c in enumerate(concessions, 1):
lines.append(f"{i}. Round {c['round_number']}")
lines.append(f" Quote: \"{c['exact_quote']}\"")
lines.append(f" Legal significance: {c['legal_significance']}")
lines.append("")
return "\n".join(lines)
def _format_trap_events(trap_events: List[Dict]) -> str:
if not trap_events:
return "TRAPS: None set."
lines = ["TRAP EVENTS:"]
for i, t in enumerate(trap_events, 1):
fell = "USER FELL INTO TRAP" if t.get("user_fell_in") else "User avoided trap"
lines.append(f"{i}. Round {t['round_number']} | {t['trap_type']} | {fell}")
lines.append(f" Trap: {t['trap_text'][:200]}")
if t.get("user_response"):
lines.append(f" Response: {t['user_response'][:200]}")
lines.append("")
return "\n".join(lines)
def _format_user_arguments(user_arguments: List[Dict]) -> str:
if not user_arguments:
return "No user arguments recorded."
lines = []
for arg in user_arguments:
lines.append(f"Round {arg['round']}: {arg['text'][:300]}")
if arg.get("key_claims"):
lines.append(f" Claims: {', '.join(arg['key_claims'][:3])}")
lines.append("")
return "\n".join(lines)
|