desolo-2918 commited on
Commit
dfc6f33
·
verified ·
1 Parent(s): fda9dd1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -0
app.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import datetime
5
+ import gradio as gr
6
+ import chromadb
7
+ import PyPDF2
8
+ from typing import List, Optional
9
+ from pydantic import BaseModel, Field
10
+ from sentence_transformers import SentenceTransformer
11
+ from smolagents import (
12
+ tool,
13
+ ToolCallingAgent,
14
+ WebSearchTool,
15
+ OpenAIServerModel,
16
+ PromptTemplates,
17
+ PlanningPromptTemplate,
18
+ ManagedAgentPromptTemplate,
19
+ FinalAnswerPromptTemplate
20
+ )
21
+
22
+ # ==========================================
23
+ # 1. SCHEMAS
24
+ # ==========================================
25
+ class ClaimInfo(BaseModel):
26
+ claim_number: str
27
+ policy_number: str
28
+ claimant_name: str
29
+ date_of_loss: str
30
+ loss_description: str
31
+ estimated_repair_cost: float
32
+ vehicle_details: Optional[str] = None
33
+
34
+ class PolicyQueries(BaseModel):
35
+ queries: List[str] = Field(default_factory=list)
36
+
37
+ class PolicyRecommendation(BaseModel):
38
+ policy_section: str
39
+ recommendation_summary: str
40
+ deductible: Optional[float] = None
41
+ settlement_amount: Optional[float] = None
42
+
43
+ class ClaimDecision(BaseModel):
44
+ claim_number: str
45
+ covered: bool
46
+ deductible: float
47
+ recommended_payout: float
48
+ notes: Optional[str] = None
49
+
50
+ # ==========================================
51
+ # 2. IMMEDIATE BACKEND INDEXING (Baked-in Knowledge)
52
+ # ==========================================
53
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
54
+ chroma_client = chromadb.Client()
55
+ collection = chroma_client.get_or_create_collection(name="auto_insurance_policy")
56
+
57
+ def initialize_policy_db():
58
+ policy_file = "policy.pdf"
59
+ if os.path.exists(policy_file) and collection.count() == 0:
60
+ print("Indexing Knowledge Base...")
61
+ with open(policy_file, "rb") as f:
62
+ reader = PyPDF2.PdfReader(f)
63
+ policy_text = "".join([page.extract_text() for page in reader.pages])
64
+
65
+ policy_chunks = policy_text.split("\n\n")
66
+ ids = [f"chunk_{i}" for i in range(len(policy_chunks))]
67
+ embeddings = embedder.encode(policy_chunks).tolist()
68
+ collection.add(documents=policy_chunks, embeddings=embeddings, ids=ids)
69
+ print(f"Knowledge Base Ready: {len(policy_chunks)} chunks indexed.")
70
+
71
+ initialize_policy_db()
72
+
73
+ # Global LLM placeholder for inner tool usage (updated per session)
74
+ llm_model = None
75
+
76
+ # ==========================================
77
+ # 3. CUSTOM TOOLS (Restored & Refined)
78
+ # ==========================================
79
+ @tool
80
+ def parse_claim(json_data: str) -> str:
81
+ """Parses claim JSON data to validate structure."""
82
+ try:
83
+ data = json.loads(json_data)
84
+ claim_info = ClaimInfo.model_validate(data)
85
+ return claim_info.model_dump_json()
86
+ except Exception as e:
87
+ return f"Error parsing claim: {str(e)}"
88
+
89
+ @tool
90
+ def is_valid_query(query: str) -> str:
91
+ """Validates policy standing and dates against coverage database."""
92
+ try:
93
+ claim_info = ClaimInfo.model_validate_json(query)
94
+ import csv
95
+
96
+ if not os.path.exists("coverage_data.csv"):
97
+ return json.dumps((False, "System Error: Coverage database not found."))
98
+
99
+ with open("coverage_data.csv", "r") as f:
100
+ reader = csv.DictReader(f)
101
+ policy = next((p for p in reader if p["policy_number"] == claim_info.policy_number), None)
102
+
103
+ if not policy: return json.dumps((False, "Policy not found."))
104
+
105
+ dues = policy.get("claim_dues_remaining", "").lower() in ("true", "1", "yes")
106
+ if dues: return json.dumps((False, "Outstanding dues found."))
107
+
108
+ d_loss = datetime.datetime.strptime(claim_info.date_of_loss, "%Y-%m-%d")
109
+ d_start = datetime.datetime.strptime(policy["coverage_start_date"], "%Y-%m-%d")
110
+ d_end = datetime.datetime.strptime(policy["coverage_end_date"], "%Y-%m-%d")
111
+
112
+ if not (d_start <= d_loss <= d_end):
113
+ return json.dumps((False, "Loss date outside coverage period."))
114
+
115
+ return json.dumps((True, "Valid claim."))
116
+ except Exception as e:
117
+ return f"Error: {str(e)}"
118
+
119
+ @tool
120
+ def generate_policy_queries(claim_info_json: str) -> str:
121
+ """Generate queries to retrieve relevant policy sections based on claim info."""
122
+ global llm_model
123
+ prompt = f"""
124
+ Analyze the following auto insurance claim to identify 3-5 key policy sections to consult:
125
+ - Focus on collision coverage, liability, deductibles, and relevant exclusions or endorsements.
126
+ - Claim Data: {claim_info_json}
127
+ - Return a JSON object with a 'queries' field containing a list of strings, e.g., {{"queries": ["query1", "query2"]}}. Do not include metadata fields.
128
+ """
129
+ try:
130
+ messages = [{"role": "user", "content": prompt}]
131
+ response = llm_model(messages)
132
+ response_content = response.content if hasattr(response, 'content') else str(response)
133
+ result = json.loads(response_content)
134
+ return json.dumps(result)
135
+ except Exception as e:
136
+ return f"Error generating policy queries: {str(e)}"
137
+
138
+ @tool
139
+ def retrieve_policy_text(queries_json: str) -> str:
140
+ """Retrieves policy text from ChromaDB using generated queries."""
141
+ try:
142
+ queries_data = json.loads(queries_json)
143
+ query_strings = queries_data.get("queries", [])
144
+ policy_texts = []
145
+ for q in query_strings:
146
+ query_embedding = embedder.encode([q])[0].tolist()
147
+ results = collection.query(query_embeddings=[query_embedding], n_results=2)
148
+ if results['documents']:
149
+ policy_texts.extend(results['documents'][0])
150
+ return "\n\n".join(set(policy_texts)) # Set removes duplicate chunks
151
+ except Exception as e:
152
+ return f"Error: {str(e)}"
153
+
154
+ @tool
155
+ def generate_recommendation(claim_info_json: str, policy_text: str) -> str:
156
+ """Generate a policy recommendation based on claim info and retrieved policy text."""
157
+ global llm_model
158
+ prompt = f"""
159
+ Evaluate the following auto insurance claim against the policy text:
160
+ - Determine if the collision is covered, the deductible, settlement amount, and applicable policy section.
161
+ - Claim Info: {claim_info_json}
162
+ - Policy Text: {policy_text}
163
+ - Return a JSON object strictly matching this schema:
164
+ {{
165
+ "policy_section": "str",
166
+ "recommendation_summary": "str",
167
+ "deductible": float or null,
168
+ "settlement_amount": float or null
169
+ }}
170
+ """
171
+ try:
172
+ messages = [{"role": "user", "content": prompt}]
173
+ response = llm_model(messages)
174
+ response_content = response.content if hasattr(response, 'content') else str(response)
175
+ result = json.loads(response_content)
176
+ PolicyRecommendation.model_validate(result) # Validate structure
177
+ return response_content
178
+ except Exception as e:
179
+ return f"Error generating recommendation: {str(e)}"
180
+
181
+ @tool
182
+ def finalize_decision(claim_info_json: str, recommendation_json: str) -> str:
183
+ """Finalize the claim decision based on the recommendation and format output."""
184
+ try:
185
+ claim_info = ClaimInfo.model_validate_json(claim_info_json)
186
+ rec_data = json.loads(recommendation_json)
187
+
188
+ # Safe defaults if the model missed a key
189
+ covered = "covered" in rec_data.get("recommendation_summary", "").lower() or (rec_data.get("settlement_amount", 0) or 0) > 0
190
+ deductible = float(rec_data.get("deductible") or 0.0)
191
+ payout = float(rec_data.get("settlement_amount") or 0.0)
192
+
193
+ decision = ClaimDecision(
194
+ claim_number=claim_info.claim_number,
195
+ covered=covered,
196
+ deductible=deductible,
197
+ recommended_payout=payout,
198
+ notes=rec_data.get("recommendation_summary", "No notes provided.")
199
+ )
200
+ return decision.model_dump_json(indent=2)
201
+ except Exception as e:
202
+ return f"Error finalizing decision: {str(e)}"
203
+
204
+ # ==========================================
205
+ # 4. PROMPT TEMPLATES (Restored from notebook)
206
+ # ==========================================
207
+ system_prompt = """
208
+ You are an expert insurance claim-processing agent specializing in auto insurance. You follow a strict, multi-step reasoning process.
209
+ CLAIM PROCESSING ORDER (MANDATORY):
210
+ 1. Parse the claim JSON to extract all ClaimInfo fields using `parse_claim`.
211
+ 2. Validate the claim using `is_valid_query`. If False, STOP immediately and return an invalid-claim decision.
212
+ 3. Generate policy-related search queries based on the extracted claim details using `generate_policy_queries`.
213
+ 4. Retrieve relevant policy text from ChromaDB using `retrieve_policy_text`.
214
+ 5. Use the web search tool to estimate typical repair costs for the described damage. Compare it to the claimed amount. If unreasonable, reject.
215
+ 6. Generate a recommendation using `generate_recommendation`.
216
+ 7. Produce the final claim decision using `finalize_decision`.
217
+ ALWAYS follow this exact sequence. Do not reorder, skip, or combine steps.
218
+ """
219
+
220
+ prompt_templates = PromptTemplates(
221
+ system_prompt=system_prompt,
222
+ planning=PlanningPromptTemplate(
223
+ initial_facts="Claim details:\n{claim_info_json}\nPolicy details:\n{policy_text}",
224
+ initial_plan="Follow the strict claim processing sequence: Parse -> Validate -> Query -> Retrieve -> Web Search Estimate -> Recommend -> Finalize.",
225
+ update_facts_pre_messages="Reassess facts:",
226
+ update_facts_post_messages="Facts updated.",
227
+ update_plan_pre_messages="Revise plan based on new facts:",
228
+ update_plan_post_messages="Plan updated."
229
+ ),
230
+ managed_agent=ManagedAgentPromptTemplate(
231
+ task="Process claim: {task_description}",
232
+ report="Generate final decision: {results}"
233
+ ),
234
+ final_answer=FinalAnswerPromptTemplate(
235
+ pre_messages="Summarize the final claim decision based on your tools.",
236
+ post_messages="Output clearly formatted decision.",
237
+ final_answer_template="""### ⚖️ Final Adjudication Result\n\n{final_answer}"""
238
+ )
239
+ )
240
+
241
+ # ==========================================
242
+ # 5. STATELESS PROCESSING GATEKEEPER
243
+ # ==========================================
244
+ def ui_process_claim(api_key, base_url, claim_no, policy_no, name, date, cost, vehicle, desc):
245
+ """Gatekeeper: validates API key and structures data safely before AI processing."""
246
+ if not api_key or not api_key.startswith("sk-"):
247
+ return "### ❌ Error\nPlease provide a valid OpenAI API Key in the Settings tab."
248
+
249
+ payload = {
250
+ "claim_number": claim_no,
251
+ "policy_number": policy_no,
252
+ "claimant_name": name,
253
+ "date_of_loss": date,
254
+ "loss_description": desc,
255
+ "estimated_repair_cost": cost,
256
+ "vehicle_details": vehicle
257
+ }
258
+
259
+ return execute_agent_workflow(api_key, base_url, json.dumps(payload))
260
+
261
+ def execute_agent_workflow(api_key, base_url, claim_json):
262
+ global llm_model
263
+ os.environ['OPENAI_API_KEY'] = api_key
264
+ os.environ['OPENAI_BASE_URL'] = base_url or "https://api.openai.com/v1"
265
+
266
+ # Initialize stateless model for current user
267
+ llm_model = OpenAIServerModel(
268
+ model_id="gpt-4.1",
269
+ api_base=os.environ['OPENAI_BASE_URL'],
270
+ api_key=os.environ['OPENAI_API_KEY']
271
+ )
272
+
273
+ agent = ToolCallingAgent(
274
+ tools=[
275
+ parse_claim,
276
+ is_valid_query,
277
+ generate_policy_queries,
278
+ retrieve_policy_text,
279
+ generate_recommendation,
280
+ finalize_decision,
281
+ WebSearchTool()
282
+ ],
283
+ model=llm_model,
284
+ prompt_templates=prompt_templates,
285
+ add_base_tools=False # Disabled base tools to strictly enforce custom workflow
286
+ )
287
+
288
+ try:
289
+ result = agent.run(f"Process this claim JSON strictly according to the mandatory workflow: {claim_json}")
290
+ return str(result)
291
+ except Exception as e:
292
+ return f"### ❌ Agent Execution Error\n{str(e)}"
293
+
294
+ # ==========================================
295
+ # 6. GRADIO UI (Guided & Anti-Breakage)
296
+ # ==========================================
297
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
298
+ gr.Markdown("# 🚗 Agentic Auto-Insurance Claims Processor")
299
+ gr.Markdown("*The Policy Knowledge Base is initialized and ready. Submit your claim below.*")
300
+
301
+ with gr.Tab("Settings"):
302
+ api_key_input = gr.Textbox(
303
+ label="OpenAI API Key",
304
+ type="password",
305
+ placeholder="sk-...",
306
+ info="Your key is only used for this session."
307
+ )
308
+ base_url_input = gr.Textbox(label="API Base URL (Optional)", value="https://api.openai.com/v1")
309
+
310
+ with gr.Tab("Claim Adjudicator"):
311
+ with gr.Row():
312
+ # Guided User Inputs
313
+ with gr.Column():
314
+ gr.Markdown("### 📝 Claim Details")
315
+ claim_no = gr.Textbox(label="Claim Number", placeholder="CLAIM-100", info="Format: CLAIM-XXX")
316
+ policy_no = gr.Dropdown(
317
+ label="Policy Number",
318
+ choices=["PN-1", "PN-2", "PN-3", "PN-4", "PN-5"],
319
+ info="Select an active policy ID."
320
+ )
321
+ claimant_name = gr.Textbox(label="Claimant Name", placeholder="Jane Doe")
322
+ loss_date = gr.Textbox(label="Date of Loss", placeholder="YYYY-MM-DD", info="Must follow YYYY-MM-DD format.")
323
+
324
+ loss_desc = gr.Textbox(
325
+ label="Loss Description",
326
+ placeholder="Describe the incident...",
327
+ lines=2
328
+ )
329
+ repair_cost = gr.Number(
330
+ label="Estimated Repair Cost ($)",
331
+ value=500.0,
332
+ minimum=0,
333
+ info="Do not use negative values."
334
+ )
335
+ vehicle_info = gr.Textbox(label="Vehicle Details", placeholder="2022 Tesla Model 3")
336
+
337
+ submit_btn = gr.Button("Evaluate Claim", variant="primary")
338
+
339
+ # Decision Output
340
+ with gr.Column():
341
+ gr.Markdown("### ⚖️ Agent Decision")
342
+ output_display = gr.Markdown(value="*Results will appear here after evaluation...*")
343
+
344
+ # Preset Examples Component
345
+ gr.Examples(
346
+ examples=[
347
+ ["CLAIM-001", "PN-1", "John Smith", "2023-10-15", 850.0, "2020 Honda Civic", "Front bumper damage from low-speed collision."],
348
+ ["CLAIM-002", "PN-3", "Alice Wong", "2024-02-10", 12000.0, "2023 Ford F-150", "Extensive side impact damage from running a red light."],
349
+ ],
350
+ inputs=[claim_no, policy_no, claimant_name, loss_date, repair_cost, vehicle_info, loss_desc],
351
+ label="Load Example Claims"
352
+ )
353
+
354
+ submit_btn.click(
355
+ fn=ui_process_claim,
356
+ inputs=[api_key_input, base_url_input, claim_no, policy_no, claimant_name, loss_date, repair_cost, vehicle_info, loss_desc],
357
+ outputs=output_display
358
+ )
359
+
360
+ if __name__ == "__main__":
361
+ demo.launch()