AI Automation & Agents  

AI Agents in Practice: Legal Document Review & Contracting Agent

Introduction

This sixth AI agent is designed for a critical business function: Legal Document Review & Contracting. This agent reads legal documents such as contracts, identifies key clauses (e.g., confidentiality, payment terms, termination rights), checks for compliance with internal policies, and proposes edits or approvals. It ensures that the contract language adheres to guidelines set by the legal team, avoiding vague terms or conflicting clauses. The agent never claims success unless actions are executed, and it always tracks changes, offering receipts for any alterations made.


The Use Case

Legal teams often handle contracts with numerous clauses, terms, and stipulations. A contract review process typically includes scanning for standard terms, checking for specific prohibitions, ensuring that the language matches company policy, and flagging terms that require manual review. The AI agent must perform these tasks and, if all conditions are met, propose edits or even auto-generate parts of the contract while preserving its integrity.


Prompt Contract (agent interface)

# file: contracts/legal_review_v1.yaml
role: "LegalContractAgent"
scope: >
  Review legal documents for compliance, standard clauses, and conflicts.
  Ask once for missing fields (contract_text, contract_type, jurisdiction, clauses).
  Propose tool calls; never assert success unless a receipt is present.
output:
  type: object
  required: [summary, decision, contract_clauses, citations, next_steps, tool_proposals]
  properties:
    summary: {type: string, maxWords: 100}
    decision: {type: string, enum: ["approve","edit","need_approval","reject","need_more_info"]}
    contract_clauses:
      type: array
      items:
        type: object
        required: [clause_name, clause_text, compliance_status]
        properties:
          clause_name: {type: string}
          clause_text: {type: string}
          compliance_status: {type: string, enum: ["compliant", "non-compliant", "needs_review"]}
    citations: {type: array, items: {type: string}}
    next_steps: {type: array, items: {type: string}, maxItems: 6}
    tool_proposals:
      type: array
      items:
        type: object
        required: [name, args, preconditions, idempotency_key]
        properties:
          name: {type: string, enum: [AnalyzeContract, CheckCompliance, SuggestEdits, ApproveContract]}
          args: {type: object}
          preconditions: {type: string}
          idempotency_key: {type: string}
policy_id: "contract_policy.v3"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
  narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
  bullets:   {top_p: 0.82, temperature: 0.45}

Example claims (context given to the model)

[
  {"claim_id":"policy:confidentiality:standard","text":"Confidentiality clause must last at least 2 years after termination.",
   "effective_date":"2025-01-01","source_id":"doc:contract_policy_v3","span":"2 years after termination"},
  {"claim_id":"policy:payment_terms:max_delay","text":"Payment terms must not exceed 60 days from invoice.",
   "effective_date":"2025-01-01","source_id":"doc:contract_policy_v3","span":"60 days from invoice"},
  {"claim_id":"policy:termination:early_notice","text":"Termination requires at least 90 days' notice for both parties.",
   "effective_date":"2025-01-01","source_id":"doc:contract_policy_v3","span":"90 days' notice"}
]

Tool Interfaces (typed, with receipts)

# tools.py
from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import date

class AnalyzeContractArgs(BaseModel):
    contract_text: str
    contract_type: str  # "NDAs", "SaaS", "Employment"
    jurisdiction: str
    clauses: Optional[List[str]] = []

class CheckComplianceArgs(BaseModel):
    clause_name: str
    clause_text: str
    policy_id: str

class SuggestEditsArgs(BaseModel):
    clause_name: str
    clause_text: str
    recommended_changes: str

class ApproveContractArgs(BaseModel):
    contract_id: str
    approver_id: str

class ToolReceipt(BaseModel):
    tool: str
    ok: bool
    ref: str
    message: str = ""
    data: Optional[Dict] = None
# adapters.py  (demo logic)
from tools import *
from datetime import datetime, timedelta

