RobertoBarrosoLuque commited on
Commit
e954acb
·
1 Parent(s): ab4adfa
.gitignore ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ *.pem
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+ cover/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ .pybuilder/
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
85
+ __pypackages__/
86
+
87
+ # Celery stuff
88
+ celerybeat-schedule
89
+ celerybeat.pid
90
+
91
+ # SageMath parsed files
92
+ *.sage.py
93
+
94
+ # Environments
95
+ .env
96
+ .venv
97
+ env/
98
+ venv/
99
+ ENV/
100
+ env.bak/
101
+ venv.bak/
102
+
103
+ # Spyder project settings
104
+ .spyderproject
105
+ .spyproject
106
+
107
+ # Rope project settings
108
+ .ropeproject
109
+
110
+ # mkdocs documentation
111
+ /site
112
+
113
+ # mypy
114
+ .mypy_cache/
115
+ .dmypy.json
116
+ dmypy.json
117
+
118
+ # Pyre type checker
119
+ .pyre/
120
+
121
+ # pytype static type analyzer
122
+ .pytype/
123
+
124
+ # Cython debug symbols
125
+ cython_debug/
126
+
127
+ # PyCharm
128
+ .idea/
129
+ # *.iws
130
+ # *.iml
131
+ # *.ipr
132
+
133
+ # Visual Studio Code
134
+ .vscode/
135
+ *.code-workspace
136
+
137
+ # Sublime Text
138
+ *.sublime-project
139
+ *.sublime-workspace
140
+
141
+ # macOS
142
+ .DS_Store
143
+ .DS_Store?
144
+ ._*
145
+ .Spotlight-V100
146
+ .Trashes
147
+ ehthumbs.db
148
+ Thumbs.db
149
+
150
+ # Windows
151
+ Thumbs.db
152
+ ehthumbs.db
153
+ Desktop.ini
154
+ $RECYCLE.BIN/
155
+
156
+ # Linux
157
+ *~
158
+
159
+ # Temporary files
160
+ *.tmp
161
+ *.temp
162
+ *.swp
163
+ *.swo
164
+ *~
165
+
166
+ # Logs
167
+ *.log
168
+ logs/
169
+
170
+ # Database files
171
+ *.db
172
+ *.sqlite
173
+ *.sqlite3
174
+
175
+ # Configuration files with sensitive data
176
+ config.ini
177
+ secrets.json
178
+ .secrets
179
+ credentials.json
180
+
181
+ # API keys and environment variables
182
+ .env.local
183
+ .env.development
184
+ .env.test
185
+ .env.production
186
+
187
+ # Machine learning / data science
188
+ *.pkl
189
+ *.pickle
190
+ *.model
191
+ *.h5
192
+ *.hdf5
193
+ data/
194
+ datasets/
195
+ models/
196
+ checkpoints/
197
+
198
+ # Jupyter specific
199
+ *.ipynb_checkpoints/
200
+
201
+ # Virtual environment alternatives
202
+ .conda/
203
+ conda-meta/
204
+ miniconda3/
205
+ anaconda3/
206
+
207
+
208
+ # IDE and editor files
209
+ .idea/
210
+ *.iml
211
+ *.ipr
212
+ *.iws
213
+ .vscode/
214
+ *.sublime-*
215
+ .atom/
216
+
217
+ # OS generated files
218
+ .DS_Store*
219
+ .directory
220
+ .Trash-*
221
+ .fuse_hidden*
222
+
223
+ # Application specific (add your own)
224
+ uploads/
225
+ downloads/
226
+ temp/
227
+ cache/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ exclude: docs/badges
8
+ - id: check-added-large-files
9
+ args: ["--maxkb=1024"] # allow up to 1MB
10
+ - id: check-json
11
+ - id: check-yaml
12
+ args: ["--unsafe"] # needed for some mkdocs extensions
13
+ - id: check-merge-conflict
14
+ - id: mixed-line-ending
15
+ args: ["--fix=lf"]
16
+ - id: check-toml
17
+ - id: detect-private-key
18
+
19
+ - repo: local
20
+ hooks:
21
+ - id: sql_formatter
22
+ name: SQL formatter
23
+ language: python
24
+ entry: sql-formatter --max-line-length=270
25
+ files: \.sql$
26
+ additional_dependencies: [sql-formatter]
27
+
28
+ - repo: https://github.com/kynan/nbstripout
29
+ rev: 0.8.1
30
+ hooks:
31
+ - id: nbstripout
32
+
33
+ - repo: https://github.com/astral-sh/ruff-pre-commit
34
+ rev: v0.12.1
35
+ hooks:
36
+ - id: ruff
37
+
38
+ - repo: https://github.com/psf/black
39
+ rev: 25.1.0
40
+ hooks:
41
+ - id: black
42
+ args: ["--target-version", "py311"]
43
+
44
+ - repo: https://github.com/Yelp/detect-secrets
45
+ rev: v1.5.0
46
+ hooks:
47
+ - id: detect-secrets
48
+ exclude: ^(graphql-mock/pnpm-lock\.yaml|.*\.ipynb)$
Makefile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: setup install clean run
2
+
3
+ setup:
4
+ @echo "Setting up local environment..."
5
+ @scripts/install_uv.sh
6
+ @uv python install 3.11
7
+ @scripts/create_venv.sh
8
+ @. .venv/bin/activate && make install
9
+ @python -m scripts.setup_ssl
10
+
11
+ install:
12
+ @echo "Installing dependencies..."
13
+ uv pip install -e .
14
+
15
+ clean:
16
+ @echo "Cleaning up..."
17
+ rm -rf .venv
18
+ rm -rf dist
19
+ rm -rf *.egg-info
20
+ find . -type d -name __pycache__ -exec rm -rf {} +
21
+ find . -type d -name .pytest_cache -exec rm -rf {} +
22
+ find . -type d -name .ipynb_checkpoints -exec rm -rf {} +
23
+
24
+ run:
25
+ @. .venv/bin/activate
26
+ python -m src.app
README.md CHANGED
@@ -1,14 +1,127 @@
1
- ---
2
- title: Scout Claims
3
- emoji: 👀
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.38.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- short_description: ScoutAI is an AI claims assistant to generate claim reports
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ![Scout Demo](assets/fireworks_logo.png)
2
+
3
+
4
+ # Scout | AI Claims Assistant
5
+
6
+ > **Automated Insurance Claims Processing powered by Fireworks AI**
7
+
8
+ Scout is an intelligent claims processing assistant that uses advanced AI to analyze vehicle damage, process incident descriptions, and generate comprehensive claim reports. It showcases cutting-edge AI capabilities including computer vision, speech transcription, natural language processing, and autonomous function calling.
9
+
10
+ ## Key Features
11
+
12
+ - **Automated Damage Analysis**: AI-powered computer vision analyzes damage photos
13
+ - **Real-time Speech Transcription**: Live audio processing with Fireworks AI
14
+ - **Intelligent Incident Processing**: Advanced NLP extracts structured claim data
15
+ - **Autonomous Function Calling**: AI automatically gathers additional context (weather, driver records)
16
+ - **Professional PDF Generation**: Comprehensive claim reports with all evidence
17
+ - **Real-time Processing**: Sub-30 second end-to-end claim processing
18
+
19
+ ## High-Level Architecture
20
+
21
+ ### Core AI Components (Powered by Fireworks AI)
22
+
23
+ #### 1. Vision Analysis Module (`image_analysis.py`)
24
+ - **Function**: Analyzes damage photos to determine severity, location, and repair estimates
25
+ - **Output**: Structured JSON with damage classification and cost estimates
26
+
27
+ #### 2. Speech Transcription Service (`transcription.py`)
28
+ - **Function**: Converts live audio to text with 500ms updates
29
+ - **Features**: Automatic speech recognition with live feedback
30
+
31
+ #### 3. Incident Processing Engine (`incident_processing.py`)
32
+ - **Function**: Extracts structured claim data from transcribed incident descriptions
33
+ - **Advanced Feature**: **Autonomous Function Calling** (see details below)
34
+
35
+ #### 4. Report Generation System (`claim_processing.py`)
36
+ - **Technology**: AI-driven professional document generation
37
+ - **Output**: Comprehensive PDF reports with evidence, analysis, and recommendations
38
+ - **Features**: Professional formatting, appendices, and legal disclaimers
39
+
40
+ ### Autonomous Function Calling System
41
+ One of Scout's most advanced features is its **autonomous function calling capability**. The AI automatically determines when additional context would be helpful and calls external functions without human intervention.
42
+
43
+ #### How It Works:
44
+
45
+ 1. **Analysis Phase**: AI analyzes the incident transcript
46
+ 2. **Decision Making**: Determines which external data would improve assessment accuracy
47
+ 3. **Autonomous Execution**: Automatically calls relevant functions
48
+ 4. **Context Integration**: Incorporates results into final claim analysis
49
+
50
+ #### Available Functions:
51
+
52
+ | Function | Purpose | Data Retrieved |
53
+ |----------|---------|----------------|
54
+ | `weather_lookup` | Get weather conditions for incident date/location | Temperature, visibility, precipitation, conditions |
55
+ | `driver_record_check` | Verify other party's driving record | License status, insurance status, violation history, risk assessment |
56
+
57
+ #### Example Function Call Flow:
58
+
59
+ ```
60
+ User describes incident → AI extracts date/location → AI calls weather_lookup() →
61
+ AI finds other driver name → AI calls driver_record_check() →
62
+ AI incorporates weather + driver data into fault assessment
63
+ ```
64
+
65
+ This autonomous approach means **no manual intervention required** - the AI intelligently gathers the exact context needed for each unique claim.
66
+
67
+ ## Setup Instructions
68
+
69
+ ### Prerequisites
70
+
71
+ - Python 3.11+
72
+ - Fireworks AI API key ([Get one here](https://fireworks.ai))
73
+ - OpenSSL (for HTTPS/microphone access)
74
+
75
+ ### Quick Start
76
+
77
+ 1. **Clone the repository**
78
+ ```bash
79
+ git clone <repository-url>
80
+ cd scout-claims-assistant
81
+ ```
82
+
83
+ 2. **Run automated setup**
84
+ ```bash
85
+ make setup
86
+ ```
87
+ This will:
88
+ - Install `uv` package manager
89
+ - Create Python 3.11 virtual environment
90
+ - Install all dependencies
91
+ - Generate SSL certificates for HTTPS
92
+
93
+ 3. **Set your API key**: add FIREWORKS_API_KEY to .env
94
+
95
+ 4. **Launch the application**
96
+ ```bash
97
+ make run
98
+ ```
99
+
100
+ 5. **Open in browser**
101
+ ```
102
+ https://localhost:7860
103
+ ```
104
+ > **Note**: Accept the security warning for self-signed certificates
105
+
106
+ ## How to Use
107
+
108
+ ### Step 1: Upload Damage Photos
109
+ - Upload clear photos of vehicle damage
110
+ - AI analyzes damage severity, location, and repair costs
111
+ - Results appear in real-time
112
+
113
+ ### Step 2: Record Incident Description
114
+ - Click the microphone to start recording
115
+ - Describe the incident including:
116
+ - **When & Where**: Date, time, location
117
+ - **Who**: Other parties, witnesses
118
+ - **What**: How the accident happened
119
+ - **Injuries**: Any medical concerns
120
+ - Watch live transcription appear as you speak
121
+
122
+ ### Step 3: Generate Professional Report
123
+ - AI processes all information + calls external functions
124
+ - Generates comprehensive PDF claim report
125
+ - Download or submit directly
126
+
127
+ **Powered by Fireworks AI** | **Built for intelligent claims processing**
assets/fireworks_logo.png ADDED
notebooks/1-Building-Blocks.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebooks/2-Exercises.ipynb ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {
6
+ "id": "view-in-github",
7
+ "colab_type": "text"
8
+ },
9
+ "source": [
10
+ "<a href=\"https://colab.research.google.com/github/RobertoBarrosoLuque/scout-claims/blob/main/notebooks/2-Exercises.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "0",
16
+ "metadata": {
17
+ "id": "0"
18
+ },
19
+ "source": [
20
+ "# Exercises: Putting the Building Blocks into Practice\n",
21
+ "\n",
22
+ "Welcome to the hands-on portion of the workshop! In these exercises, you will apply the concepts we've learned to solve a few practical problems.\n",
23
+ "\n",
24
+ "**Your goals will be to:**\n",
25
+ "1. **Extend Function Calling**: Add a new tool for the LLM to use.\n",
26
+ "2. **Modify Structured Output**: Change a Pydantic schema to extract additional structured information from an image.\n",
27
+ "3. **Bonus! Use Grammar Mode**: Force the LLM to respond in a highly specific, token-efficient format.\n",
28
+ "\n",
29
+ "Look out for the lines marked \"TODO\" in each cell; those are where you will write your code. Let's get started!"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "code",
34
+ "execution_count": null,
35
+ "id": "e966e0b4",
36
+ "metadata": {
37
+ "id": "e966e0b4"
38
+ },
39
+ "outputs": [],
40
+ "source": [
41
+ "#\n",
42
+ "# SETUP CELL #1: PLEASE RUN THIS BEFORE CONTINUING WITH THE EXERCISES.\n",
43
+ "# RESTART THE RUNTIME AFTER RUNNING THIS CELL IF PROMPTED TO DO SO.\n",
44
+ "#\n",
45
+ "!pip install pydantic requests Pillow python-dotenv"
46
+ ]
47
+ },
48
+ {
49
+ "cell_type": "code",
50
+ "execution_count": null,
51
+ "id": "eac6208b",
52
+ "metadata": {
53
+ "id": "eac6208b"
54
+ },
55
+ "outputs": [],
56
+ "source": [
57
+ "#\n",
58
+ "# SETUP CELL #2: PLEASE RUN THIS BEFORE CONTINUING WITH THE EXERCISES\n",
59
+ "#\n",
60
+ "import os\n",
61
+ "import io\n",
62
+ "import base64\n",
63
+ "from dotenv import load_dotenv\n",
64
+ "import requests\n",
65
+ "import json\n",
66
+ "load_dotenv()\n",
67
+ "\n",
68
+ "MODEL_ID = \"accounts/fireworks/models/llama4-scout-instruct-basic\"\n",
69
+ "\n",
70
+ "# This pattern is for Google Colab.\n",
71
+ "# If running locally, set the FIREWORKS_API_KEY environment variable.\n",
72
+ "try:\n",
73
+ " from google.colab import userdata\n",
74
+ " FIREWORKS_API_KEY = userdata.get('FIREWORKS_API_KEY')\n",
75
+ "except ImportError:\n",
76
+ " FIREWORKS_API_KEY = os.getenv(\"FIREWORKS_API_KEY\")\n",
77
+ "\n",
78
+ "# Make sure to set your FIREWORKS_API_KEY\n",
79
+ "if not FIREWORKS_API_KEY:\n",
80
+ " print(\"⚠️ Warning: FIREWORKS_API_KEY not set. The following cells will not run without it.\")\n",
81
+ "\n",
82
+ "# Helper function to prepare images for VLMs.\n",
83
+ "# It is defined here to be available for later exercises.\n",
84
+ "def pil_to_base64_dict(pil_image):\n",
85
+ " \"\"\"Convert PIL image to the format expected by VLMs\"\"\"\n",
86
+ " if pil_image is None:\n",
87
+ " return None\n",
88
+ "\n",
89
+ " buffered = io.BytesIO()\n",
90
+ " if pil_image.mode != \"RGB\":\n",
91
+ " pil_image = pil_image.convert(\"RGB\")\n",
92
+ "\n",
93
+ " pil_image.save(buffered, format=\"JPEG\")\n",
94
+ " img_base64 = base64.b64encode(buffered.getvalue()).decode(\"utf-8\")\n",
95
+ "\n",
96
+ " return {\"image\": pil_image, \"path\": \"uploaded_image.jpg\", \"base64\": img_base64}\n",
97
+ "\n",
98
+ "# Helper function to make api calls with requests\n",
99
+ "def make_api_call(payload, tools=None, model_id=None, base_url=None):\n",
100
+ " \"\"\"Make API call with requests\"\"\"\n",
101
+ " # Use defaults if not provided\n",
102
+ " final_model_id = model_id or MODEL_ID\n",
103
+ " final_base_url = base_url or \"https://api.fireworks.ai/inference/v1\"\n",
104
+ "\n",
105
+ " # Add model to payload\n",
106
+ " payload[\"model\"] = final_model_id\n",
107
+ "\n",
108
+ " # Add tools if provided\n",
109
+ " if tools:\n",
110
+ " payload[\"tools\"] = tools\n",
111
+ " payload[\"tool_choice\"] = \"auto\"\n",
112
+ "\n",
113
+ " headers = {\n",
114
+ " \"Authorization\": f\"Bearer {FIREWORKS_API_KEY}\",\n",
115
+ " \"Content-Type\": \"application/json\"\n",
116
+ " }\n",
117
+ "\n",
118
+ " response = requests.post(\n",
119
+ " f\"{final_base_url}/chat/completions\",\n",
120
+ " headers=headers,\n",
121
+ " json=payload\n",
122
+ " )\n",
123
+ "\n",
124
+ " if response.status_code == 200:\n",
125
+ " return response.json()\n",
126
+ " else:\n",
127
+ " raise Exception(f\"API Error: {response.status_code} - {response.text}\")\n",
128
+ "\n",
129
+ "print(\"✅ Setup complete. Helper function and API key are ready.\")"
130
+ ]
131
+ },
132
+ {
133
+ "cell_type": "markdown",
134
+ "id": "09bc4200",
135
+ "metadata": {
136
+ "id": "09bc4200"
137
+ },
138
+ "source": [
139
+ "## Exercise 1: Extending Function Calling\n",
140
+ "\n",
141
+ "[Function calling](https://docs.fireworks.ai/guides/function-calling) allows an LLM to use external tools. Your first task is to give the LLM a new tool.\n",
142
+ "\n",
143
+ "**Goal**: Define a new function called `count_letter` that counts the occurrences of a specific letter in a word. You will then define its schema and make it available to the LLM.\n",
144
+ "\n",
145
+ "**Your Steps:**\n",
146
+ "1. Define the Python function `count_letter`.\n",
147
+ "2. Add it to the `available_functions` dictionary.\n",
148
+ "3. Define its schema and add it to the `tools` list.\n",
149
+ "4. Write a prompt to test your new function"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "code",
154
+ "execution_count": null,
155
+ "id": "99c48d84",
156
+ "metadata": {
157
+ "id": "99c48d84"
158
+ },
159
+ "outputs": [],
160
+ "source": [
161
+ "###\n",
162
+ "### EXERCISE 1: WRITE YOUR CODE IN THIS CELL\n",
163
+ "###\n",
164
+ "import json\n",
165
+ "\n",
166
+ "# --- Step 1: Define the Python function and the available functions mapping ---\n",
167
+ "\n",
168
+ "# Base function from the previous notebook\n",
169
+ "def get_weather(location: str) -> str:\n",
170
+ " \"\"\"Get current weather for a location\"\"\"\n",
171
+ " weather_data = {\"New York\": \"Sunny, 72°F\", \"London\": \"Cloudy, 15°C\", \"Tokyo\": \"Rainy, 20°C\"}\n",
172
+ " return weather_data.get(location, \"Weather data not available\")\n",
173
+ "\n",
174
+ "# ---TODO Block start---- #\n",
175
+ "# Define a new function `count_letter` that takes a `word` and a `letter`\n",
176
+ "# and returns the number of times the letter appears in the word.\n",
177
+ "def count_letter(): # TODO: Add your function header here\n",
178
+ " # TODO: Add your function body here\n",
179
+ " pass\n",
180
+ "# ---TODO Block end---- #\n",
181
+ "\n",
182
+ "available_functions = {\n",
183
+ " \"get_weather\": get_weather,\n",
184
+ " # TODO: Add your new function to this dictionary\n",
185
+ "}\n",
186
+ "\n",
187
+ "\n",
188
+ "# --- Step 2: Define the function schemas for the LLM ---\n",
189
+ "\n",
190
+ "# Base tool schema from the previous notebook\n",
191
+ "tools = [\n",
192
+ " {\n",
193
+ " \"type\": \"function\",\n",
194
+ " \"function\": {\n",
195
+ " \"name\": \"get_weather\",\n",
196
+ " \"description\": \"Get current weather for a location\",\n",
197
+ " \"parameters\": {\n",
198
+ " \"type\": \"object\",\n",
199
+ " \"properties\": {\n",
200
+ " \"location\": {\n",
201
+ " \"type\": \"string\",\n",
202
+ " \"description\": \"The city name\"\n",
203
+ " }\n",
204
+ " },\n",
205
+ " \"required\": [\"location\"]\n",
206
+ " }\n",
207
+ " }\n",
208
+ " },\n",
209
+ " # TODO: Add the JSON schema for your `count_letter` function here.\n",
210
+ " # It should have two parameters: \"word\" and \"letter\", both are required strings.\n",
211
+ "]\n",
212
+ "\n",
213
+ "\n",
214
+ "# --- Step 3: Build your input to the LLM ---\n",
215
+ "\n",
216
+ "# Initialize the messages list\n",
217
+ "messages = [\n",
218
+ " {\n",
219
+ " \"role\": \"system\",\n",
220
+ " \"content\": \"You are a helpful assistant. You have access to a couple of tools, use them when needed.\"\n",
221
+ " },\n",
222
+ " {\n",
223
+ " \"role\": \"user\",\n",
224
+ " \"content\": \"\" #TODO: Add your user prompt here\n",
225
+ " }\n",
226
+ "]\n",
227
+ "\n",
228
+ "# Create payload\n",
229
+ "payload = {\n",
230
+ " \"messages\": messages,\n",
231
+ " \"tools\": tools,\n",
232
+ " \"model\": \"accounts/fireworks/models/llama4-maverick-instruct-basic\"\n",
233
+ "}\n",
234
+ "\n",
235
+ "# Get response from LLM\n",
236
+ "response = make_api_call(payload=payload)\n",
237
+ "\n",
238
+ "# Check if the model wants to call a tool/function\n",
239
+ "if response[\"choices\"][0][\"message\"][\"tool_calls\"]:\n",
240
+ " tool_call = response[\"choices\"][0][\"message\"][\"tool_calls\"][0]\n",
241
+ " function_name = tool_call[\"function\"][\"name\"]\n",
242
+ " function_args = json.loads(tool_call[\"function\"][\"arguments\"])\n",
243
+ "\n",
244
+ " print(f\"LLM wants to call: {function_name}\")\n",
245
+ " print(f\"With arguments: {function_args}\")\n",
246
+ "\n",
247
+ " # Execute the function\n",
248
+ " function_response = available_functions[function_name](**function_args)\n",
249
+ " print(f\"Function result: {function_response}\")\n",
250
+ "\n",
251
+ " # Add the assistant's tool call to the conversation\n",
252
+ " messages.append({\n",
253
+ " \"role\": \"assistant\",\n",
254
+ " \"content\": \"\",\n",
255
+ " \"tool_calls\": response[\"choices\"][0][\"message\"][\"tool_calls\"]\n",
256
+ " })\n",
257
+ "\n",
258
+ " # Add the function result to the conversation\n",
259
+ " messages.append({\n",
260
+ " \"role\": \"tool\",\n",
261
+ " \"content\": json.dumps(function_response) if isinstance(function_response, dict) else str(function_response)\n",
262
+ " })\n",
263
+ "\n",
264
+ " # Create the final payload\n",
265
+ " final_payload = {\n",
266
+ " \"messages\": messages,\n",
267
+ " \"tools\": tools,\n",
268
+ " \"model\": \"accounts/fireworks/models/llama4-maverick-instruct-basic\"\n",
269
+ " }\n",
270
+ "\n",
271
+ " # Get final response from LLM\n",
272
+ " final_response = make_api_call(payload=payload)\n",
273
+ "\n",
274
+ " print(f'Final response: {final_response[\"choices\"][0][\"message\"][\"content\"]}')"
275
+ ]
276
+ },
277
+ {
278
+ "cell_type": "markdown",
279
+ "id": "4d198002",
280
+ "metadata": {
281
+ "id": "4d198002"
282
+ },
283
+ "source": [
284
+ "## Exercise 2: Modifying Structured Outputs (JSON Mode)\n",
285
+ "\n",
286
+ "Structured output is critical for building reliable applications. Here, you'll modify an existing schema to extract more information from an image.\n",
287
+ "\n",
288
+ "**Goal**: Update the `IncidentAnalysis` Pydantic model to also extract the `make` and `model` of the vehicle in the image.\n",
289
+ "\n",
290
+ "**Your Steps:**\n",
291
+ "1. Add the `make` and `model` fields to the `IncidentAnalysis` Pydantic class.\n",
292
+ "2. Run the VLM call using [JSON mode](https://docs.fireworks.ai/structured-responses/structured-response-formatting) to see the new structured output."
293
+ ]
294
+ },
295
+ {
296
+ "cell_type": "code",
297
+ "execution_count": null,
298
+ "id": "1dc5d727",
299
+ "metadata": {
300
+ "id": "1dc5d727"
301
+ },
302
+ "outputs": [],
303
+ "source": [
304
+ "###\n",
305
+ "### EXERCISE 2: WRITE YOUR CODE IN THIS CELL\n",
306
+ "###\n",
307
+ "import requests\n",
308
+ "import io\n",
309
+ "from PIL import Image\n",
310
+ "from pydantic import BaseModel, Field\n",
311
+ "from typing import Literal\n",
312
+ "\n",
313
+ "# --- Step 1: Download a sample image ---\n",
314
+ "url = \"https://raw.githubusercontent.com/RobertoBarrosoLuque/scout-claims/main/images/back_rhs_damage.png\"\n",
315
+ "response = requests.get(url)\n",
316
+ "image = Image.open(io.BytesIO(response.content))\n",
317
+ "print(\"Image downloaded.\")\n",
318
+ "\n",
319
+ "\n",
320
+ "# --- Step 2: Define the output schema ---\n",
321
+ "# ---TODO Block start---- #\n",
322
+ "# Add two new string fields to this Pydantic model:\n",
323
+ "# - `make`: To store the make of the car (e.g., \"Ford\")\n",
324
+ "# - `model`: To store the model of the car (e.g., \"Mustang\")\n",
325
+ "class IncidentAnalysis(BaseModel):\n",
326
+ " description: str = Field(description=\"A description of the damage to the vehicle.\")\n",
327
+ " location: Literal[\"front-left\", \"front-right\", \"back-left\", \"back-right\", \"front\", \"side\"]\n",
328
+ " severity: Literal[\"minor\", \"moderate\", \"major\"]\n",
329
+ " license_plate: str | None = Field(description=\"The license plate of the vehicle, if visible.\")\n",
330
+ "# ---TODO Block end---- #\n",
331
+ "\n",
332
+ "# --- Step 3: Call the VLM with the new schema ---\n",
333
+ "# The 'pil_to_base64_dict' function was defined in the setup cell\n",
334
+ "image_for_llm = pil_to_base64_dict(image)\n",
335
+ "\n",
336
+ "# Create payload\n",
337
+ "prompt = \"Describe the car damage in this image and extract all useful information.\" # TODO: modify the prompt to include the new fields\n",
338
+ "messages=[\n",
339
+ " {\n",
340
+ " \"role\": \"user\",\n",
341
+ " \"content\": [\n",
342
+ " {\"type\": \"image_url\", \"image_url\": {\"url\": f\"data:image/jpeg;base64,{image_for_llm['base64']}\"}},\n",
343
+ " {\"type\": \"text\", \"text\": prompt},\n",
344
+ " ],\n",
345
+ " }\n",
346
+ "]\n",
347
+ "response_format={\n",
348
+ " \"type\": \"json_object\",\n",
349
+ " \"schema\": IncidentAnalysis.model_json_schema(),\n",
350
+ "}\n",
351
+ "\n",
352
+ "payload = {\n",
353
+ " \"messages\": messages,\n",
354
+ " \"response_format\": response_format,\n",
355
+ " \"model\": \"accounts/fireworks/models/llama4-maverick-instruct-basic\"\n",
356
+ "}\n",
357
+ "\n",
358
+ "# Get response from LLM\n",
359
+ "response = make_api_call(payload=payload)\n",
360
+ "\n",
361
+ "\n",
362
+ "result = json.loads(response[\"choices\"][0][\"message\"][\"content\"])\n",
363
+ "print(json.dumps(result, indent=2))"
364
+ ]
365
+ },
366
+ {
367
+ "cell_type": "markdown",
368
+ "id": "8e5a2e3d",
369
+ "metadata": {
370
+ "id": "8e5a2e3d"
371
+ },
372
+ "source": [
373
+ "## Bonus Exercise: Constrained Output with Grammar Mode\n",
374
+ "\n",
375
+ "Sometimes you need the model to respond in a very specific, non-JSON format. This is where [Grammar Mode](https://docs.fireworks.ai/structured-responses/structured-output-grammar-based) excels. It forces the model's output to conform to a strict pattern you define, which can also save output tokens vs. JSON mode and offer even more granular control.\n",
376
+ "\n",
377
+ "**Goal**: Use grammar mode to force the model to output *only* the make and model of the car as a single lowercase string (e.g., \"ford mustang\").\n",
378
+ "\n",
379
+ "**Your Steps:**\n",
380
+ "1. Define a GBNF grammar string.\n",
381
+ "2. Call the model using `response_format={\"type\": \"grammar\", \"grammar\": ...}`."
382
+ ]
383
+ },
384
+ {
385
+ "cell_type": "code",
386
+ "execution_count": null,
387
+ "id": "1ea8cec3",
388
+ "metadata": {
389
+ "id": "1ea8cec3"
390
+ },
391
+ "outputs": [],
392
+ "source": [
393
+ "###\n",
394
+ "### BONUS EXERCISE: WRITE YOUR CODE IN THIS CELL\n",
395
+ "###\n",
396
+ "\n",
397
+ "# The 'image' variable and 'pil_to_base64_dict' helper function from previous\n",
398
+ "# cells are used here. Make sure those cells have been run.\n",
399
+ "# This assumes the image from Exercise 2 is still loaded.\n",
400
+ "image_for_llm = pil_to_base64_dict(image)\n",
401
+ "\n",
402
+ "\n",
403
+ "# --- Step 1: Define the GBNF grammar ---\n",
404
+ "# Define a grammar that forces the output to be:\n",
405
+ "# 1. A 'make' (one or more lowercase letters).\n",
406
+ "# 2. Followed by a single space.\n",
407
+ "# 3. Followed by a 'model' (one or more lowercase letters).\n",
408
+ "car_grammar = r'''\n",
409
+ "# TODO: define a grammar that forces the output to satisfy the format specified above (example output: \"ford mustang\")\n",
410
+ "'''\n",
411
+ "\n",
412
+ "# --- Step 2: Define the prompt ---\n",
413
+ "# Update the prompt to ask the model to identify the make and model and to respond only in the format specified above\n",
414
+ "prompt = \"\" # TODO: write your prompt here\n",
415
+ "\n",
416
+ "\n",
417
+ "# --- Step 3: Call the VLM with grammar mode ---\n",
418
+ "messages=[\n",
419
+ " {\n",
420
+ " \"role\": \"user\",\n",
421
+ " \"content\": [\n",
422
+ " {\"type\": \"image_url\", \"image_url\": {\"url\": f\"data:image/jpeg;base64,{image_for_llm['base64']}\"}},\n",
423
+ " {\"type\": \"text\", \"text\": prompt},\n",
424
+ " ],\n",
425
+ " }\n",
426
+ "]\n",
427
+ "response_format={\n",
428
+ " # TODO: define the response format to use the grammar defined above\n",
429
+ "}\n",
430
+ "\n",
431
+ "# Define payload\n",
432
+ "payload = {\n",
433
+ " \"messages\": messages,\n",
434
+ " \"response_format\": response_format,\n",
435
+ " \"model\": \"accounts/fireworks/models/llama4-maverick-instruct-basic\"\n",
436
+ "}\n",
437
+ "\n",
438
+ "# Get response from LLM\n",
439
+ "response = make_api_call(payload=payload)\n",
440
+ "\n",
441
+ "print(f'Constrained output from model: {response[\"choices\"][0][\"message\"][\"content\"]}')"
442
+ ]
443
+ }
444
+ ],
445
+ "metadata": {
446
+ "colab": {
447
+ "provenance": [],
448
+ "include_colab_link": true
449
+ },
450
+ "kernelspec": {
451
+ "display_name": ".venv",
452
+ "language": "python",
453
+ "name": "python3"
454
+ },
455
+ "language_info": {
456
+ "codemirror_mode": {
457
+ "name": "ipython",
458
+ "version": 2
459
+ },
460
+ "file_extension": ".py",
461
+ "mimetype": "text/x-python",
462
+ "name": "python",
463
+ "nbconvert_exporter": "python",
464
+ "pygments_lexer": "ipython2",
465
+ "version": "3.11.13"
466
+ }
467
+ },
468
+ "nbformat": 4,
469
+ "nbformat_minor": 5
470
+ }
notebooks/3-Fine-Tuning.ipynb ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {
6
+ "id": "view-in-github",
7
+ "colab_type": "text"
8
+ },
9
+ "source": [
10
+ "<a href=\"https://colab.research.google.com/github/RobertoBarrosoLuque/scout-claims/blob/main/notebooks/3-Fine-Tuning.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "0",
17
+ "metadata": {
18
+ "id": "0"
19
+ },
20
+ "outputs": [],
21
+ "source": []
22
+ }
23
+ ],
24
+ "metadata": {
25
+ "kernelspec": {
26
+ "display_name": "Python 3",
27
+ "language": "python",
28
+ "name": "python3"
29
+ },
30
+ "language_info": {
31
+ "codemirror_mode": {
32
+ "name": "ipython",
33
+ "version": 2
34
+ },
35
+ "file_extension": ".py",
36
+ "mimetype": "text/x-python",
37
+ "name": "python",
38
+ "nbconvert_exporter": "python",
39
+ "pygments_lexer": "ipython2",
40
+ "version": "2.7.6"
41
+ },
42
+ "colab": {
43
+ "provenance": [],
44
+ "include_colab_link": true
45
+ }
46
+ },
47
+ "nbformat": 4,
48
+ "nbformat_minor": 5
49
+ }
pyproject.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "scout"
3
+ version = "0.1.0"
4
+ description = "An end-to-end car crash claim AI assistant powered by FireworksAI"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "gradio",
9
+ "huggingface_hub[cli]",
10
+ "fireworks-ai",
11
+ "pre-commit",
12
+ "soundfile",
13
+ "scipy>=1.11.0",
14
+ "websocket-client",
15
+ "torchaudio",
16
+ "reportlab",
17
+ "python-dotenv",
18
+ "jupyter",
19
+ "ipython",
20
+ ]
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
scripts/create_venv.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ # 3. Create a virtual environment if not already created
7
+ VENV_DIR=".venv"
8
+ if [ ! -d "$VENV_DIR" ]; then
9
+ echo "Virtual environment not found. Creating a new virtual environment..."
10
+ uv venv --python 3.11
11
+ else
12
+ echo "Virtual environment already exists."
13
+ fi
scripts/install_uv.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ # Function to check if a command exists
7
+ command_exists() {
8
+ command -v "$1" >/dev/null 2>&1
9
+ }
10
+
11
+ # 1. Check if 'uv' is installed
12
+ if ! command_exists uv; then
13
+ echo "'uv' is not installed. Installing 'uv'..."
14
+ # Install 'uv'
15
+ curl -LsSf https://astral.sh/uv/install.sh | sh
16
+ source ~/.cargo/env
17
+ else
18
+ echo "'uv' is already installed."
19
+ fi
scripts/setup_ssl.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def check_openssl():
6
+ """Check if OpenSSL is available."""
7
+ try:
8
+ subprocess.run(["openssl", "version"], capture_output=True, check=True)
9
+ return True
10
+ except (subprocess.CalledProcessError, FileNotFoundError):
11
+ return False
12
+
13
+
14
+ def generate_ssl_certificates():
15
+ """Generate self-signed SSL certificates for local development."""
16
+ cert_dir = Path("")
17
+ key_file = cert_dir / "key.pem"
18
+ cert_file = cert_dir / "cert.pem"
19
+
20
+ # Check if certificates already exist
21
+ if key_file.exists() and cert_file.exists():
22
+ print("✅ SSL certificates already exist!")
23
+ return True
24
+
25
+ if not check_openssl():
26
+ print("❌ OpenSSL not found. Please install OpenSSL first.")
27
+ print("On Ubuntu/Debian: sudo apt-get install openssl")
28
+ print("On macOS: brew install openssl")
29
+ print(
30
+ "On Windows: Download from https://slproweb.com/products/Win32OpenSSL.html"
31
+ )
32
+ return False
33
+
34
+ print("🔐 Generating self-signed SSL certificates...")
35
+
36
+ # Generate private key
37
+ key_cmd = ["openssl", "genrsa", "-out", str(key_file), "2048"]
38
+
39
+ # Generate certificate
40
+ cert_cmd = [
41
+ "openssl",
42
+ "req",
43
+ "-new",
44
+ "-x509",
45
+ "-key",
46
+ str(key_file),
47
+ "-out",
48
+ str(cert_file),
49
+ "-days",
50
+ "365",
51
+ "-subj",
52
+ "/C=US/ST=CA/L=Local/O=Dev/CN=localhost",
53
+ ]
54
+
55
+ try:
56
+ subprocess.run(key_cmd, check=True)
57
+ subprocess.run(cert_cmd, check=True)
58
+ print("✅ SSL certificates generated successfully!")
59
+ print(f" Private key: {key_file}")
60
+ print(f" Certificate: {cert_file}")
61
+ return True
62
+ except subprocess.CalledProcessError as e:
63
+ print(f"❌ Failed to generate SSL certificates: {e}")
64
+ return False
65
+
66
+
67
+ def main():
68
+ """Main function to set up SSL certificates."""
69
+ print("🚀 Setting up SSL certificates for local HTTPS...")
70
+
71
+ if generate_ssl_certificates():
72
+ print("\n📋 Next steps:")
73
+ print("1. Run your Gradio app with the generated certificates")
74
+ print("2. Open https://localhost:7860 in your browser")
75
+ print(
76
+ "3. Accept the security warning (click 'Advanced' → 'Proceed to localhost')"
77
+ )
78
+ print("4. Allow microphone access when prompted")
79
+ print(
80
+ "\n⚠️ Note: You'll see a security warning because these are self-signed certificates."
81
+ )
82
+ print(
83
+ " This is normal for local development - just click through the warning."
84
+ )
85
+ else:
86
+ print("\n❌ SSL setup failed. Alternative options:")
87
+ print(
88
+ "1. Use Chrome with: --unsafely-treat-insecure-origin-as-secure=http://localhost:7860"
89
+ )
90
+ print(
91
+ "2. Use Firefox and set media.devices.insecure.enabled=true in about:config"
92
+ )
93
+ print("3. Deploy to a server with proper SSL certificates")
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
src/__init__.py ADDED
File without changes
src/app.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import gradio as gr
3
+ import threading
4
+ import queue
5
+ import numpy as np
6
+ import base64
7
+ import tempfile
8
+ import os
9
+ from dotenv import load_dotenv
10
+
11
+ from modules.image_analysis import pil_to_base64_dict, analyze_damage_image
12
+ from modules.transcription import FireworksTranscription
13
+ from modules.incident_processing import process_transcript_description
14
+ from modules.claim_processing import generate_claim_report_pdf
15
+
16
+ load_dotenv()
17
+
18
+ _FILE_PATH = Path(__file__).parents[1]
19
+
20
+
21
+ class ClaimsAssistantApp:
22
+ def __init__(self):
23
+ self.damage_analysis = None
24
+ self.incident_data = None
25
+ self.live_transcription = ""
26
+ self.transcription_lock = threading.Lock()
27
+ self.is_recording = False
28
+ self.transcription_service = None
29
+ self.audio_queue = queue.Queue()
30
+ self.final_report_pdf = None
31
+ self.claim_reference = ""
32
+ self.pdf_temp_path = None
33
+
34
+ @staticmethod
35
+ def format_function_calls_display(incident_data):
36
+ """Format function calls and external data for display"""
37
+ if not incident_data or "function_calls_made" not in incident_data:
38
+ return "", False
39
+
40
+ function_calls = incident_data.get("function_calls_made", [])
41
+ external_data = incident_data.get("external_data_retrieved", {})
42
+
43
+ if not function_calls:
44
+ return "", False
45
+
46
+ display_html = """
47
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
48
+ color: white; padding: 20px; border-radius: 12px; margin: 15px 0;">
49
+ <h3 style="margin-top: 0; display: flex; align-items: center;">
50
+ <span style="margin-right: 10px;">🔧</span>
51
+ AI Function Calls Executed
52
+ </h3>
53
+ <p style="margin-bottom: 15px; opacity: 0.9;">
54
+ The AI automatically gathered additional context by calling external functions:
55
+ </p>
56
+ """
57
+
58
+ for i, call in enumerate(function_calls, 1):
59
+ status_icon = "✅" if call["status"] == "success" else "❌"
60
+ function_name = call["function_name"]
61
+
62
+ display_html += f"""
63
+ <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; margin: 10px 0;">
64
+ <h4 style="margin: 0 0 10px 0;">
65
+ {status_icon} {i}. {function_name.replace('_', ' ').title()}
66
+ </h4>
67
+ <p style="margin: 5px 0; opacity: 0.8; font-size: 14px;">
68
+ Status: {call['status'].title()} - {call['message']}
69
+ </p>
70
+ """
71
+
72
+ if call["status"] == "success" and function_name in external_data:
73
+ result = external_data[function_name]
74
+
75
+ if function_name == "weather_lookup":
76
+ display_html += f"""
77
+ <div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 5px;">
78
+ <strong>Weather Conditions:</strong><br/>
79
+ 🌡️ Temperature: {result.get('temperature', 'N/A')}<br/>
80
+ ☁️ Conditions: {result.get('conditions', 'N/A')}<br/>
81
+ 👁️ Visibility: {result.get('visibility', 'N/A')}<br/>
82
+ 🌧️ Precipitation: {result.get('precipitation', 'N/A')}
83
+ </div>
84
+ """
85
+
86
+ elif function_name == "driver_record_check":
87
+ display_html += f"""
88
+ <div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 5px;">
89
+ <strong>Driver Record:</strong><br/>
90
+ 🆔 License: {result.get('license_status', 'N/A')}<br/>
91
+ 🛡️ Insurance: {result.get('insurance_status', 'N/A')}<br/>
92
+ 📊 Risk Level: {result.get('risk_assessment', 'N/A')}<br/>
93
+ 📝 Previous Claims: {result.get('previous_claims', 0)}
94
+ </div>
95
+ """
96
+
97
+ display_html += "</div>"
98
+
99
+ display_html += """
100
+ <div style="margin-top: 15px; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 5px;">
101
+ <small style="opacity: 0.8;">
102
+ 💡 This additional context helps provide more accurate claim assessment and risk evaluation.
103
+ </small>
104
+ </div>
105
+ </div>
106
+ """
107
+
108
+ return display_html, True
109
+
110
+ def create_interface(self):
111
+ """Create the main Gradio interface"""
112
+
113
+ with gr.Blocks(title="Scout Claims", theme=gr.themes.Soft()) as demo:
114
+ # Header
115
+ with gr.Row():
116
+ with gr.Column():
117
+ gr.Markdown("# 🚗 Scout | AI Claims Assistant 🚗")
118
+ gr.Markdown(
119
+ "*Automated Insurance Claims Processing with AI Function Calling*"
120
+ )
121
+
122
+ # Sidebar (API Key)
123
+ with gr.Row():
124
+ with gr.Column(scale=1):
125
+ gr.Markdown("### Powered by:")
126
+ gr.Image(
127
+ value=str(_FILE_PATH / "assets/fireworks_logo.png"),
128
+ height=30,
129
+ width=100,
130
+ show_label=False,
131
+ show_download_button=False,
132
+ container=False,
133
+ show_fullscreen_button=False,
134
+ )
135
+
136
+ gr.Markdown("## ⚙️ Configuration")
137
+
138
+ val = os.getenv("FIREWORKS_API_KEY", "")
139
+
140
+ api_key = gr.Textbox(
141
+ label="Fireworks AI API Key",
142
+ type="password",
143
+ placeholder="Enter your Fireworks AI API key",
144
+ value=val,
145
+ info="Required for AI processing",
146
+ )
147
+
148
+ gr.Markdown("## 📋 Instructions")
149
+ gr.Markdown(
150
+ """
151
+ **Step 1:** Upload car damage photo(s) \n
152
+ **Step 2:** Use microphone to describe incident \n
153
+ **Step 3:** Generate and review claim report \n
154
+ """
155
+ )
156
+
157
+ # Main Content Area
158
+ with gr.Column(scale=3):
159
+ # Step 1: Upload Image
160
+ gr.Markdown("## 📷 Step 1: Upload Damage Photos 📷")
161
+ with gr.Row():
162
+ image_input = gr.Image(
163
+ label="Car Damage Photo", type="pil", height=300
164
+ )
165
+
166
+ with gr.Column():
167
+ analyze_btn = gr.Button(
168
+ "🔍 Analyze Damage", variant="primary"
169
+ )
170
+ damage_status = gr.Textbox(
171
+ label="Analysis Status",
172
+ value="Ready to analyze damage",
173
+ interactive=False,
174
+ lines=2,
175
+ )
176
+
177
+ # Damage Analysis Results
178
+ damage_results = gr.JSON(
179
+ label="Damage Analysis Results", visible=False
180
+ )
181
+
182
+ gr.Markdown("---")
183
+
184
+ # Step 2: Incident Description with Live Streaming
185
+ gr.Markdown("## 🎤 Step 2: Describe the Incident 🎤")
186
+
187
+ with gr.Accordion(
188
+ "💡 What to Include in Your Recording", open=True
189
+ ):
190
+ gr.Markdown(
191
+ """
192
+ **Please describe the following when you record:**
193
+
194
+ 📅 **When & Where:**
195
+ - Date and time of the accident
196
+ - Street address or intersection
197
+
198
+ 👥 **Who Was Involved:**
199
+ - Other driver's name and contact info
200
+ - Vehicle details (make, model, color, license plate)
201
+ - Any witnesses
202
+
203
+ 🚗 **What Happened:**
204
+ - How the accident occurred
205
+ - Who was at fault and why
206
+ - Weather and road conditions
207
+
208
+ 🏥 **Injuries & Damage:**
209
+ - Anyone hurt? How seriously?
210
+ - How severe is the vehicle damage?
211
+
212
+ """
213
+ )
214
+
215
+ with gr.Row():
216
+ # Direct audio input - no toggle button needed
217
+ with gr.Column():
218
+ audio_input = gr.Audio(
219
+ label="🎵 Record Incident Description",
220
+ sources=["microphone"],
221
+ streaming=True,
222
+ format="wav",
223
+ show_download_button=False,
224
+ )
225
+ transcription_display = gr.Textbox(
226
+ label="Live Transcription",
227
+ placeholder="Click the 'Record' button above to start recording...",
228
+ lines=8,
229
+ interactive=False,
230
+ autoscroll=True,
231
+ )
232
+
233
+ process_incident_btn = gr.Button(
234
+ "📝 Process Incident", variant="primary"
235
+ )
236
+
237
+ incident_status = gr.Textbox(
238
+ label="Processing Status",
239
+ value="Record audio first to process incident",
240
+ interactive=False,
241
+ lines=2,
242
+ )
243
+
244
+ # NEW: Function calls display
245
+ function_calls_display = gr.HTML(
246
+ label="AI Function Calls", visible=False
247
+ )
248
+
249
+ # Incident Processing Results
250
+ incident_results = gr.JSON(
251
+ label="Incident Processing Results", visible=False
252
+ )
253
+
254
+ gr.Markdown("---")
255
+
256
+ # Step 3: Generate Claim Report
257
+ gr.Markdown("## 📄 Step 3: Generate Claim Report 📄")
258
+
259
+ generate_report_btn = gr.Button(
260
+ "🚀 Generate Claim Report", variant="primary", size="lg"
261
+ )
262
+
263
+ report_status = gr.Textbox(
264
+ label="Report Generation Status",
265
+ value="Complete steps 1 and 2 to generate report",
266
+ interactive=False,
267
+ lines=2,
268
+ )
269
+
270
+ # Final Report Display - Updated for PDF
271
+ with gr.Accordion(
272
+ "📋 Generated Claim Report (PDF)", open=False
273
+ ) as report_accordion:
274
+ # PDF Viewer using HTML iframe
275
+ pdf_viewer = gr.HTML(
276
+ value="<p style='text-align: center; color: gray;'>PDF report will appear here after generation</p>",
277
+ label="Claim Report PDF",
278
+ )
279
+
280
+ with gr.Row():
281
+ download_btn = gr.DownloadButton(
282
+ "💾 Download PDF Report", visible=False
283
+ )
284
+ submit_btn = gr.Button(
285
+ "✅ Submit Claim", variant="stop", visible=False
286
+ )
287
+
288
+ # Event Handlers
289
+ def handle_damage_analysis(image, api_key):
290
+ if image is None:
291
+ return (
292
+ "❌ Please upload an image first",
293
+ gr.update(visible=False),
294
+ )
295
+
296
+ if not api_key.strip():
297
+ return (
298
+ "❌ Please enter your Fireworks AI API key first",
299
+ gr.update(visible=False),
300
+ )
301
+
302
+ try:
303
+ # Update status to show processing
304
+ yield (
305
+ "🔄 Analyzing damage... Please wait",
306
+ gr.update(visible=False),
307
+ )
308
+
309
+ image_dict = pil_to_base64_dict(image)
310
+ self.damage_analysis = analyze_damage_image(
311
+ image=image_dict, api_key=api_key
312
+ )
313
+
314
+ yield (
315
+ "✅ Damage analysis completed successfully!",
316
+ gr.update(value=self.damage_analysis, visible=True),
317
+ )
318
+ return None
319
+
320
+ except Exception as e:
321
+ yield (
322
+ f"❌ Error analyzing damage: {str(e)}",
323
+ gr.update(visible=False),
324
+ )
325
+ return None
326
+
327
+ def live_transcription_callback(text):
328
+ """Callback for live transcription updates"""
329
+ with self.transcription_lock:
330
+ self.live_transcription = text
331
+
332
+ def initialize_transcription_service(api_key):
333
+ """Initialize transcription service when audio starts"""
334
+ if not api_key.strip():
335
+ return False
336
+
337
+ if not self.transcription_service:
338
+ self.transcription_service = FireworksTranscription(api_key)
339
+ self.transcription_service.set_callback(live_transcription_callback)
340
+
341
+ if not self.is_recording:
342
+ self.is_recording = True
343
+ self.live_transcription = ""
344
+ return self.transcription_service._connect()
345
+ return True
346
+
347
+ def process_audio_stream(audio_tuple, api_key):
348
+ """Process incoming audio stream for live transcription"""
349
+ if not audio_tuple:
350
+ with self.transcription_lock:
351
+ return self.live_transcription
352
+
353
+ # Initialize transcription service if needed
354
+ if not self.is_recording:
355
+ if not initialize_transcription_service(api_key):
356
+ return "❌ Failed to initialize transcription service. Check your API key."
357
+
358
+ try:
359
+ sample_rate, audio_data = audio_tuple
360
+
361
+ # Convert audio data to proper format
362
+ if not isinstance(audio_data, np.ndarray):
363
+ audio_data = np.array(audio_data, dtype=np.float32)
364
+
365
+ if audio_data.dtype != np.float32:
366
+ if audio_data.dtype == np.int16:
367
+ audio_data = audio_data.astype(np.float32) / 32768.0
368
+ elif audio_data.dtype == np.int32:
369
+ audio_data = audio_data.astype(np.float32) / 2147483648.0
370
+ else:
371
+ audio_data = audio_data.astype(np.float32)
372
+
373
+ # Skip if audio is too quiet
374
+ if np.max(np.abs(audio_data)) < 0.01:
375
+ with self.transcription_lock:
376
+ return self.live_transcription
377
+
378
+ # Convert to mono if stereo
379
+ if len(audio_data.shape) > 1:
380
+ audio_data = np.mean(audio_data, axis=1)
381
+
382
+ # Resample to 16kHz if needed
383
+ if sample_rate != 16000:
384
+ ratio = 16000 / sample_rate
385
+ new_length = int(len(audio_data) * ratio)
386
+ if new_length > 0:
387
+ audio_data = np.interp(
388
+ np.linspace(0, len(audio_data) - 1, new_length),
389
+ np.arange(len(audio_data)),
390
+ audio_data,
391
+ )
392
+
393
+ # Convert to bytes and send to transcription service
394
+ audio_bytes = (audio_data * 32767).astype(np.int16).tobytes()
395
+
396
+ if (
397
+ self.transcription_service
398
+ and self.transcription_service.is_connected
399
+ ):
400
+ self.transcription_service._send_audio_chunk(audio_bytes)
401
+
402
+ except Exception as e:
403
+ print(f"Error processing audio stream: {e}")
404
+
405
+ # Return current transcription
406
+ with self.transcription_lock:
407
+ return self.live_transcription
408
+
409
+ def handle_incident_processing(api_key):
410
+ """Process the recorded transcription into structured incident data with function calling"""
411
+ if not self.live_transcription.strip():
412
+ return (
413
+ "❌ No transcription available. Please record audio first.",
414
+ gr.update(visible=False),
415
+ gr.update(visible=False),
416
+ )
417
+
418
+ if not api_key.strip():
419
+ return (
420
+ "❌ Please enter your Fireworks AI API key first",
421
+ gr.update(visible=False),
422
+ gr.update(visible=False),
423
+ )
424
+
425
+ try:
426
+ # Update status
427
+ yield (
428
+ "🔄 Processing incident data ... Please wait",
429
+ gr.update(visible=False),
430
+ gr.update(visible=False),
431
+ )
432
+
433
+ # Use enhanced Fireworks processing with function calling
434
+ incident_analysis = process_transcript_description(
435
+ transcript=self.live_transcription, api_key=api_key
436
+ )
437
+
438
+ # Convert Pydantic model to dict for JSON display
439
+ self.incident_data = incident_analysis.model_dump()
440
+
441
+ # Format function calls for display
442
+ function_calls_html, show_calls = (
443
+ self.format_function_calls_display(self.incident_data)
444
+ )
445
+
446
+ # Update status message based on function calls
447
+ if show_calls:
448
+ status_message = f"✅ Incident processing completed with {len(self.incident_data.get('function_calls_made', []))} AI function calls!"
449
+ else:
450
+ status_message = (
451
+ "✅ Incident processing completed successfully!"
452
+ )
453
+
454
+ yield (
455
+ status_message,
456
+ gr.update(value=function_calls_html, visible=show_calls),
457
+ gr.update(value=self.incident_data, visible=True),
458
+ )
459
+ return None
460
+
461
+ except Exception as e:
462
+ yield (
463
+ f"❌ Error processing incident: {str(e)}",
464
+ gr.update(visible=False),
465
+ gr.update(visible=False),
466
+ )
467
+ return None
468
+
469
+ def handle_report_generation(api_key):
470
+ """Generate comprehensive claim report as PDF using AI"""
471
+ if not self.damage_analysis or not self.incident_data:
472
+ return (
473
+ "❌ Please complete damage analysis and incident processing first",
474
+ "<p style='text-align: center; color: gray;'>PDF report will appear here after generation</p>",
475
+ gr.update(visible=False),
476
+ gr.update(visible=False),
477
+ gr.update(open=False),
478
+ )
479
+
480
+ if not api_key.strip():
481
+ return (
482
+ "❌ Please enter your Fireworks AI API key first",
483
+ "<p style='text-align: center; color: gray;'>PDF report will appear here after generation</p>",
484
+ gr.update(visible=False),
485
+ gr.update(visible=False),
486
+ gr.update(open=False),
487
+ )
488
+
489
+ try:
490
+ # Show processing status
491
+ yield (
492
+ "🔄 Generating comprehensive PDF claim report... Please wait",
493
+ "<p style='text-align: center; color: gray;'>PDF report will appear here after generation</p>",
494
+ gr.update(visible=False),
495
+ gr.update(visible=False),
496
+ gr.update(open=False),
497
+ )
498
+
499
+ # Generate the PDF report
500
+ self.final_report_pdf = generate_claim_report_pdf(
501
+ damage_analysis=self.damage_analysis,
502
+ incident_data=self.incident_data,
503
+ )
504
+
505
+ # Extract claim reference for download filename
506
+ from datetime import datetime
507
+
508
+ timestamp = datetime.now()
509
+ self.claim_reference = f"CLM-{timestamp.strftime('%Y%m%d')}-{timestamp.strftime('%H%M%S')}"
510
+
511
+ # Save PDF to temporary file for viewing and downloading
512
+ if self.pdf_temp_path and os.path.exists(self.pdf_temp_path):
513
+ os.remove(self.pdf_temp_path)
514
+
515
+ temp_dir = tempfile.gettempdir()
516
+ self.pdf_temp_path = os.path.join(
517
+ temp_dir, f"{self.claim_reference}.pdf"
518
+ )
519
+
520
+ with open(self.pdf_temp_path, "wb") as f:
521
+ f.write(self.final_report_pdf)
522
+
523
+ # Create PDF viewer HTML
524
+ pdf_base64 = base64.b64encode(self.final_report_pdf).decode("utf-8")
525
+ pdf_viewer_html = f"""
526
+ <div style="text-align: center; margin: 20px 0;">
527
+ <h3 style="color: #2563eb;">📋 Insurance Claim Report - {self.claim_reference}</h3>
528
+ <iframe
529
+ src="data:application/pdf;base64,{pdf_base64}"
530
+ width="100%"
531
+ height="800px"
532
+ style="border: 2px solid #e5e7eb; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
533
+ <p>Your browser does not support PDF viewing.
534
+ <a href="data:application/pdf;base64,{pdf_base64}" download="{self.claim_reference}.pdf">
535
+ Click here to download the PDF
536
+ </a></p>
537
+ </iframe>
538
+ <p style="margin-top: 15px; color: #6b7280; font-size: 14px;">
539
+ 📄 Professional PDF report generated successfully! Use the download button below to save.
540
+ </p>
541
+ </div>
542
+ """
543
+
544
+ yield (
545
+ "✅ Professional PDF claim report generated successfully!",
546
+ pdf_viewer_html,
547
+ gr.update(visible=True, value=self.pdf_temp_path),
548
+ gr.update(visible=True),
549
+ gr.update(open=True),
550
+ )
551
+ return None
552
+
553
+ except Exception as e:
554
+ yield (
555
+ f"❌ Error generating PDF report: {str(e)}",
556
+ "<p style='text-align: center; color: red;'>Error generating PDF report</p>",
557
+ gr.update(visible=False),
558
+ gr.update(visible=False),
559
+ gr.update(open=False),
560
+ )
561
+ return None
562
+
563
+ def handle_claim_submission():
564
+ """Handle final claim submission"""
565
+ if not self.final_report_pdf:
566
+ return "❌ No report available to submit"
567
+
568
+ return f"🎉 Claim submitted successfully! Reference: {self.claim_reference}"
569
+
570
+ def cleanup_temp_files():
571
+ """Clean up temporary PDF files"""
572
+ if self.pdf_temp_path and os.path.exists(self.pdf_temp_path):
573
+ try:
574
+ os.remove(self.pdf_temp_path)
575
+ except Exception as e:
576
+ print(f"Error deleting temporary PDF file: {e}")
577
+ pass
578
+
579
+ # Wire up the events
580
+ analyze_btn.click(
581
+ fn=handle_damage_analysis,
582
+ inputs=[image_input, api_key],
583
+ outputs=[damage_status, damage_results],
584
+ )
585
+
586
+ # Handle streaming audio for live transcription
587
+ audio_input.stream(
588
+ fn=process_audio_stream,
589
+ inputs=[audio_input, api_key],
590
+ outputs=[transcription_display],
591
+ time_limit=None,
592
+ stream_every=0.5, # Update every 500ms
593
+ show_progress="hidden",
594
+ )
595
+
596
+ # Updated to include function calls display
597
+ process_incident_btn.click(
598
+ fn=handle_incident_processing,
599
+ inputs=[api_key],
600
+ outputs=[incident_status, function_calls_display, incident_results],
601
+ )
602
+
603
+ generate_report_btn.click(
604
+ fn=handle_report_generation,
605
+ inputs=[api_key],
606
+ outputs=[
607
+ report_status,
608
+ pdf_viewer,
609
+ download_btn,
610
+ submit_btn,
611
+ report_accordion,
612
+ ],
613
+ )
614
+
615
+ submit_btn.click(fn=handle_claim_submission, outputs=[report_status])
616
+
617
+ # Clean up on app close
618
+ demo.load(lambda: None)
619
+
620
+ return demo
621
+
622
+
623
+ def create_claims_app():
624
+ """Factory function to create the claims assistant app"""
625
+ app = ClaimsAssistantApp()
626
+ return app.create_interface()
627
+
628
+
629
+ # Create and launch the demo
630
+ if __name__ == "__main__":
631
+ print("Starting AI Claims Assistant Demo with Function Calling")
632
+ demo = create_claims_app()
633
+ demo.launch()
src/configs/config.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ analyze_damage_image:
2
+ model: "accounts/fireworks/models/qwen2p5-vl-32b-instruct"
3
+ temperature: 0.0
4
+
5
+ incident_response:
6
+ model: "accounts/fireworks/models/llama4-scout-instruct-basic"
7
+ temperature: 0.0
8
+
9
+ report_generation:
10
+ model: "accounts/fireworks/models/deepseek-v3-0324"
11
+ temperature: 0.0
src/configs/config_models.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class ModelConfig(BaseModel):
5
+ model: str
6
+ temperature: float = Field(default=0.1, ge=0, le=1.0)
7
+
8
+
9
+ class StepModelsConfigs(BaseModel):
10
+ analyze_damage_image: ModelConfig
11
+ incident_response: ModelConfig
12
+ report_generation: ModelConfig
src/configs/load_config.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+ from typing import Any
4
+ import yaml
5
+
6
+ from configs.config_models import StepModelsConfigs
7
+
8
+
9
+ @lru_cache
10
+ def load_config(config_path: Path, _format: str = "dict") -> dict[str, Any] | str:
11
+ """
12
+ Load configuration from a YAML file into a dictionary.
13
+
14
+ Parameters
15
+ ----------
16
+ config_path : Path
17
+ Path to the YAML configuration file.
18
+ _format : str, optional
19
+ The format in which to return the configuration, by default "dict".
20
+
21
+ Returns
22
+ -------
23
+ dict[str, Any] | str
24
+ A dictionary containing the configuration parameters if _format="dict",
25
+ otherwise the raw YAML content as a string.
26
+ """
27
+ with open(config_path, "r") as file:
28
+ content = file.read()
29
+
30
+ if _format == "dict":
31
+ return yaml.safe_load(content)
32
+ else:
33
+ return content
34
+
35
+
36
+ def load_module_config(config_path, config_model=None):
37
+ """
38
+ Load a YAML configuration file and validate against a Pydantic model.
39
+ """
40
+ config_data = load_config(config_path)
41
+
42
+ if config_model:
43
+ return config_model(**config_data)
44
+
45
+ return config_data
46
+
47
+
48
+ PROMPT_LIBRARY = load_config(Path(__file__).parent / "prompt_library.yaml")
49
+ APP_STEPS_CONFIGS = load_module_config(
50
+ Path(__file__).parent / "config.yaml", StepModelsConfigs
51
+ )
src/configs/prompt_library.yaml ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ vision_damage_analysis:
2
+ simple: |
3
+ Describe the car damage in this image.
4
+
5
+ intermediate: |
6
+ Analyze this car damage image and provide:
7
+
8
+ 1. What type of damage do you see?
9
+ 2. How severe is the damage?
10
+ 3. Which parts of the car are affected?
11
+ 4. Estimate if this is minor, moderate, or major damage.
12
+
13
+ Please be specific and detailed in your response.
14
+
15
+ advanced: |
16
+ You are an expert automotive claims adjuster analyzing vehicle damage. Examine this image carefully and provide a comprehensive damage assessment.
17
+
18
+ **SEVERITY DEFINITIONS:**
19
+ - **minor**: Cosmetic damage, scratches, small dents < 2 inches
20
+ - **moderate**: Noticeable damage affecting function, dents 2-6 inches, cracked lights
21
+ - **major**: Structural damage, large dents > 6 inches, safety systems affected
22
+
23
+ **LOCATION DEFINITIONS:**
24
+ - **front-left**: Driver side front (hood, front bumper, headlight, driver door front portion)
25
+ - **front-right**: Passenger side front (hood, front bumper, headlight, passenger door front portion)
26
+ - **back-left**: Driver side rear (trunk, rear bumper, taillight, driver door rear portion)
27
+ - **back-right**: Passenger side rear (trunk, rear bumper, taillight, passenger door rear portion)
28
+
29
+ **Licence Plate Extraction:**
30
+ - If the licence plate is present, please extract it and provide it in the response.
31
+ - Make sure to provide the full plate number and any relevant characters (e.g., dashes, spaces, etc.).
32
+ - If the licence plate is not present, please provide "N/A" in the response.
33
+
34
+ **INSTRUCTIONS:**
35
+ 1. Examine the entire visible vehicle surface
36
+ 2. Identify the primary damage location using the quadrant system
37
+ 3. Assess severity based on size, depth, and functional impact
38
+ 4. Provide the license plate number if present
39
+ 5. Provide a clear, detailed description of what you observe
40
+ 6. If damage spans multiple quadrants, choose the most severely affected area
41
+ 7. Be conservative in estimates - when uncertain, classify as higher severity
42
+
43
+ Focus on accuracy and consistency. This analysis will be used for insurance claim processing.
44
+
45
+
46
+ incident_processing:
47
+ simple: |
48
+ Analyze this transcript of an car crash incident
49
+
50
+ intermediate: |
51
+ Analyze this transcript of an car crash incident and provide:
52
+
53
+ 1. When and where
54
+ 2. What happened
55
+ 3. Who was involved
56
+ 4. What was the severity
57
+
58
+ advanced: |
59
+ You are an expert automotive claims adjuster analyzing vehicle damage. You have been provided a transcript of an incident and you need to provide a detailed analysis of the incident.
60
+
61
+ **INSTRUCTIONS:**
62
+ 1. Analyze the entire transcript
63
+ 2. Indentify when and where the incident occurred
64
+ 3. Identify who was involved
65
+ 5. Identify what exact events took place
66
+ 6. Identify the severity of the incident - to both the car and people
67
+ 7. Be specific and detailed in your response
68
+
69
+ If any of the above information is unclear or not provided in the transcript, do not make any thing up. Instead just
70
+ say "No information provided in the transcript"
71
+
72
+ **Example transcript:**
73
+ <transcript>
74
+ Yeah so I was driving down the road and I hit a car and I was hurt. This happened today around 3pm
75
+ the other car involved was a VW sedan driven by a guy called John Doe. It was a pretty minor crash so just
76
+ a couple of scratchs on the driver side and a few dents on the passenger side. No on was hurt.
77
+ </transcript>
78
+
79
+ **Example response:**
80
+ <response>
81
+ When: around 3pm today
82
+ Where: on the road
83
+ What happened: I was driving down the road and I hit a car
84
+ Who was involved: VW sedan driven by a guy called John Doe
85
+ What was the severity: It was a pretty minor crash so just a couple of scratchs on the driver side and a few dents on the passenger side
86
+ Was anyone hurt: No one was hurt
87
+ </response>
src/modules/__init__.py ADDED
File without changes
src/modules/claim_processing.py ADDED
@@ -0,0 +1,789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from typing import Dict, Any
3
+ import io
4
+ from reportlab.lib.pagesizes import letter
5
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
6
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
7
+ from reportlab.platypus import (
8
+ SimpleDocTemplate,
9
+ Paragraph,
10
+ Spacer,
11
+ Table,
12
+ TableStyle,
13
+ Image,
14
+ )
15
+ from reportlab.lib.units import inch
16
+ from reportlab.lib import colors
17
+ from reportlab.platypus.flowables import HRFlowable
18
+
19
+
20
+ def generate_claim_report_pdf(
21
+ damage_analysis: Dict[str, Any],
22
+ incident_data: Dict[str, Any],
23
+ image_path: str = None,
24
+ ) -> bytes:
25
+ """
26
+ Generate a professional insurance claim report as PDF from analyzed data.
27
+
28
+ Args:
29
+ damage_analysis: Results from image damage analysis
30
+ incident_data: Processed incident data from transcript
31
+ image_path: Optional path to the damage photo to include in appendix
32
+
33
+ Returns:
34
+ PDF bytes for the formatted claim report
35
+ """
36
+
37
+ # Create a BytesIO buffer to hold the PDF
38
+ buffer = io.BytesIO()
39
+
40
+ # Generate claim reference number
41
+ timestamp = datetime.now()
42
+ claim_ref = f"CLM-{timestamp.strftime('%Y%m%d')}-{timestamp.strftime('%H%M%S')}"
43
+
44
+ # Extract key information safely
45
+ damage_description = damage_analysis.get("description", "Vehicle damage detected")
46
+ damage_severity = damage_analysis.get("severity", "moderate")
47
+ damage_location = damage_analysis.get("location", "unknown")
48
+
49
+ # Get incident details safely with date conversion
50
+ date_location = incident_data.get("date_location", {})
51
+ # Convert relative dates to actual dates
52
+ actual_date = _convert_relative_date(date_location.get("date", "Not specified"))
53
+ date_location_converted = {**date_location, "date": actual_date}
54
+
55
+ parties_involved = incident_data.get("parties_involved", {})
56
+ fault_assessment = incident_data.get("fault_assessment", {})
57
+ incident_description = incident_data.get("incident_description", {})
58
+ injuries_medical = incident_data.get("injuries_medical", {})
59
+
60
+ # Generate assessments
61
+ priority = _get_priority_level(
62
+ damage_severity, injuries_medical.get("anyone_injured", "no")
63
+ )
64
+ cost_estimate = _estimate_cost_range(damage_severity)
65
+ recommendation = _get_recommendation(
66
+ damage_severity, injuries_medical.get("anyone_injured", "no")
67
+ )
68
+
69
+ # Create PDF document with professional margins
70
+ doc = SimpleDocTemplate(
71
+ buffer,
72
+ pagesize=letter,
73
+ rightMargin=0.75 * inch,
74
+ leftMargin=0.75 * inch,
75
+ topMargin=0.75 * inch,
76
+ bottomMargin=0.75 * inch,
77
+ )
78
+
79
+ # Get styles
80
+ styles = getSampleStyleSheet()
81
+
82
+ # Professional custom styles - all black text
83
+ title_style = ParagraphStyle(
84
+ "ProfessionalTitle",
85
+ parent=styles["Heading1"],
86
+ fontSize=20,
87
+ textColor=colors.black,
88
+ spaceAfter=16,
89
+ spaceBefore=0,
90
+ alignment=TA_CENTER,
91
+ fontName="Helvetica-Bold",
92
+ )
93
+
94
+ header_style = ParagraphStyle(
95
+ "ProfessionalHeader",
96
+ parent=styles["Heading2"],
97
+ fontSize=14,
98
+ textColor=colors.black,
99
+ spaceAfter=8,
100
+ spaceBefore=16,
101
+ fontName="Helvetica-Bold",
102
+ )
103
+
104
+ subheader_style = ParagraphStyle(
105
+ "ProfessionalSubHeader",
106
+ parent=styles["Heading3"],
107
+ fontSize=12,
108
+ textColor=colors.black,
109
+ spaceAfter=6,
110
+ spaceBefore=8,
111
+ fontName="Helvetica-Bold",
112
+ )
113
+
114
+ body_style = ParagraphStyle(
115
+ "ProfessionalBody",
116
+ parent=styles["Normal"],
117
+ fontSize=10,
118
+ spaceAfter=4,
119
+ fontName="Helvetica",
120
+ textColor=colors.black,
121
+ alignment=TA_JUSTIFY,
122
+ )
123
+
124
+ # Professional table style
125
+ def create_professional_table_style():
126
+ return TableStyle(
127
+ [
128
+ ("BACKGROUND", (0, 0), (0, -1), colors.lightgrey),
129
+ ("TEXTCOLOR", (0, 0), (-1, -1), colors.black),
130
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
131
+ ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
132
+ ("FONTNAME", (1, 0), (1, -1), "Helvetica"),
133
+ ("FONTSIZE", (0, 0), (-1, -1), 10),
134
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
135
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
136
+ ("LEFTPADDING", (0, 0), (-1, -1), 6),
137
+ ("RIGHTPADDING", (0, 0), (-1, -1), 6),
138
+ ("TOPPADDING", (0, 0), (-1, -1), 4),
139
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
140
+ ]
141
+ )
142
+
143
+ # Helper function to create table cells with proper text wrapping
144
+ def create_table_cell(text: str, is_header: bool = False) -> Paragraph:
145
+ style = ParagraphStyle(
146
+ "TableCell",
147
+ parent=styles["Normal"],
148
+ fontSize=10,
149
+ textColor=colors.black,
150
+ fontName="Helvetica-Bold" if is_header else "Helvetica",
151
+ leftIndent=0,
152
+ rightIndent=0,
153
+ spaceAfter=0,
154
+ spaceBefore=0,
155
+ )
156
+ return Paragraph(str(text), style)
157
+
158
+ # Build the document content
159
+ story = []
160
+
161
+ # Professional Header
162
+ story.append(Paragraph("AUTOMOBILE INSURANCE CLAIM REPORT", title_style))
163
+ story.append(Spacer(1, 12))
164
+
165
+ # Claim Information Section
166
+ claim_info_data = [
167
+ [
168
+ create_table_cell("Claim Reference Number:", True),
169
+ create_table_cell(claim_ref),
170
+ ],
171
+ [
172
+ create_table_cell("Report Generated:", True),
173
+ create_table_cell(timestamp.strftime("%B %d, %Y at %I:%M %p")),
174
+ ],
175
+ [
176
+ create_table_cell("Claim Status:", True),
177
+ create_table_cell("Under Review - Pending Adjuster Assignment"),
178
+ ],
179
+ [
180
+ create_table_cell("Processing Priority:", True),
181
+ create_table_cell(
182
+ priority.replace("🔴", "").replace("🟡", "").replace("🟢", "").strip()
183
+ ),
184
+ ],
185
+ ]
186
+
187
+ claim_info_table = Table(claim_info_data, colWidths=[2.2 * inch, 4.5 * inch])
188
+ claim_info_table.setStyle(create_professional_table_style())
189
+ story.append(claim_info_table)
190
+ story.append(Spacer(1, 16))
191
+
192
+ # Executive Summary
193
+ story.append(Paragraph("EXECUTIVE SUMMARY", header_style))
194
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
195
+ story.append(Spacer(1, 8))
196
+
197
+ summary_text = f"""
198
+ This claim involves a motor vehicle accident resulting in <b>{damage_severity.lower()}</b> damage to the
199
+ insured vehicle's <b>{damage_location.replace('-', ' ').lower()}</b> area. Based on initial assessment
200
+ of submitted photographic evidence and incident description, this appears to be a legitimate claim
201
+ {"requiring expedited processing due to reported injuries" if injuries_medical.get('anyone_injured', 'no').lower() == 'yes' else "suitable for standard processing procedures"}.
202
+ <br/><br/>
203
+ <b>Primary Recommendation:</b> {recommendation}<br/>
204
+ <b>Preliminary Cost Assessment:</b> {cost_estimate}
205
+ """
206
+
207
+ story.append(Paragraph(summary_text, body_style))
208
+ story.append(Spacer(1, 16))
209
+
210
+ # Incident Details
211
+ story.append(Paragraph("INCIDENT DETAILS", header_style))
212
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
213
+ story.append(Spacer(1, 8))
214
+
215
+ # Date, Time & Location
216
+ story.append(Paragraph("Date, Time and Location of Loss", subheader_style))
217
+
218
+ incident_data_table = [
219
+ [
220
+ create_table_cell("Date of Loss:", True),
221
+ create_table_cell(
222
+ date_location_converted.get("date", "Not specified in report")
223
+ ),
224
+ ],
225
+ [
226
+ create_table_cell("Time of Loss:", True),
227
+ create_table_cell(date_location.get("time", "Not specified in report")),
228
+ ],
229
+ [
230
+ create_table_cell("Location of Loss:", True),
231
+ create_table_cell(date_location.get("location", "Not specified in report")),
232
+ ],
233
+ ]
234
+
235
+ incident_table = Table(incident_data_table, colWidths=[2 * inch, 4.7 * inch])
236
+ incident_table.setStyle(create_professional_table_style())
237
+ story.append(incident_table)
238
+ story.append(Spacer(1, 12))
239
+
240
+ # Description of Incident
241
+ story.append(Paragraph("Description of Incident", subheader_style))
242
+ incident_desc = incident_description.get(
243
+ "what_happened", "No detailed description provided in initial report"
244
+ )
245
+ story.append(Paragraph(incident_desc, body_style))
246
+ story.append(Spacer(1, 16))
247
+
248
+ # Parties Involved
249
+ story.append(Paragraph("PARTIES INVOLVED", header_style))
250
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
251
+ story.append(Spacer(1, 8))
252
+
253
+ parties_data = [
254
+ [
255
+ create_table_cell("Other Party Driver Name:", True),
256
+ create_table_cell(
257
+ parties_involved.get("other_driver_name", "Information not provided")
258
+ ),
259
+ ],
260
+ [
261
+ create_table_cell("Other Party Vehicle:", True),
262
+ create_table_cell(
263
+ parties_involved.get("other_driver_vehicle", "Information not provided")
264
+ ),
265
+ ],
266
+ [
267
+ create_table_cell("Witness Information:", True),
268
+ create_table_cell(
269
+ parties_involved.get("witnesses", "No witnesses reported at this time")
270
+ ),
271
+ ],
272
+ ]
273
+
274
+ parties_table = Table(parties_data, colWidths=[2 * inch, 4.7 * inch])
275
+ parties_table.setStyle(create_professional_table_style())
276
+ story.append(parties_table)
277
+ story.append(Spacer(1, 16))
278
+
279
+ # Vehicle Damage Assessment
280
+ story.append(Paragraph("VEHICLE DAMAGE ASSESSMENT", header_style))
281
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
282
+ story.append(Spacer(1, 8))
283
+
284
+ # Clean up damage description - wrap long text properly
285
+ damage_desc_clean = _format_damage_description(damage_description)
286
+
287
+ damage_data = [
288
+ [
289
+ create_table_cell("Damage Severity Classification:", True),
290
+ create_table_cell(damage_severity.title()),
291
+ ],
292
+ [
293
+ create_table_cell("Primary Damage Location:", True),
294
+ create_table_cell(damage_location.replace("-", " ").title()),
295
+ ],
296
+ [
297
+ create_table_cell("Damage Description:", True),
298
+ create_table_cell(damage_desc_clean),
299
+ ],
300
+ [
301
+ create_table_cell("Preliminary Repair Estimate:", True),
302
+ create_table_cell(cost_estimate),
303
+ ],
304
+ ]
305
+
306
+ damage_table = Table(damage_data, colWidths=[2 * inch, 4.7 * inch])
307
+ damage_table.setStyle(create_professional_table_style())
308
+ story.append(damage_table)
309
+ story.append(Spacer(1, 12))
310
+
311
+ # Evidence Documentation
312
+ evidence_text = """
313
+ <b>Photographic Evidence:</b> Digital photographs of vehicle damage received and analyzed using automated assessment tools.<br/>
314
+ <b>Incident Documentation:</b> Verbal account transcribed and processed for key incident details.
315
+ """
316
+ story.append(Paragraph(evidence_text, body_style))
317
+ story.append(Spacer(1, 16))
318
+
319
+ # Injury and Medical Information
320
+ story.append(Paragraph("INJURY AND MEDICAL INFORMATION", header_style))
321
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
322
+ story.append(Spacer(1, 8))
323
+
324
+ injury_data = [
325
+ [
326
+ create_table_cell("Personal Injuries Reported:", True),
327
+ create_table_cell(
328
+ injuries_medical.get("anyone_injured", "Not specified").title()
329
+ ),
330
+ ],
331
+ [
332
+ create_table_cell("Injury Details:", True),
333
+ create_table_cell(
334
+ injuries_medical.get(
335
+ "injury_details", "No specific injury details provided"
336
+ )
337
+ ),
338
+ ],
339
+ [
340
+ create_table_cell("Medical Treatment Sought:", True),
341
+ create_table_cell(
342
+ injuries_medical.get("medical_attention", "Information not available")
343
+ ),
344
+ ],
345
+ [
346
+ create_table_cell("Injury Severity Assessment:", True),
347
+ create_table_cell(
348
+ injuries_medical.get("injury_severity", "None reported").title()
349
+ ),
350
+ ],
351
+ ]
352
+
353
+ injury_table = Table(injury_data, colWidths=[2 * inch, 4.7 * inch])
354
+ injury_table.setStyle(create_professional_table_style())
355
+ story.append(injury_table)
356
+ story.append(Spacer(1, 16))
357
+
358
+ # Liability Assessment
359
+ story.append(Paragraph("PRELIMINARY LIABILITY ASSESSMENT", header_style))
360
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
361
+ story.append(Spacer(1, 8))
362
+
363
+ fault_data = [
364
+ [
365
+ create_table_cell("Initial Fault Determination:", True),
366
+ create_table_cell(
367
+ _format_fault_determination(
368
+ fault_assessment.get("who_at_fault", "Under investigation")
369
+ )
370
+ ),
371
+ ],
372
+ [
373
+ create_table_cell("Basis for Determination:", True),
374
+ create_table_cell(
375
+ fault_assessment.get(
376
+ "reason", "Pending detailed investigation and evidence review"
377
+ )
378
+ ),
379
+ ],
380
+ ]
381
+
382
+ fault_table = Table(fault_data, colWidths=[2 * inch, 4.7 * inch])
383
+ fault_table.setStyle(create_professional_table_style())
384
+ story.append(fault_table)
385
+ story.append(Spacer(1, 16))
386
+
387
+ # Cost Analysis
388
+ story.append(Paragraph("PRELIMINARY COST ANALYSIS", header_style))
389
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
390
+ story.append(Spacer(1, 8))
391
+
392
+ medical_costs = _format_medical_costs(injuries_medical)
393
+ total_estimate = _calculate_total_estimate(cost_estimate, injuries_medical)
394
+
395
+ cost_data = [
396
+ [
397
+ create_table_cell("Vehicle Repair Estimate:", True),
398
+ create_table_cell(cost_estimate),
399
+ ],
400
+ [
401
+ create_table_cell("Medical Expense Estimate:", True),
402
+ create_table_cell(medical_costs),
403
+ ],
404
+ [
405
+ create_table_cell("Total Preliminary Estimate:", True),
406
+ create_table_cell(total_estimate),
407
+ ],
408
+ ]
409
+
410
+ cost_table = Table(cost_data, colWidths=[2 * inch, 4.7 * inch])
411
+ cost_style = create_professional_table_style()
412
+ # Highlight total row
413
+ cost_style.add("BACKGROUND", (0, 2), (1, 2), colors.lightgrey)
414
+ cost_style.add("FONTNAME", (0, 2), (1, 2), "Helvetica-Bold")
415
+ cost_table.setStyle(cost_style)
416
+
417
+ story.append(cost_table)
418
+ story.append(Spacer(1, 16))
419
+
420
+ # Action Items and Next Steps
421
+ story.append(Paragraph("RECOMMENDED ACTION ITEMS", header_style))
422
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
423
+ story.append(Spacer(1, 8))
424
+
425
+ next_steps = _generate_next_steps_professional(
426
+ damage_severity, injuries_medical, fault_assessment
427
+ )
428
+ story.append(Paragraph(next_steps, body_style))
429
+ story.append(Spacer(1, 16))
430
+
431
+ # Processing Notes
432
+ story.append(Paragraph("PROCESSING NOTES", header_style))
433
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
434
+ story.append(Spacer(1, 8))
435
+
436
+ processing_notes = f"""
437
+ This preliminary assessment was generated using automated analysis tools to expedite initial claim processing.
438
+ Photographic evidence and incident descriptions were processed using artificial intelligence to provide rapid
439
+ initial assessment. {"Given the reported injuries, this claim has been flagged for expedited human review." if injuries_medical.get('anyone_injured', 'no').lower() == 'yes' else "Standard processing timeline applies per company guidelines."}
440
+ Final claim determination requires licensed adjuster review and approval.
441
+ """
442
+
443
+ story.append(Paragraph(processing_notes, body_style))
444
+ story.append(Spacer(1, 20))
445
+
446
+ # Footer Information
447
+ footer_data = [
448
+ [
449
+ create_table_cell("Report Generated By:", True),
450
+ create_table_cell("AI Claims Processing System"),
451
+ ],
452
+ [
453
+ create_table_cell("Processing Timestamp:", True),
454
+ create_table_cell(timestamp.strftime("%I:%M %p EST")),
455
+ ],
456
+ [
457
+ create_table_cell("Human Review Status:", True),
458
+ create_table_cell("Required - Pending Assignment"),
459
+ ],
460
+ [
461
+ create_table_cell("System Confidence Level:", True),
462
+ create_table_cell("High - Standard Processing Recommended"),
463
+ ],
464
+ ]
465
+
466
+ footer_table = Table(footer_data, colWidths=[2 * inch, 4.7 * inch])
467
+ footer_table.setStyle(create_professional_table_style())
468
+ story.append(footer_table)
469
+ story.append(Spacer(1, 12))
470
+
471
+ # Evidence/Appendix Section
472
+ story.append(Paragraph("APPENDIX - EVIDENCE DOCUMENTATION", header_style))
473
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
474
+ story.append(Spacer(1, 8))
475
+
476
+ # Raw transcript section
477
+ story.append(Paragraph("Raw Incident Description Transcript", subheader_style))
478
+
479
+ # Get the original raw description
480
+ raw_description = incident_description.get(
481
+ "what_happened", "No transcript available"
482
+ )
483
+
484
+ # Create a formatted transcript
485
+ transcript_text = f"""
486
+ <b>Original Incident Account (Unedited):</b><br/><br/>
487
+ "{raw_description}"
488
+ <br/><br/>
489
+ <i>Note: This is the unedited transcript of the policyholder's account of the incident as provided during initial report.</i>
490
+ """
491
+
492
+ story.append(Paragraph(transcript_text, body_style))
493
+ story.append(Spacer(1, 12))
494
+
495
+ # Damage photo section
496
+ story.append(Paragraph("Photographic Evidence", subheader_style))
497
+
498
+ if image_path:
499
+ try:
500
+ # Add the damage photo
501
+ img = Image(image_path, width=4 * inch, height=3 * inch)
502
+ story.append(img)
503
+ story.append(Spacer(1, 8))
504
+
505
+ photo_caption = Paragraph(
506
+ "<i>Figure 1: Vehicle damage photograph submitted with initial claim report. "
507
+ "Image analyzed using automated damage assessment tools.</i>",
508
+ ParagraphStyle(
509
+ "PhotoCaption",
510
+ parent=styles["Normal"],
511
+ fontSize=9,
512
+ textColor=colors.black,
513
+ alignment=TA_CENTER,
514
+ spaceAfter=8,
515
+ ),
516
+ )
517
+ story.append(photo_caption)
518
+ except Exception as e:
519
+ # If image can't be loaded, show placeholder text
520
+ print(f"Error loading damage photo: {e}")
521
+ story.append(
522
+ Paragraph(
523
+ "Damage photograph submitted with claim (unable to display in this report format).",
524
+ body_style,
525
+ )
526
+ )
527
+ else:
528
+ story.append(
529
+ Paragraph(
530
+ "Damage photograph submitted with claim and analyzed using automated assessment tools. "
531
+ "Original digital file maintained in claim documentation system.",
532
+ body_style,
533
+ )
534
+ )
535
+
536
+ story.append(Spacer(1, 12))
537
+
538
+ # Raw damage analysis
539
+ story.append(Paragraph("Technical Damage Analysis Output", subheader_style))
540
+
541
+ # Get the raw damage description
542
+ raw_damage_analysis = damage_analysis.get(
543
+ "description", "No technical analysis available"
544
+ )
545
+
546
+ technical_analysis_text = f"""
547
+ <b>Automated Damage Assessment Output (Technical):</b><br/><br/>
548
+ {_format_technical_description(raw_damage_analysis)}
549
+ <br/><br/>
550
+ <i>Note: This is the raw output from the automated damage assessment system.
551
+ The summary version appears in the main report above.</i>
552
+ """
553
+
554
+ story.append(Paragraph(technical_analysis_text, body_style))
555
+ story.append(Spacer(1, 16))
556
+
557
+ # Legal Disclaimer
558
+ disclaimer = Paragraph(
559
+ "<i>This automated preliminary assessment is provided for initial processing purposes only. "
560
+ "All claim determinations are subject to policy terms, conditions, and coverage verification. "
561
+ "Final settlement authority rests with assigned licensed adjuster pending completion of full investigation.</i>",
562
+ ParagraphStyle(
563
+ "Disclaimer",
564
+ parent=styles["Normal"],
565
+ fontSize=8,
566
+ textColor=colors.black,
567
+ alignment=TA_CENTER,
568
+ leftIndent=0.5 * inch,
569
+ rightIndent=0.5 * inch,
570
+ ),
571
+ )
572
+ story.append(disclaimer)
573
+
574
+ # Build PDF
575
+ doc.build(story)
576
+
577
+ # Get the PDF bytes
578
+ pdf_bytes = buffer.getvalue()
579
+ buffer.close()
580
+
581
+ return pdf_bytes
582
+
583
+
584
+ def _format_damage_description(description: str) -> str:
585
+ """Clean and format damage description for professional presentation"""
586
+ if not description or len(description) < 50:
587
+ return description
588
+
589
+ # Remove redundant technical formatting
590
+ cleaned = description.replace("###", "").replace("**", "").replace("- **", "• ")
591
+
592
+ # Extract key summary if description is very long
593
+ if len(cleaned) > 300:
594
+ lines = cleaned.split("\n")
595
+ summary_lines = [
596
+ line.strip()
597
+ for line in lines
598
+ if line.strip()
599
+ and not line.strip().startswith("##")
600
+ and len(line.strip()) > 10
601
+ ][:3]
602
+ return " ".join(summary_lines[:2]) + "..."
603
+
604
+ return cleaned[:250] + "..." if len(cleaned) > 250 else cleaned
605
+
606
+
607
+ def _format_fault_determination(fault: str) -> str:
608
+ """Format fault determination for professional presentation"""
609
+ fault_map = {
610
+ "other_driver": "Other Party - Preliminary",
611
+ "policyholder": "Policyholder - Preliminary",
612
+ "unclear": "Undetermined - Investigation Required",
613
+ "both": "Comparative Negligence - Investigation Required",
614
+ }
615
+ return fault_map.get(fault.lower(), "Under Investigation")
616
+
617
+
618
+ def _get_priority_level(damage_severity: str, injuries_reported: str) -> str:
619
+ """Determine claim priority using professional terminology"""
620
+ if injuries_reported.lower() == "yes":
621
+ return "HIGH PRIORITY - Personal Injury Claim"
622
+ elif damage_severity.lower() == "major":
623
+ return "ELEVATED PRIORITY - Significant Property Damage"
624
+ elif damage_severity.lower() == "moderate":
625
+ return "STANDARD PRIORITY - Moderate Property Damage"
626
+ else:
627
+ return "ROUTINE PRIORITY - Minor Property Damage"
628
+
629
+
630
+ def _estimate_cost_range(damage_severity: str) -> str:
631
+ """Estimate repair costs based on damage severity"""
632
+ severity_costs = {
633
+ "minor": "$750 - $2,500",
634
+ "moderate": "$2,500 - $7,500",
635
+ "major": "$7,500 - $18,000",
636
+ "severe": "$18,000 - $35,000",
637
+ }
638
+ return severity_costs.get(damage_severity.lower(), "$2,000 - $5,000")
639
+
640
+
641
+ def _get_recommendation(damage_severity: str, injuries_reported: str) -> str:
642
+ """Generate professional recommendation"""
643
+ if injuries_reported.lower() == "yes":
644
+ return "IMMEDIATE ACTION REQUIRED: Assign specialist adjuster for personal injury claim within 24 hours"
645
+ elif damage_severity.lower() in ["major", "severe"]:
646
+ return "PRIORITY PROCESSING: Schedule comprehensive inspection within 48 hours"
647
+ else:
648
+ return "STANDARD PROCESSING: Assign adjuster within normal service level agreement timeframe"
649
+
650
+
651
+ def _format_medical_costs(injuries_medical: Dict[str, Any]) -> str:
652
+ """Format medical cost estimate professionally"""
653
+ if injuries_medical.get("anyone_injured", "no").lower() == "yes":
654
+ severity = injuries_medical.get("injury_severity", "minor").lower()
655
+ if severity == "severe":
656
+ return "$15,000 - $75,000 (Preliminary)"
657
+ elif severity == "moderate":
658
+ return "$3,000 - $15,000 (Preliminary)"
659
+ else:
660
+ return "$500 - $3,000 (Preliminary)"
661
+ return "No medical expenses anticipated"
662
+
663
+
664
+ def _calculate_total_estimate(
665
+ repair_cost: str, injuries_medical: Dict[str, Any]
666
+ ) -> str:
667
+ """Calculate total claim estimate professionally"""
668
+ if injuries_medical.get("anyone_injured", "no").lower() == "yes":
669
+ return f"{repair_cost} plus medical expenses (subject to investigation)"
670
+ return repair_cost
671
+
672
+
673
+ def _generate_next_steps_professional(
674
+ damage_severity: str,
675
+ injuries_medical: Dict[str, Any],
676
+ fault_assessment: Dict[str, Any],
677
+ ) -> str:
678
+ """Generate professional action items"""
679
+
680
+ steps = []
681
+
682
+ # Always required steps
683
+ steps.append(
684
+ "1. <b>Adjuster Assignment:</b> Assign licensed adjuster for detailed investigation and coverage verification."
685
+ )
686
+ steps.append(
687
+ "2. <b>Vehicle Inspection:</b> Schedule comprehensive damage assessment with approved appraiser."
688
+ )
689
+ steps.append(
690
+ "3. <b>Third Party Contact:</b> Attempt contact with other party's insurance carrier for coordination."
691
+ )
692
+
693
+ # Conditional steps based on circumstances
694
+ step_num = 4
695
+
696
+ if injuries_medical.get("anyone_injured", "no").lower() == "yes":
697
+ steps.append(
698
+ f"{step_num}. <b>Medical Documentation:</b> Request medical records and treatment documentation from healthcare providers."
699
+ )
700
+ step_num += 1
701
+ steps.append(
702
+ f"{step_num}. <b>Injury Specialist:</b> Engage personal injury specialist for claim evaluation."
703
+ )
704
+ step_num += 1
705
+
706
+ if fault_assessment.get("who_at_fault", "unclear").lower() == "unclear":
707
+ steps.append(
708
+ f"{step_num}. <b>Police Report:</b> Obtain official police report if available for liability determination."
709
+ )
710
+ step_num += 1
711
+
712
+ if damage_severity.lower() in ["major", "severe"]:
713
+ steps.append(
714
+ f"{step_num}. <b>Multiple Estimates:</b> Secure at least two independent repair estimates for cost validation."
715
+ )
716
+ step_num += 1
717
+
718
+ # Final step
719
+ steps.append(
720
+ f"{step_num}. <b>Customer Communication:</b> Contact policyholder within 24 hours to confirm receipt and outline next steps."
721
+ )
722
+
723
+ return "<br/>".join(steps)
724
+
725
+
726
+ def _convert_relative_date(date_str: str) -> str:
727
+ """Convert relative dates like 'today', 'yesterday' to actual dates"""
728
+ if not date_str or date_str.lower() in ["not specified", "unknown", ""]:
729
+ return "Not specified in report"
730
+
731
+ # Get current date
732
+ today = datetime.now()
733
+
734
+ # Dictionary of relative date conversions
735
+ relative_dates = {
736
+ "today": today.strftime("%B %d, %Y"),
737
+ "yesterday": (today - timedelta(days=1)).strftime("%B %d, %Y"),
738
+ "yesterday night": (today - timedelta(days=1)).strftime("%B %d, %Y (evening)"),
739
+ "yesterday evening": (today - timedelta(days=1)).strftime(
740
+ "%B %d, %Y (evening)"
741
+ ),
742
+ "last night": (today - timedelta(days=1)).strftime("%B %d, %Y (night)"),
743
+ "this morning": today.strftime("%B %d, %Y (morning)"),
744
+ "this afternoon": today.strftime("%B %d, %Y (afternoon)"),
745
+ "this evening": today.strftime("%B %d, %Y (evening)"),
746
+ "2 days ago": (today - timedelta(days=2)).strftime("%B %d, %Y"),
747
+ "3 days ago": (today - timedelta(days=3)).strftime("%B %d, %Y"),
748
+ "a few days ago": (today - timedelta(days=2)).strftime(
749
+ "%B %d, %Y (approximate)"
750
+ ),
751
+ "earlier today": today.strftime("%B %d, %Y (earlier)"),
752
+ "day before yesterday": (today - timedelta(days=2)).strftime("%B %d, %Y"),
753
+ }
754
+
755
+ # Check for exact matches first
756
+ date_lower = date_str.lower().strip()
757
+ if date_lower in relative_dates:
758
+ return relative_dates[date_lower]
759
+
760
+ # Check for partial matches
761
+ for relative_term, actual_date in relative_dates.items():
762
+ if relative_term in date_lower:
763
+ return actual_date
764
+
765
+ # If no relative date found, return original
766
+ return date_str
767
+
768
+
769
+ def _format_technical_description(description: str) -> str:
770
+ """Format technical damage description for appendix"""
771
+ if not description:
772
+ return "No technical analysis data available"
773
+
774
+ # Clean up technical formatting but preserve more detail than main report
775
+ cleaned = description.replace("###", "").replace("**", "")
776
+
777
+ # If it's very long, keep more content than the main report version
778
+ if len(cleaned) > 800:
779
+ # Split into sections and keep first few sections
780
+ sections = cleaned.split("\n\n")
781
+ important_sections = [
782
+ s.strip() for s in sections if s.strip() and len(s.strip()) > 20
783
+ ]
784
+ return (
785
+ "\n\n".join(important_sections[:4])
786
+ + "\n\n[Additional technical details available in system logs]"
787
+ )
788
+
789
+ return cleaned
src/modules/image_analysis.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Literal
3
+
4
+ from fireworks.llm import LLM
5
+ from src.configs.load_config import PROMPT_LIBRARY, APP_STEPS_CONFIGS
6
+ from pydantic import BaseModel
7
+ import io
8
+ import base64
9
+
10
+
11
+ class IncidentAnalysis(BaseModel):
12
+ description: str
13
+ location: Literal["front-left", "front-right", "back-left", "back-right"]
14
+ severity: Literal["minor", "moderate", "major"]
15
+ license_plate: str
16
+
17
+
18
+ def get_llm(api_key: str, model: str, temperature: float) -> LLM:
19
+ return LLM(
20
+ model=model,
21
+ temperature=temperature,
22
+ deployment_type="serverless",
23
+ api_key=api_key,
24
+ )
25
+
26
+
27
+ def pil_to_base64_dict(pil_image):
28
+ """Convert PIL image to the format expected by analyze_damage_image"""
29
+ if pil_image is None:
30
+ return None
31
+
32
+ buffered = io.BytesIO()
33
+ if pil_image.mode != "RGB":
34
+ pil_image = pil_image.convert("RGB")
35
+
36
+ pil_image.save(buffered, format="JPEG")
37
+ img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
38
+
39
+ return {"image": pil_image, "path": "uploaded_image.jpg", "base64": img_base64}
40
+
41
+
42
+ def analyze_damage_image(image, api_key: str, prompt: str = "advanced"):
43
+ """
44
+ Analyze the damage in an image using the Fireworks VLM model.
45
+ """
46
+ assert (
47
+ prompt in PROMPT_LIBRARY["vision_damage_analysis"].keys()
48
+ ), f"Invalid prompt choose from {list(PROMPT_LIBRARY['vision_damage_analysis'].keys())}"
49
+
50
+ prompt_text = PROMPT_LIBRARY["vision_damage_analysis"][prompt]
51
+
52
+ llm = get_llm(
53
+ api_key=api_key,
54
+ model=APP_STEPS_CONFIGS.analyze_damage_image.model,
55
+ temperature=APP_STEPS_CONFIGS.analyze_damage_image.temperature,
56
+ )
57
+ response = llm.chat.completions.create(
58
+ messages=[
59
+ {
60
+ "role": "user",
61
+ "content": [
62
+ {
63
+ "type": "image_url",
64
+ "image_url": {
65
+ "url": f"data:image/jpeg;base64,{image['base64']}"
66
+ },
67
+ },
68
+ {"type": "text", "text": prompt_text},
69
+ ],
70
+ }
71
+ ],
72
+ response_format={
73
+ "type": "json_object",
74
+ "schema": IncidentAnalysis.model_json_schema(),
75
+ },
76
+ )
77
+
78
+ result = json.loads(response.choices[0].message.content)
79
+ return result
src/modules/incident_processing.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.image_analysis import get_llm
2
+ from src.configs.load_config import PROMPT_LIBRARY, APP_STEPS_CONFIGS
3
+ from pydantic import BaseModel
4
+ from typing import Optional, Literal, List, Dict, Any
5
+ import json
6
+ import random
7
+ import time
8
+
9
+
10
+ class DateLocation(BaseModel):
11
+ date: Optional[str] = None
12
+ time: Optional[str] = None
13
+ location: Optional[str] = None
14
+
15
+
16
+ class PartiesInvolved(BaseModel):
17
+ other_driver_name: Optional[str] = None
18
+ other_driver_vehicle: Optional[str] = None
19
+ witnesses: Optional[str] = None
20
+
21
+
22
+ class FaultAssessment(BaseModel):
23
+ who_at_fault: Literal["me", "other_driver", "unclear"]
24
+ reason: str
25
+
26
+
27
+ class IncidentDescription(BaseModel):
28
+ what_happened: str
29
+ damage_severity: Literal["minor", "moderate", "severe", "unclear"]
30
+
31
+
32
+ class InjuriesMedical(BaseModel):
33
+ anyone_injured: Literal["yes", "no", "unknown"]
34
+ injury_details: Optional[str] = None
35
+ medical_attention: Optional[str] = None
36
+ injury_severity: Optional[Literal["none", "minor", "moderate", "severe", "unclear"]]
37
+
38
+
39
+ class FunctionCallResult(BaseModel):
40
+ function_name: str
41
+ result: Dict[str, Any]
42
+ status: Literal["success", "error"]
43
+ message: str
44
+
45
+
46
+ class IncidentAnalysis(BaseModel):
47
+ date_location: DateLocation
48
+ parties_involved: PartiesInvolved
49
+ fault_assessment: FaultAssessment
50
+ incident_description: IncidentDescription
51
+ injuries_medical: InjuriesMedical
52
+ function_calls_made: List[FunctionCallResult] = []
53
+ external_data_retrieved: Dict[str, Any] = {}
54
+
55
+
56
+ def mock_weather_lookup(date: str, location: str) -> Dict[str, Any]:
57
+ """Mock function to look up weather conditions for a specific date and location"""
58
+ time.sleep(0.5) # Simulate API call delay
59
+
60
+ weather_conditions = [
61
+ "Clear",
62
+ "Rainy",
63
+ "Foggy",
64
+ "Snowy",
65
+ "Overcast",
66
+ "Partly Cloudy",
67
+ ]
68
+ temperatures = range(20, 85)
69
+
70
+ return {
71
+ "date": date,
72
+ "location": location,
73
+ "temperature": f"{random.choice(temperatures)}°F",
74
+ "conditions": random.choice(weather_conditions),
75
+ "visibility": random.choice(["Good", "Poor", "Fair"]),
76
+ "precipitation": random.choice(["None", "Light Rain", "Heavy Rain", "Snow"]),
77
+ "wind_speed": f"{random.randint(0, 25)} mph",
78
+ }
79
+
80
+
81
+ def mock_driver_record_check(
82
+ driver_name: str, license_plate: str = None
83
+ ) -> Dict[str, Any]:
84
+ """Mock function to check driver record and vehicle registration"""
85
+ risk_levels = ["Low", "Medium", "High"]
86
+
87
+ return {
88
+ "driver_found": True,
89
+ "license_status": random.choice(["Valid", "Suspended", "Expired"]),
90
+ "insurance_status": random.choice(["Active", "Lapsed", "Unknown"]),
91
+ "previous_claims": random.randint(0, 5),
92
+ "violations_last_3_years": random.randint(0, 3),
93
+ "risk_assessment": random.choice(risk_levels),
94
+ "vehicle_registration": "Valid" if license_plate else "Not checked",
95
+ }
96
+
97
+
98
+ AVAILABLE_FUNCTIONS = {
99
+ "weather_lookup": {
100
+ "name": "weather_lookup",
101
+ "description": "Look up weather conditions for a specific date and location to understand incident context",
102
+ "parameters": {
103
+ "type": "object",
104
+ "properties": {
105
+ "date": {"type": "string", "description": "Date of the incident"},
106
+ "location": {
107
+ "type": "string",
108
+ "description": "Location where incident occurred",
109
+ },
110
+ },
111
+ "required": ["date", "location"],
112
+ },
113
+ "function": mock_weather_lookup,
114
+ },
115
+ "driver_record_check": {
116
+ "name": "driver_record_check",
117
+ "description": "Check driving record and insurance status of other party involved",
118
+ "parameters": {
119
+ "type": "object",
120
+ "properties": {
121
+ "driver_name": {
122
+ "type": "string",
123
+ "description": "Name of the other driver",
124
+ },
125
+ "license_plate": {
126
+ "type": "string",
127
+ "description": "License plate number if available",
128
+ },
129
+ },
130
+ "required": ["driver_name"],
131
+ },
132
+ "function": mock_driver_record_check,
133
+ },
134
+ }
135
+
136
+
137
+ def execute_function_call(
138
+ function_name: str, parameters: Dict[str, Any]
139
+ ) -> FunctionCallResult:
140
+ """Execute a function call and return the result"""
141
+ try:
142
+ if function_name not in AVAILABLE_FUNCTIONS:
143
+ return FunctionCallResult(
144
+ function_name=function_name,
145
+ result={},
146
+ status="error",
147
+ message=f"Function {function_name} not found",
148
+ )
149
+
150
+ function_impl = AVAILABLE_FUNCTIONS[function_name]["function"]
151
+ result = function_impl(**parameters)
152
+
153
+ return FunctionCallResult(
154
+ function_name=function_name,
155
+ result=result,
156
+ status="success",
157
+ message=f"Successfully executed {function_name}",
158
+ )
159
+
160
+ except Exception as e:
161
+ return FunctionCallResult(
162
+ function_name=function_name,
163
+ result={},
164
+ status="error",
165
+ message=f"Error executing {function_name}: {str(e)}",
166
+ )
167
+
168
+
169
+ def process_transcript_description(transcript: str, api_key: str):
170
+ """
171
+ Analyze the provided transcript and extract structured information for insurance claim processing.
172
+ Now includes function calling capabilities to gather additional context.
173
+
174
+ Args:
175
+ transcript: transcript string to process
176
+ api_key: api key to use
177
+
178
+ Returns:
179
+ incident_description: incident description with function call results
180
+ """
181
+ print("Starting incident analysis with function calling...")
182
+
183
+ llm = get_llm(
184
+ api_key=api_key,
185
+ model="accounts/fireworks/models/llama4-scout-instruct-basic",
186
+ temperature=APP_STEPS_CONFIGS.incident_response.temperature,
187
+ )
188
+
189
+ # Enhanced prompt that includes function calling
190
+ prompt_text = f"""
191
+ {PROMPT_LIBRARY["incident_processing"]["advanced"]}
192
+
193
+ **ADDITIONAL CAPABILITIES:**
194
+ You now have access to external functions that can help gather additional context for the claim.
195
+ Based on the transcript, you should consider calling these functions if the information would be helpful:
196
+
197
+ Available Functions:
198
+ {json.dumps([{k: {kk: vv for kk, vv in v.items() if kk != 'function'}} for k, v in AVAILABLE_FUNCTIONS.items()], indent=2)}
199
+
200
+ **IMPORTANT:**
201
+ 1. First, extract all the basic incident information from the transcript
202
+ 2. Then determine which functions (if any) would provide helpful additional context
203
+ 3. For each function you want to call, include a "function_calls" section in your response
204
+ 4. I will execute the functions and provide you with the results
205
+ 5. You will then incorporate those results into your final analysis
206
+
207
+ **TRANSCRIPT TO ANALYZE:**
208
+ <transcript>
209
+ {transcript}
210
+ </transcript>
211
+
212
+ Please provide your initial analysis and specify any function calls you'd like to make.
213
+ """
214
+
215
+ # First pass - get initial analysis and any function calls
216
+ response = llm.chat.completions.create(
217
+ messages=[
218
+ {
219
+ "role": "system",
220
+ "content": "You are an expert automotive claims adjuster analyzing vehicle damage with access to external data sources.",
221
+ },
222
+ {"role": "user", "content": prompt_text},
223
+ ],
224
+ response_format={
225
+ "type": "json_object",
226
+ "schema": IncidentAnalysis.model_json_schema(),
227
+ },
228
+ temperature=APP_STEPS_CONFIGS.incident_response.temperature,
229
+ )
230
+
231
+ # Parse initial response
232
+ incident_data = IncidentAnalysis.model_validate_json(
233
+ response.choices[0].message.content
234
+ )
235
+
236
+ print("Initial analysis complete. Checking for function calls...")
237
+
238
+ function_calls_to_make = []
239
+ external_data = {}
240
+
241
+ # Making it more robust by checking for necessary inputs
242
+ if (
243
+ incident_data.date_location.date
244
+ and incident_data.date_location.location
245
+ and incident_data.date_location.date.lower()
246
+ not in ["not specified", "unknown", ""]
247
+ ):
248
+ function_calls_to_make.append(
249
+ {
250
+ "name": "weather_lookup",
251
+ "params": {
252
+ "date": incident_data.date_location.date,
253
+ "location": incident_data.date_location.location,
254
+ },
255
+ }
256
+ )
257
+
258
+ if (
259
+ incident_data.parties_involved.other_driver_name
260
+ and incident_data.parties_involved.other_driver_name.lower()
261
+ not in ["information not provided", "not specified", ""]
262
+ ):
263
+ function_calls_to_make.append(
264
+ {
265
+ "name": "driver_record_check",
266
+ "params": {
267
+ "driver_name": incident_data.parties_involved.other_driver_name,
268
+ "license_plate": incident_data.parties_involved.other_driver_vehicle,
269
+ },
270
+ }
271
+ )
272
+
273
+ # Execute tool calls
274
+ function_results = []
275
+ if function_calls_to_make:
276
+ print(f"Executing {len(function_calls_to_make)} function calls...")
277
+
278
+ for call in function_calls_to_make:
279
+ print(f" - Calling {call['name']}...")
280
+ result = execute_function_call(call["name"], call["params"])
281
+ function_results.append(result)
282
+
283
+ if result.status == "success":
284
+ external_data[call["name"]] = result.result
285
+
286
+ incident_data.function_calls_made = function_results
287
+ incident_data.external_data_retrieved = external_data
288
+
289
+ # Update analysis with external data pulled from tools
290
+ if function_results:
291
+ print("Incorporating external data into final analysis...")
292
+
293
+ enhancement_prompt = f"""
294
+ Based on the initial incident analysis and the additional data retrieved from external sources,
295
+ please provide an enhanced analysis that incorporates this new information.
296
+
297
+ **INITIAL ANALYSIS:**
298
+ {incident_data.model_dump_json(indent=2)}
299
+
300
+ **EXTERNAL DATA RETRIEVED:**
301
+ {json.dumps(external_data, indent=2)}
302
+
303
+ Please update your analysis to incorporate insights from this external data where relevant.
304
+ For example:
305
+ - Weather conditions might affect fault assessment
306
+ - Other traffic incidents might provide context
307
+ - Driver record might influence risk assessment
308
+ - Medical facility info might be relevant for injury cases
309
+
310
+ Provide the complete updated analysis.
311
+ """
312
+
313
+ enhanced_response = llm.chat.completions.create(
314
+ messages=[
315
+ {
316
+ "role": "system",
317
+ "content": "You are an expert automotive claims adjuster incorporating external data into your analysis.",
318
+ },
319
+ {"role": "user", "content": enhancement_prompt},
320
+ ],
321
+ response_format={
322
+ "type": "json_object",
323
+ "schema": IncidentAnalysis.model_json_schema(),
324
+ },
325
+ temperature=APP_STEPS_CONFIGS.incident_response.temperature,
326
+ )
327
+
328
+ enhanced_data = IncidentAnalysis.model_validate_json(
329
+ enhanced_response.choices[0].message.content
330
+ )
331
+
332
+ enhanced_data.function_calls_made = function_results
333
+ enhanced_data.external_data_retrieved = external_data
334
+ incident_data = enhanced_data
335
+
336
+ print("Finished incident analysis with function calling.")
337
+ return incident_data
src/modules/transcription.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import threading
3
+ import time
4
+ import urllib.parse
5
+ from typing import Optional, Callable
6
+ import websocket
7
+
8
+
9
+ class FireworksTranscription:
10
+ """Fireworks AI transcription for Gradio integration."""
11
+
12
+ WEBSOCKET_URL = "ws://audio-streaming.us-virginia-1.direct.fireworks.ai/v1/audio/transcriptions/streaming"
13
+
14
+ def __init__(self, api_key: str):
15
+ self.api_key = api_key
16
+ self.websocket_client = None
17
+ self.is_connected = False
18
+ self.segments = {}
19
+ self.lock = threading.Lock()
20
+ self.transcription_callback: Optional[Callable[[str], None]] = None
21
+
22
+ def set_callback(self, callback: Callable[[str], None]):
23
+ """Set callback to receive live transcription updates."""
24
+ self.transcription_callback = callback
25
+
26
+ def _connect(self) -> bool:
27
+ """Connect to Fireworks WebSocket."""
28
+ try:
29
+ params = urllib.parse.urlencode({"language": "en"})
30
+ full_url = f"{self.WEBSOCKET_URL}?{params}"
31
+
32
+ self.websocket_client = websocket.WebSocketApp(
33
+ full_url,
34
+ header={"Authorization": self.api_key},
35
+ on_open=self._on_open,
36
+ on_message=self._on_message,
37
+ on_error=self._on_error,
38
+ )
39
+
40
+ # Start WebSocket in background thread
41
+ ws_thread = threading.Thread(
42
+ target=self.websocket_client.run_forever, daemon=True
43
+ )
44
+ ws_thread.start()
45
+
46
+ # Wait for connection (max 5 seconds)
47
+ timeout = 5
48
+ start_time = time.time()
49
+ while not self.is_connected and (time.time() - start_time) < timeout:
50
+ time.sleep(0.1)
51
+
52
+ return self.is_connected
53
+
54
+ except Exception as e:
55
+ print(f"Connection error: {e}")
56
+ return False
57
+
58
+ def _send_audio_chunk(self, chunk: bytes) -> bool:
59
+ """Send audio chunk to Fireworks."""
60
+ if not self.is_connected or not self.websocket_client:
61
+ return False
62
+
63
+ try:
64
+ self.websocket_client.send(chunk, opcode=websocket.ABNF.OPCODE_BINARY)
65
+ return True
66
+ except Exception as e:
67
+ print(f"Error sending audio chunk: {e}")
68
+ return False
69
+
70
+ def _on_open(self, ws):
71
+ """Handle WebSocket connection opening."""
72
+ self.is_connected = True
73
+ print("✅ Connected to Fireworks transcription service")
74
+
75
+ def _on_message(self, ws, message):
76
+ """Handle transcription messages from Fireworks."""
77
+ try:
78
+ data = json.loads(message)
79
+
80
+ # Process segments
81
+ if "segments" in data:
82
+ with self.lock:
83
+ # Update segments
84
+ for segment in data["segments"]:
85
+ segment_id = segment["id"]
86
+ text = segment["text"]
87
+ self.segments[segment_id] = text
88
+
89
+ # Build complete current transcription
90
+ complete_text = self._build_complete_text()
91
+
92
+ # Call callback with live update
93
+ if self.transcription_callback and complete_text.strip():
94
+ self.transcription_callback(complete_text)
95
+
96
+ except json.JSONDecodeError as e:
97
+ print(f"Failed to parse message: {e}")
98
+ except Exception as e:
99
+ print(f"Error processing message: {e}")
100
+
101
+ @staticmethod
102
+ def _on_error(ws, error):
103
+ """Handle WebSocket errors."""
104
+ print(f"WebSocket error: {error}")
105
+
106
+ def _build_complete_text(self) -> str:
107
+ """Build complete text from all segments."""
108
+ if not self.segments:
109
+ return ""
110
+
111
+ sorted_segments = sorted(self.segments.items(), key=lambda x: int(x[0]))
112
+ return " ".join(segment[1] for segment in sorted_segments if segment[1].strip())