Jump to: The Problem Prerequisites Setup The Code Adjuster Routing Run It Extend It
The Problem This Solves

Without Agent

  • Customer calls claims hotline
  • Waits on hold for 15–30 minutes
  • Rep manually collects all details
  • Rep validates coverage by looking up policy
  • Rep types up summary and emails adjuster
  • 45–60 min per claim, reps unavailable nights/weekends

With Agent

  • Customer opens chat, no hold time
  • Agent collects all FNOL data conversationally
  • Coverage validation runs automatically
  • Claim classified and priority assigned
  • Adjuster emailed structured intake form
  • 8 min per claim, 24/7 availability
Prerequisites
Step 1 — Project Setup
bash — create project and install dependencies
mkdir fnol-agent && cd fnol-agent
python3 -m venv venv
source venv/bin/activate
pip install anthropic python-dotenv
.env — environment variables
ANTHROPIC_API_KEY=sk-ant-...your-key...
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=claims-bot@yourcompany.com
SMTP_PASSWORD=your-app-password
Step 2 — The Agent Code
fnol_agent.py — full FNOL intake agent
import os
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# ── Mock policy database ──────────────────────────────────────────────────────
# In production, replace lookup_policy() with an API call to your 
# policy management system.

POLICY_DB = {
    "POL-12345": {
        "holder": "John Smith",
        "type": "Auto",
        "vehicle": "2021 Toyota Camry",
        "coverage": ["Collision", "Comprehensive", "Liability"],
        "deductible": 500,
        "status": "Active",
        "adjuster": "auto-claims@insurer.com"
    },
    "POL-67890": {
        "holder": "Sarah Johnson",
        "type": "Home",
        "property": "123 Oak Street, Portland OR",
        "coverage": ["Dwelling", "Personal Property", "Liability"],
        "deductible": 1000,
        "status": "Active",
        "adjuster": "home-claims@insurer.com"
    },
    "POL-11111": {
        "holder": "Bob Davis",
        "type": "Auto",
        "vehicle": "2019 Honda Civic",
        "coverage": ["Liability"],
        "deductible": 0,
        "status": "Active",
        "adjuster": "auto-claims@insurer.com"
    }
}

def lookup_policy(policy_number: str) -> dict | None:
    return POLICY_DB.get(policy_number.upper())

# ── Claim classification ──────────────────────────────────────────────────────

CLAIM_TYPES = {
    "Auto": {
        "adjusters": {
            "Collision": "auto-collision@insurer.com",
            "Theft":     "auto-theft@insurer.com",
            "Weather":   "auto-weather@insurer.com",
            "default":   "auto-claims@insurer.com"
        }
    },
    "Home": {
        "adjusters": {
            "Fire":        "home-fire@insurer.com",
            "Water":       "home-water@insurer.com",
            "Theft":       "home-theft@insurer.com",
            "Storm":       "home-storm@insurer.com",
            "default":     "home-claims@insurer.com"
        }
    }
}

# ── Email routing ─────────────────────────────────────────────────────────────

def send_adjuster_email(policy: dict, claim_data: dict):
    """Send structured claim intake email to the appropriate adjuster."""
    adjuster_email = claim_data.get('adjuster_email', policy['adjuster'])

    subject = (f"NEW FNOL — {claim_data['priority']} Priority | "
               f"Policy {claim_data['policy_number']} | {claim_data['claim_type']}")

    body = f"""
FIRST NOTICE OF LOSS — INTAKE SUMMARY
======================================
Received: {claim_data['timestamp']}
Priority: {claim_data['priority']}

POLICY INFORMATION
------------------
Policy Number : {claim_data['policy_number']}
Policy Holder : {policy['holder']}
Policy Type   : {policy['type']}
Status        : {policy['status']}
Deductible    : ${policy['deductible']}
Coverage      : {', '.join(policy['coverage'])}

INCIDENT DETAILS
----------------
Date of Loss    : {claim_data.get('date_of_loss', 'Not provided')}
Location        : {claim_data.get('location', 'Not provided')}
Claim Type      : {claim_data['claim_type']}
Description     : {claim_data.get('description', 'Not provided')}
Injuries Reported: {claim_data.get('injuries', 'None reported')}
Third Parties   : {claim_data.get('third_parties', 'None')}
Police Report   : {claim_data.get('police_report', 'None')}

CLAIMANT CONTACT
----------------
Name  : {claim_data.get('claimant_name', policy['holder'])}
Phone : {claim_data.get('phone', 'Not provided')}
Email : {claim_data.get('email', 'Not provided')}

COVERAGE VALIDATION
-------------------
Claim covered: {claim_data.get('coverage_valid', 'Pending review')}
Notes: {claim_data.get('coverage_notes', 'Standard processing')}

NEXT STEPS
----------
1. Contact claimant within 24 hours to confirm receipt
2. Assign claim number and send acknowledgement letter
3. Schedule inspection if required
4. Request any missing documentation

---
This intake was processed by the FNOL AI Agent.
Reference ID: {claim_data.get('session_id', 'N/A')}
"""

    msg = MIMEMultipart()
    msg['From']    = os.getenv("SMTP_USER")
    msg['To']      = adjuster_email
    msg['Subject'] = subject
    msg.attach(MIMEText(body, 'plain'))

    try:
        with smtplib.SMTP(os.getenv("SMTP_HOST"), int(os.getenv("SMTP_PORT"))) as server:
            server.starttls()
            server.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASSWORD"))
            server.send_message(msg)
        print(f"✓ Claim routed to adjuster: {adjuster_email}")
    except Exception as e:
        print(f"Email send failed: {e}")
        print(f"[SIMULATED EMAIL]\nTo: {adjuster_email}\n{body}")

