Judgement from Gmail
Say you’re writing an email, and want to make sure it’s not too spicy/unprofessional - in this world of LLMs you can get judgement whenever you want.
Google, for whatever reason, haven’t added proper Workspace Gemini support to Appscript so I’m using Cerebras for this example, since their inference speed is an order of magnitude faster than anything else and I don’t like spending time on emails
Steps to create
- Navigate to https://script.google.com/home and click
New project
- Change the project name to something meaningful like
IsThisEmailAGoodIdea?
- Paste the Code.gs into the code editor
- Click the settings cog on the left
- tick the “Show ‘appsscript.json’ manifest file in editor”
- in
Script properties
create a propertyCEREBRAS_API_KEY
with the value of your Cerebras API key
- Go back to the editor, and overwrite
appsscript.json
with the one below - Click “Save project to Drive”
- Click “Deploy” -> “Test Deployments” then “Application(s): Gmail” -> Install
The code
To be clear, not sponsored. Just wanted fast inference so I didn’t have to wait to test this out
This code was mostly generated by claude-sonnet-4-20250514
via Kagi Assistant
Yes the caching strategy is pretty terrible, but good enough for this experiment
Code.gs
/**
* Gmail Add-on for Professional Email Tone Checker
* Uses Cerebras Qwen2.5 model via OpenAI-compatible API
*/
function onGmailCompose(e) {
return createComposeActionCard();
}
function createComposeActionCard() {
// Check if we have cached results
const cachedResult = getCachedAnalysis();
if (cachedResult) {
return createResultCard(cachedResult, true); // true = minimized
}
const card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Professional Tone Checker')
.setImageUrl('https://www.gstatic.com/images/branding/product/1x/gmail_48dp.png'))
.addSection(CardService.newCardSection()
.addWidget(CardService.newButtonSet()
.addButton(CardService.newTextButton()
.setText('Check Professional Tone')
.setOnClickAction(CardService.newAction()
.setFunctionName('checkEmailTone')))))
.build();
return [card];
}
function checkEmailTone(e) {
try {
const emailContent = getEmailContent(e);
if (!emailContent.subject && !emailContent.body) {
return createErrorCard('Please add content to your email before checking tone.');
}
// Create content hash for caching
const contentHash = createContentHash(emailContent);
// Check cache first
let analysis = getCachedAnalysisByHash(contentHash);
if (!analysis) {
// Analyze with Cerebras if not cached
analysis = analyzeWithCerebras(emailContent);
// Cache the result
cacheAnalysis(analysis, contentHash);
}
return createResultCard(analysis, false); // false = expanded
} catch (error) {
console.error('Error checking email tone:', error);
return createErrorCard('Error analyzing email. Please try again.');
}
}
function toggleResultView(e) {
const isMinimized = e.parameters?.minimized === 'true';
const cachedResult = getCachedAnalysis();
if (cachedResult) {
return createResultCard(cachedResult, !isMinimized);
}
return createComposeActionCard();
}
function clearCache(e) {
clearUserCache();
return createComposeActionCard();
}
function getEmailContent(e) {
const subject = e.formInput?.subject || e.gmail?.subject || '';
const body = e.formInput?.body || e.gmail?.body || '';
return {
subject: subject,
body: body
};
}
function createContentHash(emailContent) {
const content = `${emailContent.subject}|${emailContent.body}`;
return Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, content)
.map(byte => (byte + 256) % 256)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
function cacheAnalysis(analysis, contentHash) {
const cache = Utilities.getUuid(); // Generate unique cache key per user session
const cacheData = {
analysis: analysis,
contentHash: contentHash,
timestamp: new Date().getTime()
};
try {
CacheService.getUserCache().put('email_analysis', JSON.stringify(cacheData), 3600); // 1 hour
CacheService.getUserCache().put('current_hash', contentHash, 3600);
} catch (error) {
console.error('Cache error:', error);
}
}
function getCachedAnalysis() {
try {
const cached = CacheService.getUserCache().get('email_analysis');
if (cached) {
const cacheData = JSON.parse(cached);
// Check if cache is less than 1 hour old
if (new Date().getTime() - cacheData.timestamp < 3600000) {
return cacheData.analysis;
}
}
} catch (error) {
console.error('Cache retrieval error:', error);
}
return null;
}
function getCachedAnalysisByHash(contentHash) {
try {
const cached = CacheService.getUserCache().get('email_analysis');
const currentHash = CacheService.getUserCache().get('current_hash');
if (cached && currentHash === contentHash) {
const cacheData = JSON.parse(cached);
return cacheData.analysis;
}
} catch (error) {
console.error('Cache hash retrieval error:', error);
}
return null;
}
function clearUserCache() {
try {
CacheService.getUserCache().removeAll(['email_analysis', 'current_hash']);
} catch (error) {
console.error('Cache clear error:', error);
}
}
function analyzeWithCerebras(emailContent) {
const prompt = `Analyze this email for professional tone and business appropriateness:
Subject: ${emailContent.subject}
Body: ${emailContent.body}
Provide analysis in this exact JSON format:
{
"score": [1-10 number],
"tone": "[brief tone description]",
"issues": ["issue1", "issue2"],
"suggestions": ["suggestion1", "suggestion2"],
"positives": ["positive1", "positive2"]
}
Focus on:
- Professional language usage
- Appropriate formality level
- Clear communication
- Respectful tone
- Business etiquette`;
try {
const response = callCerebrasAPI(prompt);
return parseCerebrasResponse(response);
} catch (error) {
console.error('Cerebras API error:', error);
throw new Error('Failed to analyze email with AI');
}
}
function callCerebrasAPI(prompt) {
const apiKey = PropertiesService.getScriptProperties().getProperty('CEREBRAS_API_KEY');
if (!apiKey) {
throw new Error('Cerebras API key not configured');
}
const url = 'https://api.cerebras.ai/v1/chat/completions';
const payload = {
model: 'qwen-3-32b',
messages: [
{
role: 'system',
content: 'You are a professional communication expert. Analyze emails for business appropriateness and provide constructive feedback in JSON format only.'
},
{
role: 'user',
content: prompt
}
],
max_tokens: 1000,
temperature: 0.7,
top_p: 0.95
};
const options = {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() !== 200) {
const errorText = response.getContentText();
console.error('API Error Response:', errorText);
throw new Error(`API request failed: ${response.getResponseCode()}`);
}
return JSON.parse(response.getContentText());
}
function parseCerebrasResponse(response) {
try {
const content = response.choices[0].message.content.trim();
let jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return parseTextResponse(content);
} catch (error) {
console.error('Parse error:', error);
throw new Error('Failed to parse AI response');
}
}
function parseTextResponse(text) {
const result = {
score: 7,
tone: 'Professional',
issues: [],
suggestions: [],
positives: []
};
const scoreMatch = text.match(/score[:\s]*(\d+)/i);
if (scoreMatch) {
result.score = Math.min(10, Math.max(1, parseInt(scoreMatch[1])));
}
const toneMatch = text.match(/tone[:\s]*["\']?([^"\'\n]+)["\']?/i);
if (toneMatch) {
result.tone = toneMatch[1].trim();
}
const issuesSection = text.match(/issues?[:\s]*\[(.*?)\]/is);
if (issuesSection) {
result.issues = issuesSection[1].split(',').map(s => s.replace(/["\'\[\]]/g, '').trim()).filter(s => s);
}
const suggestionsSection = text.match(/suggestions?[:\s]*\[(.*?)\]/is);
if (suggestionsSection) {
result.suggestions = suggestionsSection[1].split(',').map(s => s.replace(/["\'\[\]]/g, '').trim()).filter(s => s);
}
const positivesSection = text.match(/positives?[:\s]*\[(.*?)\]/is);
if (positivesSection) {
result.positives = positivesSection[1].split(',').map(s => s.replace(/["\'\[\]]/g, '').trim()).filter(s => s);
}
return result;
}
function createResultCard(analysis, isMinimized = false) {
const card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Professional Tone Analysis'));
// Always show score and basic info
const mainSection = CardService.newCardSection()
.addWidget(CardService.newKeyValue()
.setTopLabel('Professional Score')
.setContent(`${analysis.score}/10`)
.setIcon(CardService.Icon.STAR))
.addWidget(CardService.newKeyValue()
.setTopLabel('Tone Assessment')
.setContent(analysis.tone || 'Professional')
.setIcon(CardService.Icon.DESCRIPTION));
card.addSection(mainSection);
if (!isMinimized) {
// Add detailed sections when expanded
if (analysis.issues && analysis.issues.length > 0) {
const issuesSection = CardService.newCardSection()
.setHeader('⚠️ Areas for Improvement');
analysis.issues.slice(0, 3).forEach(issue => {
if (issue && issue.trim()) {
issuesSection.addWidget(CardService.newTextParagraph()
.setText(`• ${issue.trim()}`));
}
});
card.addSection(issuesSection);
}
if (analysis.suggestions && analysis.suggestions.length > 0) {
const suggestionsSection = CardService.newCardSection()
.setHeader('💡 Suggestions');
analysis.suggestions.slice(0, 3).forEach(suggestion => {
if (suggestion && suggestion.trim()) {
suggestionsSection.addWidget(CardService.newTextParagraph()
.setText(`• ${suggestion.trim()}`));
}
});
card.addSection(suggestionsSection);
}
if (analysis.positives && analysis.positives.length > 0) {
const positivesSection = CardService.newCardSection()
.setHeader('✅ Positive Aspects');
analysis.positives.slice(0, 3).forEach(positive => {
if (positive && positive.trim()) {
positivesSection.addWidget(CardService.newTextParagraph()
.setText(`• ${positive.trim()}`));
}
});
card.addSection(positivesSection);
}
}
// Control buttons
const buttonSection = CardService.newCardSection();
const buttonSet = CardService.newButtonSet();
if (isMinimized) {
buttonSet.addButton(CardService.newTextButton()
.setText('Show Details')
.setOnClickAction(CardService.newAction()
.setFunctionName('toggleResultView')
.setParameters({minimized: 'true'})));
} else {
buttonSet.addButton(CardService.newTextButton()
.setText('Minimize')
.setOnClickAction(CardService.newAction()
.setFunctionName('toggleResultView')
.setParameters({minimized: 'false'})));
}
buttonSet.addButton(CardService.newTextButton()
.setText('Analyze Again')
.setOnClickAction(CardService.newAction()
.setFunctionName('checkEmailTone')))
.addButton(CardService.newTextButton()
.setText('Clear Cache')
.setOnClickAction(CardService.newAction()
.setFunctionName('clearCache')));
buttonSection.addWidget(buttonSet);
card.addSection(buttonSection);
return card.build();
}
function createErrorCard(message) {
return CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Error'))
.addSection(CardService.newCardSection()
.addWidget(CardService.newTextParagraph()
.setText(message))
.addWidget(CardService.newButtonSet()
.addButton(CardService.newTextButton()
.setText('Try Again')
.setOnClickAction(CardService.newAction()
.setFunctionName('checkEmailTone')))))
.build();
}
function testCerebrasAPI() {
const testEmail = {
subject: 'Test Email',
body: 'Hey there! Just wanted to check if we can meet tomorrow. Let me know!'
};
try {
const result = analyzeWithCerebras(testEmail);
console.log('Test result:', result);
return result;
} catch (error) {
console.error('Test failed:', error);
return error.toString();
}
}
appscript.json
{
"timeZone": "Etc/UTC",
"dependencies": {
"enabledAdvancedServices": []
},
"exceptionLogging": "NONE",
"runtimeVersion": "V8",
"gmail": {
"name": "Professional Tone Checker",
"logoUrl": "https://www.gstatic.com/images/branding/product/1x/gmail_48dp.png",
"contextualTriggers": [
{
"unconditional": {},
"onTriggerFunction": "onGmailCompose"
}
],
"composeTrigger": {
"selectActions": [
{
"text": "Check Professional Tone",
"runFunction": "checkEmailTone"
}
],
"draftAccess": "METADATA"
}
},
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/gmail.addons.current.message.metadata"
]
}
How to use
- Open a drafted email
- click the vertical … more options button
- Select “Check Professional Tone”
- First time, you’ll be prompted to provide oauth permissions to the appscript script.
- A card will show up, with guidance about your email
Next steps for users
Tune what exactly you want from the guidance by tweaking the prompt and display details in Code.gs