Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions legal_warroom/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ANTHROPIC_API_KEY=sk-ant-...
225 changes: 225 additions & 0 deletions legal_warroom/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
Autonomous Legal War Game — CLI Entry Point

Usage examples:

# Anthropic (cloud) — full simulation, 3 rounds per segment
python main.py agreement.pdf

# Ollama (local, free) — default model llama3.1:8b
python main.py agreement.pdf --provider ollama

# Ollama with a specific model + more rounds
python main.py agreement.pdf --provider ollama --model qwen2.5:14b --rounds 4

# Mix providers: Plaintiff on Ollama, Defense on Anthropic
python main.py agreement.pdf \\
--plaintiff-provider ollama --plaintiff-model qwen2.5:14b \\
--defense-provider anthropic

# Parallel + HTML report
python main.py agreement.pdf --parallel --html

# Dry run: only first 2 segments
python main.py agreement.pdf --max-segments 2

# Full help
python main.py --help

Ollama setup:
1. Install Ollama: https://ollama.com
2. Pull a model: ollama pull qwen2.5:14b
3. Run: python main.py agreement.pdf --provider ollama --model qwen2.5:14b

Recommended Ollama models (best → fastest):
qwen2.5:14b — best local quality for legal reasoning (~9GB)
qwen2.5:7b — good quality (~5GB)
llama3.1:8b — solid baseline (~5GB)
llama3.2:3b — fastest, lower quality (~2GB)
"""

from __future__ import annotations

from pathlib import Path
from typing import Optional

import typer
from dotenv import load_dotenv
from rich.console import Console

from warroom import orchestrator
from warroom.providers.base import make_provider
from warroom.report import generator

load_dotenv()
app = typer.Typer(add_completion=False, rich_markup_mode="rich")
console = Console()


@app.command()
def main(
document: str = typer.Argument(
..., help="Path to the legal document (.pdf or .txt)"
),
# ── Provider shortcuts (same provider for both agents) ─────────────────
provider: str = typer.Option(
"anthropic",
"--provider", "-p",
help="Backend for both agents: 'anthropic' (cloud) or 'ollama' (local).",
),
model: Optional[str] = typer.Option(
None,
"--model", "-m",
help=(
"Model to use. Defaults: anthropic=claude-opus-4-6, ollama=llama3.1:8b. "
"Override: --model qwen2.5:14b"
),
),
# ── Fine-grained per-agent provider overrides ──────────────────────────
plaintiff_provider: Optional[str] = typer.Option(
None, "--plaintiff-provider",
help="Provider override for the Red Team agent.",
),
plaintiff_model: Optional[str] = typer.Option(
None, "--plaintiff-model",
help="Model override for the Red Team agent.",
),
defense_provider: Optional[str] = typer.Option(
None, "--defense-provider",
help="Provider override for the Blue Team agent.",
),
defense_model: Optional[str] = typer.Option(
None, "--defense-model",
help="Model override for the Blue Team agent.",
),
# ── Ollama config ──────────────────────────────────────────────────────
ollama_url: str = typer.Option(
"http://localhost:11434/v1",
"--ollama-url",
help="Ollama API base URL.",
),
# ── Simulation config ──────────────────────────────────────────────────
rounds: int = typer.Option(
3, "--rounds", "-r",
help="Max adversarial rounds per segment (default 3).",
),
convergence: int = typer.Option(
2, "--convergence",
help=(
"Stop iterating when max severity drops to this level or below "
"(after ≥2 rounds). Default 2."
),
),
words: int = typer.Option(
800, "--words", "-w",
help="Soft word-count cap per segment (default 800).",
),
parallel: bool = typer.Option(
False, "--parallel",
help="Process segments concurrently (faster, higher API concurrency).",
),
workers: int = typer.Option(
3, "--workers",
help="Max parallel threads when --parallel is set.",
),
# ── Output config ──────────────────────────────────────────────────────
output_dir: str = typer.Option(
"output", "--output", "-o",
help="Directory for report files.",
),
html: bool = typer.Option(
False, "--html",
help="Also generate a self-contained HTML report.",
),
max_segments: Optional[int] = typer.Option(
None, "--max-segments",
help="Limit to the first N segments (useful for dry-runs).",
),
) -> None:
"""
[bold cyan]Autonomous Legal War Game[/bold cyan] — M&A contract stress-testing.

Runs a [bold red]Plaintiff Agent (Red Team)[/bold red] against a
[bold green]Defense Agent (Blue Team)[/bold green] across multiple rounds
per clause. Each round, the Plaintiff re-attacks the Defense's latest
hardened rewrite until convergence or max rounds is reached.

