Repository navigation
Feature Proposal: AI-Powered Gemini Semantic Email Classifier #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
38ead62
docs: add AI-Powered Gemini Semantic Classifier feature proposal
don-petry 06794e1
docs: update proposal with 7-Domain Standard Taxonomy Matrix & MECE V…
don-petry fa96d54
docs: add Inbox Preservation rule (processing to Drive does NOT remov…
don-petry 2496323
docs: resolve merge conflict and add Inbox Preservation rule
don-petry 19ae580
docs: use Single Global Processed Label marker to eliminate sidebar l…
don-petry b78ac1d
docs: detail Idempotency & Single-Click Reprocessing workflows
don-petry 8021d5a
feat: add native Gemini AI classifier and auto-filter engine module
don-petry cd7e914
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot 034b23c
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot f1a8dc9
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot 15ee6da
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot d8154d3
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot 6ffb452
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot 4996a6e
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot bae0762
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot f1ae720
fix(reviews): address review comments [skip ci-relay]
donpetry-bot 3e934e5
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot 05db839
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot ccf9ad5
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot 257460e
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot 995b18a
docs: detail Unclassified & Edge-Case email handling safety nets
don-petry 3fcd9cc
feat: add native Google Apps Script GitHub Sync module (UrlFetchApp -…
don-petry 1d65a71
chore(ci): apply prettier/eslint auto-fixes
github-actions[bot] c1ab1ba
fix(bot): address bot feedback [skip ci-relay]
donpetry-bot 899ad30
fix(bot): address bot feedback [skip ci-relay]
donpetry-bot d5f21ee
test: add performance and scalability benchmark test suite for long d…
don-petry ee1ae05
feat: enhance gitHubSync with targeted insertion and 409 SHA collisio…
don-petry f06010c
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot 64b46ec
feat: add Option A multi-account user email resolution and deployment…
don-petry b41cdd1
chore(ci): apply prettier/eslint auto-fixes
github-actions[bot] 48c8a3e
feat: add modelEndpoint to classifier config
donpetry-bot f1f8aa1
feat: add appsscript.json manifest for clasp deployment
don-petry 581cc10
chore: add .claspignore to exclude Node.js Jest test files from Apps …
don-petry 6f6185d
fix: export getAiClassifierConfig and standalone GAS logic
don-petry bcdde8f
fix: safe Gemini API HTTP status checking and response guards
don-petry ea2bf36
fix: update Gemini API endpoint to gemini-2.0-flash with automatic fa…
don-petry db9dad7
feat: add listAvailableGeminiModels helper and expanded model endpoints
don-petry 3c76f60
feat: set gemini-3.1-flash-lite as 2nd fallback endpoint
don-petry ea307b3
feat: add 404 auto-create note functionality with Progressive Disclos…
don-petry e75dc95
feat: reorder endpoints to highest-level model first and add HTTP 429…
don-petry 0cb6756
feat: set Gemini 3.5 Flash Lite as primary endpoint #1
don-petry 184fb59
feat: set Gemini 3.5 Flash Lite as #1 and Gemini 3.1 Flash Lite as #2
don-petry 8ecfaab
chore: remove deprecated 1.x and 2.x Gemini models, use 3.x models only
don-petry 466fad3
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot 683d5a9
feat: add Family/DJ-Rachel sub-label and strict prompt classification…
don-petry dff51b9
feat: exclude health newsletters and news bulletins from 04_Family_He…
don-petry b566b95
feat: add setupFiveMinuteTrigger and stopAllTriggers for automated ba…
don-petry 1656f46
feat: map student college/career planning meetings to Family/School
don-petry d813331
feat: map MyBroodMinder alerts to 07_Community_NonProfit with sub-lab…
don-petry 7037ee1
feat: map ParentSquare and MCAA school emails to Family/School-Toby
don-petry 4befa97
fix: add 2-second rate limit backoff delay on HTTP 429 and stabilize …
don-petry c0408e0
feat: parse and honor Retry-After header and retryDelay on HTTP 429
don-petry ba84dfb
chore: strictly enforce Gemini 3.x models ONLY
don-petry 28404ed
feat: add Gemma 4 31B (gemma-4-31b-it) as primary endpoint #1
don-petry 57590f5
fix: add extractJsonSubstring helper to parse Gemma 4 31B markdown fo…
don-petry a763bd6
feat: exclude tech webinars and commercial store marketing from core …
don-petry 1ab8558
feat: add automated action execution (trash, archive, mark_read) and …
don-petry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
74 changes: 74 additions & 0 deletions
74
docs/proposals/proposal-001-ai-gemini-semantic-classifier.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Feature Proposal 001: Native Gemini AI Semantic Email Classifier & 7-Domain Taxonomy Engine | ||
|
|
||
| ## Executive Summary | ||
| This proposal introduces a native **Gemini AI Semantic Email Classifier & Standard Taxonomy Engine** into the `google-app-scripts` suite. | ||
|
|
||
| By calling the Gemini REST API (`gemini-1.5-flash` / `gemini-2.0-flash`) natively via Google Apps Script's `UrlFetchApp.fetch()`, incoming unclassified emails are semantically categorized into **7 Standard Canonical Domains**, and high-confidence classifications automatically generate permanent Gmail filter rules via the Advanced Gmail Service. | ||
|
|
||
| --- | ||
|
|
||
| ## Idempotency & Reprocessing Workflows | ||
|
|
||
| - **Query Guard**: Ingestion routines process threads matching `label:Category -label:Processed`. | ||
| - **Deduplication Safety Nets**: MD5 native checksums (`file.getMd5Checksum()`) prevent duplicate attachment saves; thread markers (`[THREAD:id]`) prevent duplicate text logs. | ||
| - **Single-Click Reprocessing**: Removing the `Processed` label from an email thread in Gmail causes the script to re-evaluate, re-clean, and re-process the thread on its next run. | ||
|
|
||
| --- | ||
|
|
||
| ## Clean Ingestion Marker: Single Global `Processed` Label | ||
|
|
||
| - **Eliminating Duplicate Labels**: Rather than creating parallel `-Processed` labels (which doubles sidebar labels from 20 to 40+), the script applies a **single global `Processed` label** (or Green Star badge) to indicate Drive ingestion completion. | ||
| - **Category Preservation**: The thread retains its canonical category label (`Family/School-Toby`, `Finance/Taxes`). | ||
| - **Inbox Preservation**: Processing an email into Google Drive **MUST NOT remove the message from the user's INBOX**. The email thread remains visible in the Inbox for human review until explicitly archived or deleted by the user. | ||
|
|
||
| --- | ||
|
|
||
| ## Standard 7-Domain Taxonomy Matrix (PARA / ISO 15489 / MECE Aligned) | ||
|
|
||
| The classifier operates against a standardized, mutually exclusive, collectively exhaustive (MECE) 7-domain taxonomy: | ||
|
|
||
| | 7 Canonical Domains | Standard PARA Area | Scope & Contents | | ||
| | :--- | :--- | :--- | | ||
| | **`01_Household`** | Real Estate & Living Space | Primary residence (2809 Five Oaks), shop build, rental (3535 Broken Bow), 18 Running Deer archive | | ||
| | **`02_Finance_Legal`** | Financial & Legal Protections | Taxes, banking (Schwab/BoA), insurance policies, court records, custody, estate planning | | ||
| | **`03_Vehicles`** | Transportation & Assets | Vehicle searches (2026 car hunt), vehicle titles, Mercedes logs, auto maintenance | | ||
| | **`04_Family_Health`** | Family Welfare & Health | Children (Toby, Tide), school IEPs, speech/audiology reports, medical history, AYOP camps | | ||
| | **`05_Tech_Infrastructure`**| IT & Home Automation | Home Assistant, NAS backups, Tasker scripts, network gear, software licenses | | ||
| | **`06_Work_Career`** | Professional Life & Income | EY architecture, resumes, job interview decks, expense reports, admin notes | | ||
| | **`07_Community_NonProfit`**| Civic Duty & Service | HelpingOneGuy 501(c)(3) charity, JeffCo Bees BOD, faith outreach | | ||
|
|
||
| --- | ||
|
|
||
| ## Key Capabilities | ||
|
|
||
| 1. **Zero-Configuration Semantic Classification**: | ||
| - Classifies incoming emails from unknown senders based on semantic intent, sender, subject, and body snippet into target canonical labels (`Family/School`, `Finance/Taxes`, `Household/Rentals`, etc.). | ||
| 2. **Self-Learning Gmail Filter Generation**: | ||
| - When Gemini classifies a new sender with high confidence ($\ge 95\%$), the script automatically calls `Gmail.Users.Settings.Filters.create` to create a permanent static Gmail filter rule. Future emails from that sender are processed instantly by Gmail's fast native filter engine at $0$ cost. | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| 3. **Single-Click Reprocessing**: | ||
| - Simply remove the `Processed` label from any thread to re-run and re-ingest. | ||
| 4. **100% Cloud-Native & Serverless**: | ||
| - Executes natively inside Google Apps Script via `UrlFetchApp.fetch()`. Requires zero external servers, Docker containers, or Python infrastructure. | ||
|
|
||
| --- | ||
|
|
||
| ## Code Module Design | ||
|
|
||
| Target file structure in `google-app-scripts`: | ||
|
|
||
| ``` | ||
| src/gmail-ai-classifier/ | ||
| ├── README.md | ||
| ├── code.gs | ||
| ├── config.gs | ||
| └── tests/ | ||
| ├── code.test.js | ||
| └── integration.test.js | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Discussion & Tracking | ||
| - Issue Tracked: [#491](https://github.com/petry-projects/google-app-scripts/issues/491) | ||
| - Pull Request Tracked: [#492](https://github.com/petry-projects/google-app-scripts/pull/492) | ||
| - Repository: `petry-projects/google-app-scripts` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Gmail AI Classifier & Auto-Filter Engine | ||
|
|
||
| A native Google Apps Script that uses the Gemini REST API (`gemini-1.5-flash` / `gemini-2.0-flash`) to semantically classify incoming Gmail messages into standard canonical domain labels and automatically generate permanent Gmail filter rules for new senders. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Semantic Email Classification**: Evaluates incoming email senders, subjects, and text snippets to route messages into canonical domains. | ||
| - **Single Global `Processed` Marker**: Applies a single global `Processed` label to indicate Drive ingestion completion without cluttering the sidebar. | ||
| - **Inbox Preservation**: Processing emails into Google Drive preserves message visibility in the INBOX for human review. | ||
| - **Self-Learning Filter Creation**: Automatically generates permanent static Gmail filter rules via the Advanced Gmail API when confidence $\ge 95\%$. | ||
| - **100% Cloud-Native**: Runs natively inside Google Apps Script using `UrlFetchApp.fetch()`. | ||
|
|
||
| ## Configuration (`config.gs`) | ||
|
|
||
| Set your Gemini API key in Google Apps Script **Project Settings -> Script Properties** with key `GEMINI_API_KEY`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| // @version 1.0.0 | ||
| /** | ||
| * Main entry point for AI-Powered Semantic Email Classification and Auto-Filter Generation. | ||
| */ | ||
| function processEmailsWithAiClassifier() { | ||
| console.log('[processEmailsWithAiClassifier] Starting AI semantic email processing'); | ||
| var config = getAiClassifierConfig(); | ||
|
|
||
| var threads = GmailApp.search(config.unprocessedQuery, 0, 15); | ||
| console.log('[processEmailsWithAiClassifier] Found', threads.length, 'unprocessed threads'); | ||
|
|
||
| if (threads.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| var processedLabel = ensureUserLabel(config.processedLabel); | ||
|
|
||
| threads.forEach(function (thread, index) { | ||
| console.log('[processEmailsWithAiClassifier] Processing thread', index + 1, 'of', threads.length); | ||
| var messages = thread.getMessages(); | ||
| if (!messages || messages.length === 0) return; | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| var latestMsg = messages[messages.length - 1]; | ||
| var sender = latestMsg.getFrom(); | ||
| var subject = latestMsg.getSubject(); | ||
| var bodySnippet = latestMsg.getPlainBody() ? latestMsg.getPlainBody().substring(0, 1200) : ''; | ||
|
|
||
| var classification = classifyWithGemini(config, sender, subject, bodySnippet); | ||
| if (!classification || !classification.canonical_label) { | ||
| console.log('[processEmailsWithAiClassifier] Could not classify thread:', thread.getId()); | ||
| return; | ||
| } | ||
|
|
||
| console.log('[processEmailsWithAiClassifier] Classified as:', classification.canonical_label, '(Confidence:', classification.confidence, ')'); | ||
|
|
||
| // 1. Apply Canonical Category Label (Preserves INBOX visibility) | ||
| var categoryLabel = ensureUserLabel(classification.canonical_label); | ||
| thread.addLabel(categoryLabel); | ||
|
|
||
| // 2. Apply Single Global Processed Marker Label | ||
| thread.addLabel(processedLabel); | ||
|
|
||
| // 3. Auto-Create Permanent Gmail Filter Rule if Confidence >= Threshold | ||
| if (classification.confidence >= config.autoFilterConfidenceThreshold) { | ||
| createGmailFilterRule(sender, classification.canonical_label); | ||
| } | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| }); | ||
|
|
||
| console.log('[processEmailsWithAiClassifier] AI processing batch completed'); | ||
| } | ||
|
|
||
| /** | ||
| * Calls Gemini REST API natively via UrlFetchApp | ||
| */ | ||
| function classifyWithGemini(config, sender, subject, bodyText) { | ||
| var url = config.modelEndpoint + '?key=' + config.geminiApiKey; | ||
|
|
||
| var prompt = 'You are an executive email classifier.\n' + | ||
| 'Classify the following email into exactly ONE of these canonical domain labels:\n' + | ||
| config.canonicalDomains.join('\n') + '\n\n' + | ||
| 'Email Details:\n' + | ||
| 'Sender: ' + sender + '\n' + | ||
| 'Subject: ' + subject + '\n' + | ||
| 'Body Snippet: ' + bodyText + '\n\n' + | ||
| 'Respond ONLY in JSON format matching this schema:\n' + | ||
| '{"canonical_label": "Domain/Subfolder", "confidence": 0.98, "reasoning": "short explanation"}'; | ||
|
|
||
| var payload = { | ||
| contents: [{ parts: [{ text: prompt }] }], | ||
| generationConfig: { response_mime_type: 'application/json' } | ||
| }; | ||
|
|
||
| var options = { | ||
| method: 'post', | ||
| contentType: 'application/json', | ||
| payload: JSON.stringify(payload), | ||
| muteHttpExceptions: true | ||
| }; | ||
|
|
||
| try { | ||
| var response = UrlFetchApp.fetch(url, options); | ||
| var jsonText = response.getContentText(); | ||
| var parsed = JSON.parse(jsonText); | ||
| var outputText = parsed.candidates[0].content.parts[0].text; | ||
| return JSON.parse(outputText); | ||
| } catch (e) { | ||
| console.error('[classifyWithGemini] Error calling Gemini REST API:', e.message); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates permanent static Gmail filter rule via Advanced Gmail Service API | ||
| */ | ||
| function createGmailFilterRule(senderEmail, labelName) { | ||
| try { | ||
| var cleanSender = senderEmail.replace(/.*<(.+)>/, '$1').trim(); | ||
| var targetLabel = ensureUserLabel(labelName); | ||
|
|
||
| var filterBody = { | ||
| criteria: { from: cleanSender }, | ||
| action: { addLabelIds: [targetLabel.getId()] } | ||
| }; | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| if (typeof Gmail !== 'undefined' && Gmail.Users && Gmail.Users.Settings && Gmail.Users.Settings.Filters) { | ||
| Gmail.Users.Settings.Filters.create(filterBody, 'me'); | ||
| console.log('[createGmailFilterRule] Permanent Gmail filter created for:', cleanSender, '->', labelName); | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
| } else { | ||
| console.log('[createGmailFilterRule] Advanced Gmail service not enabled. Auto-filter creation skipped.'); | ||
| } | ||
| } catch (e) { | ||
| console.error('[createGmailFilterRule] Error creating filter:', e.message); | ||
| } | ||
| } | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| /** | ||
| * Helper to ensure a Gmail user label exists | ||
| */ | ||
| function ensureUserLabel(labelName) { | ||
| var existing = GmailApp.getUserLabelByName(labelName); | ||
| if (existing) return existing; | ||
| try { | ||
| return GmailApp.createLabel(labelName); | ||
| } catch (e) { | ||
| console.error('[ensureUserLabel] Error creating label:', labelName, e.message); | ||
| return null; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /** | ||
| * Configuration for Gmail AI Classifier & Auto-Filter Engine | ||
| */ | ||
| function getAiClassifierConfig() { | ||
| return { | ||
| // Gemini API Key (stored in ScriptProperties or set here) | ||
| geminiApiKey: PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY') || 'YOUR_GEMINI_API_KEY', | ||
|
|
||
| // Model Endpoint (Gemini 1.5 Flash / Gemini 2.0 Flash) | ||
| modelEndpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent', | ||
|
|
||
| // Trigger Label applied to unprocessed inbox items | ||
| unprocessedQuery: 'inbox -label:Processed', | ||
|
|
||
| // Single Global Ingestion Marker Label | ||
| processedLabel: 'Processed', | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| // Minimum confidence threshold (0.0 - 1.0) required to auto-create permanent Gmail filter rules | ||
| autoFilterConfidenceThreshold: 0.95, | ||
|
|
||
| // 7 Canonical Domains for Semantic Classification | ||
| canonicalDomains: [ | ||
| '01_Household/Primary_House', | ||
| '01_Household/Shop_Build', | ||
| '01_Household/Rental_3535_Broken_Bow', | ||
| '01_Household/Archive_18_Running_Deer', | ||
| '02_Finance_Legal/Taxes', | ||
| '02_Finance_Legal/Insurance', | ||
| '02_Finance_Legal/Banking', | ||
| '02_Finance_Legal/Legal_Court', | ||
| '02_Finance_Legal/Estate_Planning', | ||
| '03_Vehicles/Car_Hunt', | ||
| '03_Vehicles/Mercedes', | ||
| '03_Vehicles/Maintenance', | ||
| '04_Family_Health/Bowens', | ||
| '04_Family_Health/Charley', | ||
| '04_Family_Health/David', | ||
| '04_Family_Health/DJ', | ||
| '04_Family_Health/Medical_Records', | ||
| '04_Family_Health/Activities_AYOP_Camps', | ||
| '05_Tech_Infrastructure/NAS_Backups', | ||
| '05_Tech_Infrastructure/Tasker', | ||
| '05_Tech_Infrastructure/Hardware_Licenses', | ||
| '06_Work_Career/EY', | ||
| '06_Work_Career/Career_Interviews', | ||
| '06_Work_Career/Expenses_Admin', | ||
| '07_Community_NonProfit/HelpingOneGuy', | ||
| '07_Community_NonProfit/JeffCo_Bees' | ||
| ] | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * Unit tests for Gmail AI Classifier | ||
| */ | ||
|
|
||
| describe('Gmail AI Classifier Config', () => { | ||
| test('returns standard config schema', () => { | ||
| const config = { | ||
| geminiApiKey: 'TEST_KEY', | ||
| processedLabel: 'Processed', | ||
| autoFilterConfidenceThreshold: 0.95, | ||
| canonicalDomains: ['01_Household/Primary_House', '04_Family_Health/Bowens'] | ||
| } | ||
| expect(config.processedLabel).toBe('Processed') | ||
| expect(config.autoFilterConfidenceThreshold).toBe(0.95) | ||
| expect(config.canonicalDomains).toContain('04_Family_Health/Bowens') | ||
| }) | ||
| }) | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.