File size: 15,170 Bytes
8c1f582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#!/usr/bin/env python3
"""
Israel Bilateral Relationship Impact Analysis

Analyzes how the Gaza ceasefire resolution affects Israel's bilateral relationships
with each UN member state, categorizing the impact and providing reasoning.

Usage:
    python scripts/analyze_israel_bilateral_impact.py <motion_id> [--sample N]

Example:
    python scripts/analyze_israel_bilateral_impact.py 01_gaza_ceasefire_resolution --sample 5
"""

import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import re

# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))


class BilateralImpactAnalyzer:
    """Analyzes bilateral relationship impacts from UN voting results"""

    # Impact categories
    IMPACT_CATEGORIES = [
        "strengthened_significantly",  # Major improvement in relations
        "strengthened_moderately",     # Noticeable improvement
        "strengthened_slightly",       # Minor improvement
        "neutral",                     # No meaningful change
        "strained_slightly",           # Minor tension
        "strained_moderately",         # Noticeable tension
        "strained_significantly"       # Major deterioration
    ]

    def __init__(self, model: str = "claude-3-5-haiku-20241022"):
        """
        Initialize the analyzer with a fast model for efficiency

        Args:
            model: Model name (default: claude-3-5-haiku for speed)
        """
        self.model = model
        self.project_root = PROJECT_ROOT
        self.reactions_dir = self.project_root / "tasks" / "reactions"
        self.results_dir = self.project_root / "tasks" / "analysis"

        # Load configuration
        self._load_config()
        self._init_ai_client()

    def _load_config(self):
        """Load configuration from environment variables"""
        from dotenv import load_dotenv
        load_dotenv()

        self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
        if not self.anthropic_api_key:
            raise ValueError("ANTHROPIC_API_KEY not found in environment")

    def _init_ai_client(self):
        """Initialize Anthropic AI client"""
        try:
            import anthropic
            self.client = anthropic.Anthropic(api_key=self.anthropic_api_key)
            print(f"βœ“ Initialized Anthropic API client (model: {self.model})")
        except ImportError:
            print("Error: anthropic package not installed. Run: pip install anthropic")
            sys.exit(1)

    def load_voting_results(self, motion_id: str) -> Dict:
        """Load the latest voting results for a motion"""
        results_file = self.reactions_dir / f"{motion_id}_latest.json"

        if not results_file.exists():
            raise FileNotFoundError(
                f"No voting results found for {motion_id}. "
                f"Run the motion simulation first."
            )

        with open(results_file, 'r', encoding='utf-8') as f:
            return json.load(f)

    def analyze_bilateral_impact(self, country_vote: Dict, motion_context: str) -> Dict:
        """
        Analyze how a country's vote affects its bilateral relationship with Israel

        Args:
            country_vote: Dict with country name, vote, and statement
            motion_context: Context about the motion being voted on

        Returns:
            Dict with impact category, reasoning, and confidence
        """

        system_prompt = """You are an expert international relations analyst specializing in Middle East diplomacy and bilateral relationships.

Your task is to analyze how a country's vote on a UN resolution affects its bilateral relationship with Israel. Consider:
- Historical relationship baseline
- Vote alignment or divergence
- Diplomatic tone in statement
- Strategic implications
- Regional dynamics
- Economic/security ties

Be objective and nuanced. Not all "yes" votes strengthen relations equally, and not all "no" votes strain them equally."""

        user_prompt = f"""Analyze how this country's vote affects its bilateral relationship with Israel:

**Motion Context:** {motion_context}

**Country:** {country_vote['country']}
**Vote:** {country_vote['vote'].upper()}
**Statement:** {country_vote['statement']}

Based on this vote and statement, categorize the impact on Israel-{country_vote['country']} bilateral relations.

You must respond with a JSON object containing:
1. "impact_category": Must be exactly one of: {', '.join(self.IMPACT_CATEGORIES)}
2. "reasoning": 2-3 sentences explaining your assessment
3. "confidence": Your confidence level (high/medium/low)
4. "key_factors": Array of 2-4 key factors driving this assessment

Consider:
- Does this vote strengthen or strain the relationship compared to baseline?
- How significant is the impact (slightly/moderately/significantly)?
- What does the statement's tone reveal about the relationship?
- Are there strategic, economic, or security dimensions?

Your response must be valid JSON in this exact format:
{{
  "impact_category": "neutral",
  "reasoning": "Your analysis here.",
  "confidence": "high",
  "key_factors": ["factor 1", "factor 2", "factor 3"]
}}"""

        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=600,
                system=system_prompt,
                messages=[
                    {"role": "user", "content": user_prompt}
                ],
                temperature=0.5
            )
            content = response.content[0].text.strip()

            # Extract JSON from response (handle markdown code blocks and extra text)
            if content.startswith("```"):
                content = re.sub(r'^```(?:json)?\n', '', content)
                content = re.sub(r'\n```$', '', content)

            # Find JSON object boundaries
            start_idx = content.find('{')
            if start_idx == -1:
                raise ValueError("No JSON object found in response")

            # Find matching closing brace
            brace_count = 0
            end_idx = start_idx
            for i in range(start_idx, len(content)):
                if content[i] == '{':
                    brace_count += 1
                elif content[i] == '}':
                    brace_count -= 1
                    if brace_count == 0:
                        end_idx = i + 1
                        break

            json_str = content[start_idx:end_idx]

            # Parse JSON response
            result = json.loads(json_str)

            # Validate response
            required_fields = ["impact_category", "reasoning", "confidence", "key_factors"]
            for field in required_fields:
                if field not in result:
                    raise ValueError(f"Response missing required field: {field}")

            if result["impact_category"] not in self.IMPACT_CATEGORIES:
                raise ValueError(f"Invalid impact category: {result['impact_category']}")

            return result

        except json.JSONDecodeError as e:
            print(f"  ⚠ JSON parse error for {country_vote['country']}: {e}")
            return {
                "impact_category": "neutral",
                "reasoning": "[Error: Unable to parse response]",
                "confidence": "low",
                "key_factors": ["analysis_error"],
                "error": str(e)
            }
        except Exception as e:
            print(f"  ⚠ Error analyzing {country_vote['country']}: {e}")
            return {
                "impact_category": "neutral",
                "reasoning": f"[Error: {str(e)}]",
                "confidence": "low",
                "key_factors": ["analysis_error"],
                "error": str(e)
            }

    def run_analysis(self, motion_id: str, sample_size: Optional[int] = None) -> Dict:
        """
        Run bilateral impact analysis for all countries

        Args:
            motion_id: ID of the motion to analyze
            sample_size: If set, only analyze this many countries (for testing)

        Returns:
            Dict containing all analyses and summary statistics
        """
        print(f"\n{'='*70}")
        print(f"Israel Bilateral Relationship Impact Analysis")
        print(f"Motion: {motion_id}")
        print(f"Model: {self.model}")
        print(f"{'='*70}\n")

        # Load voting results
        voting_results = self.load_voting_results(motion_id)
        print(f"βœ“ Loaded voting results: {voting_results['total_votes']} countries\n")

        # Extract motion context (first 500 chars of motion text for context)
        motion_context = f"Gaza ceasefire resolution - Vote summary: {voting_results['vote_summary']}"

        # Get votes to analyze
        votes = voting_results['votes']
        if sample_size:
            votes = votes[:sample_size]
            print(f"πŸ“Š Analyzing {sample_size} countries (sample mode)\n")
        else:
            print(f"πŸ“Š Analyzing {len(votes)} countries\n")

        # Analyze each country's impact
        analyses = []
        impact_counts = {cat: 0 for cat in self.IMPACT_CATEGORIES}

        for i, vote in enumerate(votes, 1):
            # Skip Israel itself
            if vote['country'].lower() == 'israel':
                print(f"[{i}/{len(votes)}] Skipping Israel (self)...")
                continue

            print(f"[{i}/{len(votes)}] Analyzing {vote['country']}...", end=" ", flush=True)

            impact_analysis = self.analyze_bilateral_impact(vote, motion_context)

            impact_counts[impact_analysis["impact_category"]] += 1

            analyses.append({
                "country": vote['country'],
                "vote": vote['vote'],
                "statement": vote['statement'],
                "impact_analysis": impact_analysis
            })

            # Print impact result with emoji
            impact_emoji = {
                "strengthened_significantly": "πŸ’š",
                "strengthened_moderately": "🟒",
                "strengthened_slightly": "🟑",
                "neutral": "βšͺ",
                "strained_slightly": "🟠",
                "strained_moderately": "πŸ”΄",
                "strained_significantly": "πŸ”₯"
            }
            emoji = impact_emoji.get(impact_analysis["impact_category"], "❓")
            print(f"{emoji} {impact_analysis['impact_category']}")

        # Compile results
        results = {
            "motion_id": motion_id,
            "timestamp": datetime.now().isoformat(),
            "model": self.model,
            "total_analyzed": len(analyses),
            "impact_summary": impact_counts,
            "analyses": analyses,
            "metadata": {
                "voting_summary": voting_results['vote_summary'],
                "original_votes": voting_results['total_votes']
            }
        }

        # Print summary
        print(f"\n{'='*70}")
        print(f"Impact Summary:")
        total = len(analyses)
        for category in self.IMPACT_CATEGORIES:
            count = impact_counts[category]
            pct = (count/total*100) if total > 0 else 0
            print(f"  {category:30s}: {count:3d} ({pct:5.1f}%)")
        print(f"{'='*70}\n")

        return results

    def save_results(self, results: Dict):
        """Save analysis results to file"""
        # Create results directory if it doesn't exist
        self.results_dir.mkdir(parents=True, exist_ok=True)

        # Generate filename with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{results['motion_id']}_israel_bilateral_impact_{timestamp}.json"
        filepath = self.results_dir / filename

        # Save results
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(results, f, indent=2, ensure_ascii=False)

        print(f"βœ“ Results saved to: {filepath}")

        # Also create/update a "latest" version
        latest_filepath = self.results_dir / f"{results['motion_id']}_israel_bilateral_impact_latest.json"
        with open(latest_filepath, 'w', encoding='utf-8') as f:
            json.dump(results, f, indent=2, ensure_ascii=False)

        print(f"βœ“ Latest results: {latest_filepath}")

        # Generate CSV export for easy analysis
        self._export_csv(results, self.results_dir / f"{results['motion_id']}_israel_bilateral_impact.csv")

    def _export_csv(self, results: Dict, filepath: Path):
        """Export results to CSV format"""
        import csv

        with open(filepath, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)

            # Header
            writer.writerow([
                "Country",
                "Vote",
                "Impact Category",
                "Confidence",
                "Reasoning",
                "Key Factors"
            ])

            # Data rows
            for analysis in results['analyses']:
                impact = analysis['impact_analysis']
                writer.writerow([
                    analysis['country'],
                    analysis['vote'],
                    impact['impact_category'],
                    impact['confidence'],
                    impact['reasoning'],
                    '; '.join(impact['key_factors'])
                ])

        print(f"βœ“ CSV export: {filepath}")