Supports [bold]Anthropic (cloud)[/bold] and [bold]Ollama (local, free)[/bold].
"""
doc_path = Path(document)
if not doc_path.exists():
console.print(f"[bold red]Error:[/bold red] File not found: {doc_path}")
raise typer.Exit(code=1)

# ── Build providers ──────────────────────────────────────────────────
# Per-agent overrides take precedence; fall back to --provider / --model
p_provider = plaintiff_provider or provider
p_model = plaintiff_model or model
d_provider = defense_provider or provider
d_model = defense_model or model

try:
pp = _build_provider(p_provider, p_model, ollama_url)
dp = _build_provider(d_provider, d_model, ollama_url)
except (ValueError, ImportError) as exc:
console.print(f"[bold red]Provider error:[/bold red] {exc}")
raise typer.Exit(code=1)

# ── Run simulation ────────────────────────────────────────────────────
reports = orchestrator.run_simulation(
document_path=str(doc_path),
plaintiff_provider=pp,
defense_provider=dp,
words_per_segment=words,
max_rounds=rounds,
convergence_threshold=convergence,
parallel=parallel,
max_workers=workers,
)

if max_segments is not None:
reports = reports[:max_segments]

if not reports:
console.print("[yellow]No segments produced. Check your document.[/yellow]")
raise typer.Exit(code=1)

# ── Output ────────────────────────────────────────────────────────────
generator.print_terminal_summary(reports)

out_dir = Path(output_dir)
stem = doc_path.stem
generator.save_json(reports, out_dir / f"{stem}_warroom_report.json")

if html:
generator.save_html(reports, out_dir / f"{stem}_warroom_report.html")

# Exit code 2 signals CRITICAL findings (useful in CI / review pipelines)
has_critical = any(r.status == "CRITICAL" for r in reports)
if has_critical:
console.print(
"\n[bold red]⚠ CRITICAL vulnerabilities remain.[/bold red] "
"Manual legal review required before proceeding."
)
raise typer.Exit(code=2)

console.print(
"\n[bold green]✓ Simulation complete.[/bold green] "
f"Reports in [cyan]{out_dir}/[/cyan]"
)


def _build_provider(name: str, model: Optional[str], ollama_url: str):
"""Build a provider, injecting the Ollama URL when needed."""
if name.lower() == "ollama":
from warroom.providers.ollama_p import OllamaProvider
return OllamaProvider(
model=model or "llama3.1:8b",
base_url=ollama_url,
)
return make_provider(name, model)


if __name__ == "__main__":
app()
8 changes: 8 additions & 0 deletions legal_warroom/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
anthropic>=0.40.0
openai>=1.0.0 # Required for Ollama provider (OpenAI-compatible API)
pydantic>=2.0.0
pdfplumber>=0.10.0
pypdf>=4.0.0
rich>=13.0.0
typer>=0.12.0
python-dotenv>=1.0.0
Empty file.
Empty file.
99 changes: 99 additions & 0 deletions legal_warroom/warroom/agents/defense.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Defense Agent — Blue Team

Receives the clause under attack and the Plaintiff's analysis, then
returns a hardened rewrite. Accepts any LLMProvider.
"""

from __future__ import annotations

from ..models.schemas import DefenseAnalysis, PlaintiffAnalysis
from ..providers.base import LLMProvider

DEFENSE_SYSTEM = """\
You are the Defense Counsel Agent (Blue Team) and lead drafter for the \
acquiring party in a high-stakes Mergers & Acquisitions transaction.

JURISDICTION: Standard US corporate law, contract law precedents, and Delaware \
Court of Chancery standards.

OBJECTIVE: Fortify the contract against every vulnerability identified by the \
Plaintiff Agent while preserving the original business intent of the deal.

EXECUTION DIRECTIVES:
1. PRECISION REDRAFTING — Rewrite exploited clauses with absolute semantic \
precision. Close every loophole in the Plaintiff's attack report.
2. RISK MITIGATION — Inject:
• Exact numeric definitions (no vague qualifiers without explicit anchors)
• Explicit liability caps with stated carve-outs
• Severability and savings clauses where appropriate
• Clear governing law and exclusive jurisdiction provisions
• Knowledge qualifiers only where commercially necessary, with defined \
Knowledge Persons
• No "and/or" — use "and" or "or" explicitly
3. INTENT PRESERVATION — Do NOT alter the underlying financial or operational \
agreement. Only alter the legal execution of that agreement. If a business \
term cannot be hardened without changing its substance, flag it in \
residual_risk.
4. DRAFTING STANDARDS — Formal contract English, active voice preferred, \
sequential sub-clause numbering, all new terms defined inline.

If this is a re-hardening in round 2+: also address any new vulnerabilities \
the Plaintiff found in your previous rewrite.
"""


def run(
provider: LLMProvider,
clause_text: str,
plaintiff_analysis: PlaintiffAnalysis,
segment_id: str,
round_number: int = 1,
) -> DefenseAnalysis:
"""
Harden a clause against the Plaintiff's attack and return DefenseAnalysis.

Args:
provider: Any LLMProvider (Anthropic, Ollama, …).
clause_text: The clause being defended (may be a prior hardened rewrite).
plaintiff_analysis: Output from the Plaintiff Agent this round.
segment_id: Identifier used for logging.
round_number: Current round number.
"""
attack_summary = _format_attack_vectors(plaintiff_analysis)

user_message = (
f"[SEGMENT: {segment_id} | ROUND: {round_number}]\n\n"
f"━━━ CLAUSE TO HARDEN ━━━\n"
f"{clause_text}\n\n"
f"━━━ PLAINTIFF ATTACK REPORT ━━━\n"
f"{attack_summary}\n\n"
"Produce your defense report with fully hardened clause language."
)

return provider.complete_structured(
system=DEFENSE_SYSTEM,
messages=[{"role": "user", "content": user_message}],
schema=DefenseAnalysis,
max_tokens=12288,
)


def _format_attack_vectors(analysis: PlaintiffAnalysis) -> str:
lines = [
f"Executive Summary: {analysis.executive_summary}",
f"Highest Severity: {analysis.highest_severity}/5",
"",
"Attack Vectors (highest severity first):",
]
for i, v in enumerate(analysis.attack_vectors, 1):
lines += [
f"\n[{i}] {v.title}",
f" Severity: {v.severity}/5 ({v.vulnerability_type})",
f" Clause Ref: {v.clause_reference}",
f" Description: {v.description}",
f" Legal Theory:{v.legal_theory}",
f" Scenario: {v.exploitation_scenario}",
f" Exposure: {v.estimated_exposure}",
]
return "\n".join(lines)
Loading