# ── FNOL Agent ────────────────────────────────────────────────────────────────

SYSTEM_PROMPT = """You are a First Notice of Loss (FNOL) intake agent for an 
insurance company. Your job is to collect all information needed to process 
a new insurance claim.

REQUIRED INFORMATION TO COLLECT:
1. Policy number (format: POL-XXXXX)
2. Claimant's name and contact (phone + email)
3. Date and location of the incident
4. Type of incident (collision, fire, theft, water damage, etc.)
5. Detailed description of what happened
6. Whether there are any injuries
7. Third parties involved (other drivers, people, etc.)
8. Whether a police report was filed (and report number if so)

CONVERSATION GUIDELINES:
- Introduce yourself warmly. Acknowledge this is a stressful situation.
- Ask for information one topic at a time — never a long list of questions
- Be empathetic, especially if there are injuries
- If the policy number is invalid, say so clearly and ask them to check
- If a coverage type is NOT in the policy, gently explain before continuing
- When you have ALL required information, say exactly: 
  "INTAKE_COMPLETE" on its own line, followed by the data as JSON

DATA FORMAT when intake is complete:
INTAKE_COMPLETE
{"policy_number": "...", "claimant_name": "...", "phone": "...", 
 "email": "...", "date_of_loss": "...", "location": "...", 
 "claim_type": "...", "description": "...", "injuries": "...", 
 "third_parties": "...", "police_report": "..."}

NEVER rush the customer. NEVER make up policy details."""

def run_fnol_agent():
    """Main FNOL intake conversation loop."""
    import uuid
    from datetime import datetime

    session_id = str(uuid.uuid4())[:8]
    conversation = []

    print("\n" + "="*60)
    print("FNOL Claims Intake Agent — Ready")
    print("="*60)
    print("(Type 'quit' to exit)\n")

    # Initial greeting from agent
    initial_response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=500,
        system=SYSTEM_PROMPT,
        messages=[{
            "role": "user",
            "content": "Hello, I need to report a claim."
        }]
    )
    greeting = initial_response.content[0].text
    print(f"Agent: {greeting}\n")
    conversation.append({"role": "user", "content": "Hello, I need to report a claim."})
    conversation.append({"role": "assistant", "content": greeting})

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() == 'quit':
            break
        if not user_input:
            continue

        # Add policy validation context if a policy number is mentioned
        extra_context = ""
        for word in user_input.upper().split():
            if word.startswith("POL-"):
                policy = lookup_policy(word)
                if policy:
                    extra_context = (f"\n[SYSTEM: Policy {word} is VALID. "
                                     f"Holder: {policy['holder']}, "
                                     f"Type: {policy['type']}, "
                                     f"Coverage: {', '.join(policy['coverage'])}, "
                                     f"Status: {policy['status']}]")
                else:
                    extra_context = f"\n[SYSTEM: Policy {word} NOT FOUND in database]"

        conversation.append({
            "role": "user",
            "content": user_input + extra_context
        })

        response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=800,
            system=SYSTEM_PROMPT,
            messages=conversation
        )

        agent_reply = response.content[0].text
        conversation.append({"role": "assistant", "content": agent_reply})

        # Check if intake is complete
        if "INTAKE_COMPLETE" in agent_reply:
            lines = agent_reply.split('\n')
            print(f"\nAgent: {lines[0]}")  # Print the completion message

            # Extract JSON data
            json_line = next((l for l in lines if l.strip().startswith('{')), None)
            if json_line:
                try:
                    claim_data = json.loads(json_line)
                    claim_data['session_id']   = session_id
                    claim_data['timestamp']    = datetime.now().strftime("%Y-%m-%d %H:%M")
                    claim_data['priority']     = "High" if claim_data.get('injuries', 'none').lower() not in ['none', 'no', ''] else "Standard"

                    # Look up policy for routing
                    policy = lookup_policy(claim_data.get('policy_number', ''))
                    if policy:
                        # Determine claim type category for routing
                        claim_type = claim_data.get('claim_type', '').lower()
                        if any(w in claim_type for w in ['collision', 'accident', 'hit']):
                            routing_key = 'Collision'
                        elif 'theft' in claim_type or 'stolen' in claim_type:
                            routing_key = 'Theft'
                        elif any(w in claim_type for w in ['fire', 'flood', 'water', 'storm', 'wind']):
                            routing_key = 'Weather' if policy['type'] == 'Auto' else 'Storm'
                        else:
                            routing_key = 'default'

                        adjuster_map = CLAIM_TYPES.get(policy['type'], {}).get('adjusters', {})
                        claim_data['adjuster_email']  = adjuster_map.get(routing_key, adjuster_map.get('default', policy['adjuster']))
                        claim_data['coverage_valid']  = "Yes — claim type is covered under this policy"

                        print("\n✅ Intake complete. Routing claim to adjuster...")
                        send_adjuster_email(policy, claim_data)
                        print("\nAgent: Your claim has been received and sent to the appropriate adjuster. "
                              f"Your reference number is FNOL-{session_id.upper()}. "
                              "You'll receive a confirmation email within the hour and "
                              "someone will be in touch within 24 hours.")
                    else:
                        print("\n⚠️ Policy not found — cannot route claim automatically.")

                except json.JSONDecodeError:
                    print("\n⚠️ Could not parse intake data. Please review manually.")
            break
        else:
            print(f"\nAgent: {agent_reply}\n")

