bpHigh commited on
Commit
76d67c5
·
1 Parent(s): 59dbc51

inital_mvp

Browse files
Files changed (6) hide show
  1. adjuster_dashboard.py +81 -0
  2. app.py +170 -0
  3. claims_agent.py +79 -0
  4. test_agent.py +19 -0
  5. test_doc_conversion.py +25 -0
  6. 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