A conversational agent that handles First Notice of Loss — collecting incident details, validating coverage, classifying the claim type, and routing to the right adjuster via email.
Check: python3 --version. Install from python.org if needed.
Sign up at console.anthropic.com → API Keys → Create Key. Claude is used here because its conversational quality and instruction-following are better for multi-turn FNOL intake flows. You'll need a small amount of credit — $5 lasts thousands of conversations.
You need an email account the agent can send from. Gmail works: go to Google Account → Security → 2-Step Verification → App Passwords → generate a password for "Mail". This is safer than using your main Gmail password. Or use any SMTP provider (Mailgun, SendGrid, etc.).
This example uses a mock policy database (a Python dictionary). In production, you'd replace this with an API call to your policy management system. The mock is enough to understand and test the full flow.
mkdir fnol-agent && cd fnol-agent
python3 -m venv venv
source venv/bin/activate
pip install anthropic python-dotenv
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
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()
To add more claim types, simply update the CLAIM_TYPES dictionary and the routing logic in run_fnol_agent().
source venv/bin/activate
python fnol_agent.py
Test it with this scenario:
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.
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.
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.
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.
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.