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)