def main():
    parser = argparse.ArgumentParser(
        description="Analyze how Gaza ceasefire vote affects Israel's bilateral relationships",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Run full analysis
  python scripts/analyze_israel_bilateral_impact.py 01_gaza_ceasefire_resolution

  # Test with sample
  python scripts/analyze_israel_bilateral_impact.py 01_gaza_ceasefire_resolution --sample 5

  # Use different model
  python scripts/analyze_israel_bilateral_impact.py 01_gaza_ceasefire_resolution --model claude-3-5-sonnet-20241022
        """
    )

    parser.add_argument(
        "motion_id",
        help="ID of the motion to analyze (e.g., 01_gaza_ceasefire_resolution)"
    )

    parser.add_argument(
        "--sample",
        type=int,
        help="Only analyze N countries (for testing)"
    )

    parser.add_argument(
        "--model",
        default="claude-3-5-haiku-20241022",
        help="Model to use (default: claude-3-5-haiku for speed)"
    )

    args = parser.parse_args()

    # Run analysis
    try:
        analyzer = BilateralImpactAnalyzer(model=args.model)
        results = analyzer.run_analysis(args.motion_id, sample_size=args.sample)
        analyzer.save_results(results)

        print("\nβœ“ Bilateral impact analysis complete!")

    except FileNotFoundError as e:
        print(f"\n❌ Error: {e}", file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n\n⚠ Analysis interrupted by user")
        sys.exit(130)
    except Exception as e:
        print(f"\n❌ Unexpected error: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()