CONTRACTS = {"contract_1234": {"status": "draft", "type": "SaaS", "jurisdiction": "US"}}
CLAUSE_RULES = {
    "confidentiality": {"text": "Confidentiality must last at least 2 years", "compliant": False},
    "payment_terms": {"text": "Payment must be within 60 days", "compliant": True},
    "termination": {"text": "Termination requires 90 days' notice", "compliant": False}
}

def analyze_contract(a: AnalyzeContractArgs) -> ToolReceipt:
    contract = CONTRACTS.get(a.contract_type)
    if not contract:
        return ToolReceipt(tool="AnalyzeContract", ok=False, ref="contract-missing", message="Contract not found")
    
    clauses = []
    for clause_name in CLAUSE_RULES:
        clauses.append({"clause_name": clause_name, "clause_text": CLAUSE_RULES[clause_name]["text"], 
                        "compliance_status": "non-compliant" if not CLAUSE_RULES[clause_name]["compliant"] else "compliant"})
    return ToolReceipt(tool="AnalyzeContract", ok=True, ref=f"contract-{a.contract_type}",
                       message="Contract analyzed", data={"clauses": clauses})

def check_compliance(a: CheckComplianceArgs) -> ToolReceipt:
    rule = CLAUSE_RULES.get(a.clause_name)
    if not rule:
        return ToolReceipt(tool="CheckCompliance", ok=False, ref="clause-not-found", message="Clause not found")
    if a.clause_text != rule["text"]:
        return ToolReceipt(tool="CheckCompliance", ok=False, ref="clause-mismatch", message="Clause text mismatch")
    return ToolReceipt(tool="CheckCompliance", ok=True, ref=f"clause-{a.clause_name}", message="Clause is compliant")

def suggest_edits(a: SuggestEditsArgs) -> ToolReceipt:
    changes = f"Replace with: 'Confidentiality will last for 2 years post-termination'"
    return ToolReceipt(tool="SuggestEdits", ok=True, ref=f"edit-{a.clause_name}", message="Suggested edit", 
                       data={"recommended_changes": changes})

def approve_contract(a: ApproveContractArgs) -> ToolReceipt:
    return ToolReceipt(tool="ApproveContract", ok=True, ref=f"approve-{a.contract_id}", message="Contract approved")

Agent Loop (proposal → verification → execution → receipts)

# agent_legal.py
import uuid, json
from typing import Any, Dict, List
from tools import *
from adapters import *

ALLOWED_TOOLS = {"AnalyzeContract","CheckCompliance","SuggestEdits","ApproveContract"}

def new_idem() -> str:
    return f"idem-{uuid.uuid4()}"

def verify_proposal(p: Dict[str, Any]) -> str:
    required = {"name","args","preconditions","idempotency_key"}
    if not required.issubset(p): return "Missing proposal fields"
    if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
    return ""

def execute(p: Dict[str, Any]) -> ToolReceipt:
    n, a = p["name"], p["args"]
    if n == "AnalyzeContract":   return analyze_contract(AnalyzeContractArgs(**a))
    if n == "CheckCompliance":   return check_compliance(CheckComplianceArgs(**a))
    if n == "SuggestEdits":      return suggest_edits(SuggestEditsArgs(**a))
    if n == "ApproveContract":   return approve_contract(ApproveContractArgs(**a))
    return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")