if __name__ == "__main__":
    run_fnol_agent()
How Adjuster Routing Works
The agent routes each claim to the right specialist based on policy type and incident type.
Policy Type Incident Type Routes To
AutoCollision / accidentauto-collision@insurer.com
AutoTheft / stolenauto-theft@insurer.com
AutoWeather / hail / floodauto-weather@insurer.com
HomeFirehome-fire@insurer.com
HomeWater / floodhome-water@insurer.com
HomeStorm / wind / hailhome-storm@insurer.com

To add more claim types, simply update the CLAIM_TYPES dictionary and the routing logic in run_fnol_agent().

Step 3 — Run and Test
bash — run the agent
source venv/bin/activate
python fnol_agent.py

Test it with this scenario:

Test conversation walkthrough
Agent: Hello! I'm the claims intake assistant. I'm sorry to hear 
you need to file a claim — I'm here to help make this as smooth 
as possible. Could you please provide your policy number?

You: POL-12345

Agent: Thank you. I can see your policy is active, John. I'm sorry 
you're going through this. Could you tell me the date when the 
incident occurred?

You: It happened yesterday, March 29th

Agent: Thank you. And where did the incident take place?

You: On Interstate 84 near Portland, Oregon

Agent: Got it. What happened? Can you describe the incident briefly?

You: I was rear-ended at a red light. The other driver hit me at about 
20mph. My car has significant damage to the rear bumper and trunk.

Agent: I'm sorry to hear that. Are you or anyone else injured?

You: I have some neck pain but haven't seen a doctor yet.

[... conversation continues collecting all required information ...]

✅ Intake complete. Routing claim to adjuster...
✓ Claim routed to adjuster: auto-collision@insurer.com

Agent: Your claim has been received and sent to the appropriate 
adjuster. Your reference number is FNOL-A3B2C1D4. You'll receive 
a confirmation email within the hour and someone will be in touch 
within 24 hours.

Testing tip: Use the mock policies POL-12345 (Auto), POL-67890 (Home), and POL-11111 (Auto, liability-only) to test different routing paths. Try claiming a collision on POL-11111 — the agent should note that collision is not covered.

Extend It

Web chat interface

Wrap this in a Flask web app with a chat UI (see the Web Chat Widget example) and embed it on your claims portal. Customers can file claims from their browser.

Photo upload handling

Add a file upload step asking for photos of the damage. Store them in S3 or Azure Blob Storage and include the links in the adjuster email. Claude's vision capabilities can also analyse damage photos.

SMS-based FNOL

Use Twilio's Messaging API to run the same conversation over SMS. Many claimants prefer texting. The conversation logic stays identical — only the I/O channel changes.

Real policy API integration

Replace the POLICY_DB dictionary with an HTTP call to your policy management system (Guidewire, Duck Creek, etc.). The rest of the code doesn't change.