Spaces:
Running
Running
RobertoBarrosoLuque
commited on
Commit
·
e954acb
1
Parent(s):
ab4adfa
Ready
Browse files- .gitignore +227 -0
- .pre-commit-config.yaml +48 -0
- Makefile +26 -0
- README.md +127 -14
- assets/fireworks_logo.png +0 -0
- notebooks/1-Building-Blocks.ipynb +0 -0
- notebooks/2-Exercises.ipynb +470 -0
- notebooks/3-Fine-Tuning.ipynb +49 -0
- pyproject.toml +23 -0
- scripts/create_venv.sh +13 -0
- scripts/install_uv.sh +19 -0
- scripts/setup_ssl.py +97 -0
- src/__init__.py +0 -0
- src/app.py +633 -0
- src/configs/config.yaml +11 -0
- src/configs/config_models.py +12 -0
- src/configs/load_config.py +51 -0
- src/configs/prompt_library.yaml +87 -0
- src/modules/__init__.py +0 -0
- src/modules/claim_processing.py +789 -0
- src/modules/image_analysis.py +79 -0
- src/modules/incident_processing.py +337 -0
- src/modules/transcription.py +112 -0
.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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+

|
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())
|