# --- Model shim returning a plan per contract (replace with your LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], contract_data: Dict[str, Any]) -> Dict[str,Any]:
    return {
      "summary": "Contract reviewed for compliance with internal policies.",
      "decision": "edit",
      "contract_clauses": [
        {"clause_name": "confidentiality", "clause_text": "Confidentiality must last at least 2 years", 
         "compliance_status": "non-compliant"},
        {"clause_name": "payment_terms", "clause_text": "Payment must be within 60 days", 
         "compliance_status": "compliant"},
        {"clause_name": "termination", "clause_text": "Termination requires 90 days' notice", 
         "compliance_status": "non-compliant"}
      ],
      "citations": ["policy:confidentiality:standard", "policy:payment_terms:max_delay", "policy:termination:early_notice"],
      "next_steps": ["Check compliance of clauses", "Suggest edits for non-compliant clauses", "Approve contract"],
      "tool_proposals": [
        {"name":"AnalyzeContract","args":{"contract_text":contract_data["contract_text"],"contract_type":"SaaS","jurisdiction":"US","clauses":["confidentiality","payment_terms","termination"]},
         "preconditions":"Analyze contract for compliance with policies.","idempotency_key": new_idem()},
        {"name":"CheckCompliance","args":{"clause_name":"confidentiality","clause_text":"Confidentiality must last at least 2 years","policy_id":"policy:confidentiality:standard"},
         "preconditions":"Ensure clause meets confidentiality standards.","idempotency_key": new_idem()},
        {"name":"SuggestEdits","args":{"clause_name":"confidentiality","clause_text":"Confidentiality must last at least 2 years","recommended_changes":"Replace with: 'Confidentiality will last for 2 years post-termination'"},
         "preconditions":"Suggest edits to non-compliant clauses.","idempotency_key": new_idem()},
        {"name":"ApproveContract","args":{"contract_id":"contract_1234","approver_id":"M001"},
         "preconditions":"If all clauses are compliant, approve contract.","idempotency_key": new_idem()}
      ]
    }

def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
    idx = {r.tool:r for r in receipts}
    lines = [model_json["summary"], ""]
    lines.append(f"Decision: {model_json['decision']}")
    for clause in model_json["contract_clauses"]:
        lines.append(f"Clause {clause['clause_name']}: {clause['clause_text']} - {clause['compliance_status']}")
    if idx.get("ApproveContract") and idx["ApproveContract"].ok:
        lines.append(f"\nContract approved: {idx['ApproveContract'].ref}")
    lines.append("\nNext:")
    for s in model_json["next_steps"]:
        lines.append(f"• {s}")
    lines.append("\nCitations: " + ", ".join(model_json["citations"]))
    return "\n".join(lines)

def handle(contract_data: Dict[str,Any]) -> str:
    contract = open("contracts/legal_review_v1.yaml").read()
    claims: List[Dict[str,Any]] = []  # load real claims from company policy
    plan = call_model(contract, claims, contract_data)

    receipts: List[ToolReceipt] = []
    for prop in plan["tool_proposals"]:
        reason = verify_proposal(prop)
        if reason:
            receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
        rec = execute(prop)
        receipts.append(rec)
        if not rec.ok and prop["name"] in {"ApproveContract"}:
            break
    return render_response(plan, receipts)

if __name__ == "__main__":
    example_contract = {
      "contract_id":"contract_1234",
      "contract_text":"Confidentiality must last at least 2 years after termination. Payment must be within 60 days from invoice date. Termination requires 90 days' notice.",
      "vendor":"VendorX",
      "jurisdiction":"US"
    }
    print(handle(example_contract))

The Prompt You’d Send to the Model (concise and testable)

System:
You are LegalContractAgent. Follow the contract:
- Ask once if contract_text, contract_type, or jurisdiction is missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, decision, contract_clauses[], citations[], next_steps[], tool_proposals[].

Claims (eligible only):
[ ... JSON array of policy claims like above ... ]

User:
Review this contract for compliance and suggest edits if needed:
{"contract_id":"contract_1234","contract_text":"Confidentiality must last at least 2 years after termination. Payment must be within 60 days from invoice date. Termination requires 90 days' notice.","vendor":"VendorX","jurisdiction":"US"}

How to adapt quickly

Wire contract analysis, compliance checking, and edit suggestion tools to your internal contract management systems. Load company policy claims for compliance checks such as payment terms, confidentiality duration, and termination notice periods. Add a validator step before execution for schema, lexicon, locale, citation coverage, and no implied writes. Ship the contract, policy bundle, and decoder settings behind a feature flag for canary deployment and rollback support.