Spaces:
Sleeping
Sleeping
inital_mvp
Browse files- adjuster_dashboard.py +81 -0
- app.py +170 -0
- claims_agent.py +79 -0
- test_agent.py +19 -0
- test_doc_conversion.py +25 -0
- utils.py +129 -0
adjuster_dashboard.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from utils import get_mock_claims
|
| 3 |
+
|
| 4 |
+
class AdjusterDashboard:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.claims = get_mock_claims()
|
| 7 |
+
|
| 8 |
+
def get_claims_data(self):
|
| 9 |
+
"""Formats claims for the dataframe."""
|
| 10 |
+
return [[c["id"], c["submitter"], c["date"], c["vehicle"], c["status"], c["ai_analysis"]["fraud_risk"], c["ai_analysis"].get("adjuster_classification", "Junior Adjuster")] for c in self.claims]
|
| 11 |
+
|
| 12 |
+
def get_claim_details(self, evt: gr.SelectData):
|
| 13 |
+
"""
|
| 14 |
+
Returns details for a selected claim.
|
| 15 |
+
Args:
|
| 16 |
+
evt: The selection event from the dataframe.
|
| 17 |
+
"""
|
| 18 |
+
# Get the row index selected
|
| 19 |
+
index = evt.index[0]
|
| 20 |
+
if index < len(self.claims):
|
| 21 |
+
claim = self.claims[index]
|
| 22 |
+
analysis = claim["ai_analysis"]
|
| 23 |
+
|
| 24 |
+
details_md = f"""
|
| 25 |
+
### Claim Details: {claim['id']}
|
| 26 |
+
**Submitter:** {claim['submitter']}
|
| 27 |
+
**Vehicle:** {claim['vehicle']}
|
| 28 |
+
**Date:** {claim['date']}
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
### AI Analysis
|
| 32 |
+
* **Damage Estimate:** {analysis['damage_estimate']}
|
| 33 |
+
* **Fraud Risk:** {analysis['fraud_risk']}
|
| 34 |
+
* **Adjuster Classification:** {analysis.get('adjuster_classification', 'Junior Adjuster')}
|
| 35 |
+
* **Recommendation:** {analysis['recommendation']}
|
| 36 |
+
|
| 37 |
+
**Summary:**
|
| 38 |
+
{analysis['summary']}
|
| 39 |
+
"""
|
| 40 |
+
return details_md, gr.update(interactive=True), gr.update(interactive=True)
|
| 41 |
+
return "Select a claim to view details.", gr.update(interactive=False), gr.update(interactive=False)
|
| 42 |
+
|
| 43 |
+
def add_claim(self, claim_data):
|
| 44 |
+
"""
|
| 45 |
+
Adds a new claim to the dashboard.
|
| 46 |
+
Args:
|
| 47 |
+
claim_data (dict): Extracted claim data.
|
| 48 |
+
"""
|
| 49 |
+
import datetime
|
| 50 |
+
new_id = f"CLM-{1000 + len(self.claims) + 1}"
|
| 51 |
+
new_claim = {
|
| 52 |
+
"id": new_id,
|
| 53 |
+
"submitter": claim_data.get("submitter", "Anonymous"),
|
| 54 |
+
"date": datetime.date.today().strftime("%Y-%m-%d"),
|
| 55 |
+
"vehicle": claim_data.get("vehicle", "Unknown"),
|
| 56 |
+
"status": "New",
|
| 57 |
+
"ai_analysis": {
|
| 58 |
+
"damage_estimate": claim_data.get("damage_estimate", "N/A"),
|
| 59 |
+
"fraud_risk": claim_data.get("fraud_risk", "Unknown"),
|
| 60 |
+
"adjuster_classification": claim_data.get("adjuster_classification", "Junior Adjuster"),
|
| 61 |
+
"recommendation": claim_data.get("recommendation", "Review"),
|
| 62 |
+
"summary": claim_data.get("summary", "")
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
self.claims.append(new_claim)
|
| 66 |
+
return self.get_claims_data()
|
| 67 |
+
|
| 68 |
+
def approve_claim(self, claim_details_text):
|
| 69 |
+
# Extract ID from text (simple parsing for MVP)
|
| 70 |
+
try:
|
| 71 |
+
claim_id = claim_details_text.split("Claim Details: ")[1].split("\n")[0].strip()
|
| 72 |
+
return f"Claim {claim_id} Approved. Payment processing initiated."
|
| 73 |
+
except:
|
| 74 |
+
return "Error approving claim."
|
| 75 |
+
|
| 76 |
+
def escalate_claim(self, claim_details_text):
|
| 77 |
+
try:
|
| 78 |
+
claim_id = claim_details_text.split("Claim Details: ")[1].split("\n")[0].strip()
|
| 79 |
+
return f"Claim {claim_id} Escalated to SIU for investigation."
|
| 80 |
+
except:
|
| 81 |
+
return "Error escalating claim."
|
app.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from claims_agent import ClaimsIntakeAgent
|
| 3 |
+
from adjuster_dashboard import AdjusterDashboard
|
| 4 |
+
from utils import extract_claim_data
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
# Initialize Agents and Dashboard
|
| 8 |
+
intake_agent = ClaimsIntakeAgent()
|
| 9 |
+
dashboard = AdjusterDashboard()
|
| 10 |
+
|
| 11 |
+
# --- Claims Intake View Functions ---
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
# --- Claims Intake View Functions ---
|
| 15 |
+
def intake_chat(message, image, file, progress=gr.Progress()):
|
| 16 |
+
# Yield status updates to the output box directly
|
| 17 |
+
yield "🔄 **Initializing AI Agent...**"
|
| 18 |
+
progress(0.1, desc="Initializing AI Agent...")
|
| 19 |
+
time.sleep(0.5) # Simulate init time
|
| 20 |
+
|
| 21 |
+
yield "🔍 **Analyzing Image & Documents...**"
|
| 22 |
+
progress(0.3, desc="Analyzing Image & Documents...")
|
| 23 |
+
# If file is provided, it comes as a filepath
|
| 24 |
+
response = intake_agent.process_claim(message, image, file)
|
| 25 |
+
|
| 26 |
+
yield "💾 **Syncing with Adjuster Dashboard...**"
|
| 27 |
+
progress(0.7, desc="Syncing with Adjuster Dashboard...")
|
| 28 |
+
# Sync with Dashboard
|
| 29 |
+
if response and "Error" not in response:
|
| 30 |
+
try:
|
| 31 |
+
claim_data = extract_claim_data(response)
|
| 32 |
+
# Update the dashboard data
|
| 33 |
+
# Note: In a real app with concurrent users, this needs better state management.
|
| 34 |
+
# Here we update the global dashboard object.
|
| 35 |
+
dashboard.add_claim(claim_data)
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Error syncing to dashboard: {e}")
|
| 38 |
+
|
| 39 |
+
progress(1.0, desc="Assessment Complete!")
|
| 40 |
+
yield response
|
| 41 |
+
|
| 42 |
+
# --- Adjuster Auth Functions ---
|
| 43 |
+
def check_password(password):
|
| 44 |
+
if password == "password":
|
| 45 |
+
return gr.update(visible=False), gr.update(visible=True)
|
| 46 |
+
return gr.update(visible=True), gr.update(visible=False)
|
| 47 |
+
|
| 48 |
+
# --- UI Construction ---
|
| 49 |
+
custom_css = """
|
| 50 |
+
#main_container {
|
| 51 |
+
max-width: 1000px;
|
| 52 |
+
margin: 0 auto;
|
| 53 |
+
padding: 20px;
|
| 54 |
+
background-color: white;
|
| 55 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 56 |
+
border-radius: 10px;
|
| 57 |
+
margin-top: 20px;
|
| 58 |
+
}
|
| 59 |
+
body {
|
| 60 |
+
background-color: #f0f2f5;
|
| 61 |
+
}
|
| 62 |
+
h1 {
|
| 63 |
+
text-align: center;
|
| 64 |
+
color: #2c3e50;
|
| 65 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
| 66 |
+
}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="slate"), title="EMA Claims MVP", css=custom_css) as demo:
|
| 70 |
+
with gr.Column(elem_id="main_container"):
|
| 71 |
+
gr.Markdown("# 🛡️ EMA Claims MVP")
|
| 72 |
+
|
| 73 |
+
with gr.Tabs():
|
| 74 |
+
# Tab 1: Customer View (Claims Intake)
|
| 75 |
+
with gr.Tab("🚗 Claims Intake (Customer)"):
|
| 76 |
+
gr.Markdown("### Report an Accident")
|
| 77 |
+
gr.Markdown("Upload a photo of the damage and any relevant documents (PDF/Doc). Our AI will assess it immediately.")
|
| 78 |
+
|
| 79 |
+
with gr.Row():
|
| 80 |
+
with gr.Column(scale=1):
|
| 81 |
+
image_input = gr.Image(type="pil", label="Upload Damage Photo")
|
| 82 |
+
file_input = gr.File(label="Upload Documents (PDF/Doc)", file_types=[".pdf", ".docx", ".txt"])
|
| 83 |
+
msg_input = gr.Textbox(placeholder="Describe the accident...", label="Description")
|
| 84 |
+
submit_btn = gr.Button("Submit Claim", variant="primary")
|
| 85 |
+
|
| 86 |
+
with gr.Column(scale=1):
|
| 87 |
+
chat_output = gr.Markdown(label="AI Assessment")
|
| 88 |
+
|
| 89 |
+
# We need to update the claims table if it's visible, but since it's in another tab,
|
| 90 |
+
# we can rely on the global dashboard object being updated.
|
| 91 |
+
# However, Gradio components need an event to refresh.
|
| 92 |
+
# For MVP, we'll just update the global list. The table in the other tab won't auto-refresh
|
| 93 |
+
# without a trigger, but we can add a "Refresh" button or make the table update on tab select (harder in simple Blocks).
|
| 94 |
+
# Let's add a "Refresh" button to the dashboard.
|
| 95 |
+
|
| 96 |
+
submit_btn.click(
|
| 97 |
+
fn=intake_chat,
|
| 98 |
+
inputs=[msg_input, image_input, file_input],
|
| 99 |
+
outputs=[chat_output],
|
| 100 |
+
show_progress=True
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# Tab 2: Adjuster View (Dashboard)
|
| 104 |
+
with gr.Tab("👨💼 Adjuster Dashboard (Internal)"):
|
| 105 |
+
|
| 106 |
+
# Login State
|
| 107 |
+
with gr.Group(visible=True) as login_group:
|
| 108 |
+
gr.Markdown("### Internal Access Only")
|
| 109 |
+
password_input = gr.Textbox(label="Password", type="password")
|
| 110 |
+
login_btn = gr.Button("Login")
|
| 111 |
+
|
| 112 |
+
# Dashboard Content (Hidden by default)
|
| 113 |
+
with gr.Group(visible=False) as dashboard_group:
|
| 114 |
+
gr.Markdown("### Incoming Claims Queue")
|
| 115 |
+
refresh_btn = gr.Button("Refresh List", size="sm")
|
| 116 |
+
|
| 117 |
+
with gr.Row():
|
| 118 |
+
with gr.Column(scale=2):
|
| 119 |
+
# Dataframe for claims list
|
| 120 |
+
claims_df = gr.Dataframe(
|
| 121 |
+
headers=["ID", "Submitter", "Date", "Vehicle", "Status", "Fraud Risk", "Classification"],
|
| 122 |
+
value=dashboard.get_claims_data, # Pass method to call on load/refresh
|
| 123 |
+
interactive=False,
|
| 124 |
+
label="Active Claims",
|
| 125 |
+
elem_id="claims_table"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
with gr.Column(scale=1):
|
| 129 |
+
# Details Panel
|
| 130 |
+
details_panel = gr.Markdown("Select a claim to view details.", label="Claim Analysis")
|
| 131 |
+
with gr.Row():
|
| 132 |
+
approve_btn = gr.Button("Approve Payment", variant="primary", interactive=False)
|
| 133 |
+
escalate_btn = gr.Button("Escalate to SIU", variant="stop", interactive=False)
|
| 134 |
+
|
| 135 |
+
action_output = gr.Label(label="Action Status")
|
| 136 |
+
|
| 137 |
+
# Interactions
|
| 138 |
+
claims_df.select(
|
| 139 |
+
fn=dashboard.get_claim_details,
|
| 140 |
+
inputs=None,
|
| 141 |
+
outputs=[details_panel, approve_btn, escalate_btn]
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
approve_btn.click(
|
| 145 |
+
fn=dashboard.approve_claim,
|
| 146 |
+
inputs=[details_panel],
|
| 147 |
+
outputs=[action_output]
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
escalate_btn.click(
|
| 151 |
+
fn=dashboard.escalate_claim,
|
| 152 |
+
inputs=[details_panel],
|
| 153 |
+
outputs=[action_output]
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
refresh_btn.click(
|
| 157 |
+
fn=lambda: dashboard.get_claims_data(),
|
| 158 |
+
inputs=None,
|
| 159 |
+
outputs=[claims_df]
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
login_btn.click(
|
| 163 |
+
fn=check_password,
|
| 164 |
+
inputs=[password_input],
|
| 165 |
+
outputs=[login_group, dashboard_group]
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
demo.queue()
|
| 170 |
+
demo.launch()
|
claims_agent.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
class ClaimsIntakeAgent:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 11 |
+
if not api_key:
|
| 12 |
+
print("Warning: GEMINI_API_KEY not found in environment variables.")
|
| 13 |
+
else:
|
| 14 |
+
genai.configure(api_key=api_key)
|
| 15 |
+
self.model = genai.GenerativeModel('gemini-2.5-flash')
|
| 16 |
+
|
| 17 |
+
def process_claim(self, user_input, image=None, document_path=None):
|
| 18 |
+
"""
|
| 19 |
+
Processes a user claim report.
|
| 20 |
+
Args:
|
| 21 |
+
user_input (str): The user's description of the accident.
|
| 22 |
+
image (PIL.Image): Optional image of the damage.
|
| 23 |
+
document_path (str): Optional path to a uploaded document (PDF/Doc).
|
| 24 |
+
Returns:
|
| 25 |
+
str: The agent's response.
|
| 26 |
+
"""
|
| 27 |
+
if not hasattr(self, 'model'):
|
| 28 |
+
return "Error: Gemini API key not configured. Please check your .env file."
|
| 29 |
+
|
| 30 |
+
from utils import convert_doc_to_markdown
|
| 31 |
+
doc_content = ""
|
| 32 |
+
if document_path:
|
| 33 |
+
doc_content = convert_doc_to_markdown(document_path)
|
| 34 |
+
doc_content = f"\n\n**Uploaded Document Content:**\n{doc_content}\n"
|
| 35 |
+
|
| 36 |
+
prompt = f"""
|
| 37 |
+
You are an AI assistant for an auto insurance company. Your goal is to help users report vehicle claims and provide a preliminary assessment for the back-office.
|
| 38 |
+
Analyze the user's input, any provided images, and any uploaded document content.
|
| 39 |
+
|
| 40 |
+
If an image is provided, estimate the damage severity and cost (in GBP) based on visual inspection.
|
| 41 |
+
Extract vehicle information if possible.
|
| 42 |
+
Analyze the text and documents for consistency and potential fraud indicators.
|
| 43 |
+
|
| 44 |
+
**Uploaded Document Content:**
|
| 45 |
+
{doc_content}
|
| 46 |
+
|
| 47 |
+
Return a structured response in the following format:
|
| 48 |
+
|
| 49 |
+
**Claim Assessment**
|
| 50 |
+
* **Submitter Name:** [Name if found in text/docs, else 'Anonymous']
|
| 51 |
+
* **Vehicle:** [Vehicle Make/Model/Year if identifiable, else 'Unknown']
|
| 52 |
+
* **Damage Severity:** [Minor/Moderate/Severe]
|
| 53 |
+
* **Estimated Repair Cost:** [Amount in £]
|
| 54 |
+
* **Fraud Risk:** [Low/Medium/High] - [Brief reason]
|
| 55 |
+
* **Adjuster Classification:** [Junior Adjuster/Senior Adjuster] (Route complex/high-risk/high-value >£5000 claims to Senior)
|
| 56 |
+
* **Policy Check:** Coverage appears valid (Simulated).
|
| 57 |
+
* **Summary:** [Concise summary of the incident and findings]
|
| 58 |
+
* **Next Steps:** [Brief instruction to user]
|
| 59 |
+
|
| 60 |
+
If no image is provided, ask the user to upload one for a better assessment, but still acknowledge the report.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
inputs = [prompt, user_input]
|
| 65 |
+
if image:
|
| 66 |
+
inputs.append(image)
|
| 67 |
+
|
| 68 |
+
response = self.model.generate_content(inputs)
|
| 69 |
+
return response.text
|
| 70 |
+
except Exception as e:
|
| 71 |
+
return f"Error processing claim: {str(e)}"
|
| 72 |
+
|
| 73 |
+
def analyze_document(self, file_path):
|
| 74 |
+
"""
|
| 75 |
+
Analyzes an uploaded document (PDF/Image) for claim info.
|
| 76 |
+
For MVP, this handles images primarily, but could be expanded for PDFs.
|
| 77 |
+
"""
|
| 78 |
+
# Placeholder for document analysis logic using Gemini
|
| 79 |
+
return "Document received. Analysis feature is in beta (MVP)."
|
test_agent.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from claims_agent import ClaimsIntakeAgent
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def test_agent():
|
| 5 |
+
print("Testing ClaimsIntakeAgent...")
|
| 6 |
+
agent = ClaimsIntakeAgent()
|
| 7 |
+
|
| 8 |
+
# Check if API key is set
|
| 9 |
+
if not os.getenv("GEMINI_API_KEY") or os.getenv("GEMINI_API_KEY") == "your_api_key_here":
|
| 10 |
+
print("Skipping live API test: GEMINI_API_KEY not set.")
|
| 11 |
+
return
|
| 12 |
+
|
| 13 |
+
# Test with text only
|
| 14 |
+
response = agent.process_claim("I scratched my bumper backing into a pole.")
|
| 15 |
+
print("\nResponse to text input:")
|
| 16 |
+
print(response)
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
test_agent()
|
test_doc_conversion.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from utils import convert_doc_to_markdown
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def test_conversion():
|
| 5 |
+
# Create a dummy text file to test conversion (since we don't have a PDF handy easily without downloading)
|
| 6 |
+
# MarkItDown handles text files too.
|
| 7 |
+
dummy_file = "test_doc.txt"
|
| 8 |
+
with open(dummy_file, "w") as f:
|
| 9 |
+
f.write("This is a test document for claims processing.\nRepair estimate: £500.\n")
|
| 10 |
+
|
| 11 |
+
print(f"Testing conversion of {dummy_file}...")
|
| 12 |
+
content = convert_doc_to_markdown(dummy_file)
|
| 13 |
+
print("Converted Content:")
|
| 14 |
+
print(content)
|
| 15 |
+
|
| 16 |
+
if "Repair estimate: £500" in content:
|
| 17 |
+
print("SUCCESS: Content extracted correctly.")
|
| 18 |
+
else:
|
| 19 |
+
print("FAILURE: Content not extracted.")
|
| 20 |
+
|
| 21 |
+
# Clean up
|
| 22 |
+
os.remove(dummy_file)
|
| 23 |
+
|
| 24 |
+
if __name__ == "__main__":
|
| 25 |
+
test_conversion()
|
utils.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from markitdown import MarkItDown
|
| 5 |
+
|
| 6 |
+
def encode_image_to_base64(image):
|
| 7 |
+
"""Encodes a PIL Image to a base64 string."""
|
| 8 |
+
if isinstance(image, str):
|
| 9 |
+
# If it's a file path
|
| 10 |
+
with open(image, "rb") as image_file:
|
| 11 |
+
return base64.b64encode(image_file.read()).decode('utf-8')
|
| 12 |
+
elif isinstance(image, Image.Image):
|
| 13 |
+
buffered = BytesIO()
|
| 14 |
+
image.save(buffered, format="JPEG")
|
| 15 |
+
return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 16 |
+
return None
|
| 17 |
+
|
| 18 |
+
def convert_doc_to_markdown(file_path):
|
| 19 |
+
"""Converts a document (PDF, Docx, etc.) to markdown."""
|
| 20 |
+
if not file_path:
|
| 21 |
+
return ""
|
| 22 |
+
try:
|
| 23 |
+
md = MarkItDown()
|
| 24 |
+
result = md.convert(file_path)
|
| 25 |
+
return result.text_content
|
| 26 |
+
except Exception as e:
|
| 27 |
+
return f"Error converting document: {str(e)}"
|
| 28 |
+
|
| 29 |
+
def get_mock_claims():
|
| 30 |
+
"""Returns a list of mock claims for the dashboard."""
|
| 31 |
+
return [
|
| 32 |
+
{
|
| 33 |
+
"id": "CLM-1001",
|
| 34 |
+
"submitter": "John Doe",
|
| 35 |
+
"date": "2023-10-25",
|
| 36 |
+
"vehicle": "2018 Toyota Camry",
|
| 37 |
+
"status": "New",
|
| 38 |
+
"ai_analysis": {
|
| 39 |
+
"damage_estimate": "£820",
|
| 40 |
+
"fraud_risk": "Low",
|
| 41 |
+
"adjuster_classification": "Junior Adjuster",
|
| 42 |
+
"recommendation": "Auto-approve payment",
|
| 43 |
+
"summary": "Minor rear bumper damage. Policy active. No suspicious indicators."
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "CLM-1002",
|
| 48 |
+
"submitter": "Jane Smith",
|
| 49 |
+
"date": "2023-10-24",
|
| 50 |
+
"vehicle": "2022 Tesla Model 3",
|
| 51 |
+
"status": "Under Review",
|
| 52 |
+
"ai_analysis": {
|
| 53 |
+
"damage_estimate": "£15,000",
|
| 54 |
+
"fraud_risk": "High",
|
| 55 |
+
"adjuster_classification": "Senior Adjuster",
|
| 56 |
+
"recommendation": "Escalate to SIU",
|
| 57 |
+
"summary": "Severe front-end collision. Multiple vehicles involved. Discrepancy in accident location report."
|
| 58 |
+
}
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"id": "CLM-1003",
|
| 62 |
+
"submitter": "Robert Brown",
|
| 63 |
+
"date": "2023-10-26",
|
| 64 |
+
"vehicle": "2015 Ford Focus",
|
| 65 |
+
"status": "New",
|
| 66 |
+
"ai_analysis": {
|
| 67 |
+
"damage_estimate": "£1,200",
|
| 68 |
+
"fraud_risk": "Low",
|
| 69 |
+
"adjuster_classification": "Junior Adjuster",
|
| 70 |
+
"recommendation": "Review Further",
|
| 71 |
+
"summary": "Side panel scratch and dent. consistent with description. Higher than average repair cost for model."
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
def extract_claim_data(text):
|
| 77 |
+
"""
|
| 78 |
+
Extracts structured data from the AI's markdown response.
|
| 79 |
+
Returns a dictionary with keys: vehicle, damage_estimate, fraud_risk, adjuster_classification, summary, recommendation.
|
| 80 |
+
"""
|
| 81 |
+
import re
|
| 82 |
+
data = {
|
| 83 |
+
"submitter": "Anonymous",
|
| 84 |
+
"vehicle": "Unknown",
|
| 85 |
+
"damage_estimate": "N/A",
|
| 86 |
+
"fraud_risk": "Unknown",
|
| 87 |
+
"adjuster_classification": "Junior Adjuster",
|
| 88 |
+
"recommendation": "Review",
|
| 89 |
+
"summary": "Auto-generated summary from intake."
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Simple regex extraction based on the prompt format
|
| 93 |
+
# * **Submitter Name:** [Name]
|
| 94 |
+
name_match = re.search(r"\*\*Submitter Name:\*\*\s*(.*)", text)
|
| 95 |
+
if name_match:
|
| 96 |
+
data["submitter"] = name_match.group(1).strip()
|
| 97 |
+
|
| 98 |
+
# * **Vehicle:** [Vehicle Make/Model/Year if identifiable, else 'Unknown']
|
| 99 |
+
vehicle_match = re.search(r"\*\*Vehicle:\*\*\s*(.*)", text)
|
| 100 |
+
if vehicle_match:
|
| 101 |
+
data["vehicle"] = vehicle_match.group(1).strip()
|
| 102 |
+
|
| 103 |
+
estimate_match = re.search(r"\*\*Estimated Repair Cost:\*\*\s*(.*)", text)
|
| 104 |
+
if estimate_match:
|
| 105 |
+
data["damage_estimate"] = estimate_match.group(1).strip()
|
| 106 |
+
|
| 107 |
+
fraud_match = re.search(r"\*\*Fraud Risk:\*\*\s*(.*)", text)
|
| 108 |
+
if fraud_match:
|
| 109 |
+
data["fraud_risk"] = fraud_match.group(1).strip()
|
| 110 |
+
|
| 111 |
+
class_match = re.search(r"\*\*Adjuster Classification:\*\*\s*(.*)", text)
|
| 112 |
+
if class_match:
|
| 113 |
+
data["adjuster_classification"] = class_match.group(1).strip()
|
| 114 |
+
|
| 115 |
+
summary_match = re.search(r"\*\*Summary:\*\*\s*(.*)", text)
|
| 116 |
+
if summary_match:
|
| 117 |
+
data["summary"] = summary_match.group(1).strip()
|
| 118 |
+
else:
|
| 119 |
+
# Fallback: use the first few lines or the whole text if summary not found explicitly
|
| 120 |
+
data["summary"] = text[:200] + "..."
|
| 121 |
+
|
| 122 |
+
# Infer recommendation based on classification/risk if not explicitly parsed (or add prompt for it)
|
| 123 |
+
# For now, let's look for "Next Steps" or just default based on risk
|
| 124 |
+
if "High" in data["fraud_risk"]:
|
| 125 |
+
data["recommendation"] = "Escalate to SIU"
|
| 126 |
+
elif "Low" in data["fraud_risk"] and "Junior" in data["adjuster_classification"]:
|
| 127 |
+
data["recommendation"] = "Auto-approve payment"
|
| 128 |
+
|
| 129 |
+
return data
|