Skip to content

Commit dc5cbe4

Browse files
committed
Initial import of devdocs link preview from COL300/Next24
1 parent 9e67983 commit dc5cbe4

File tree

6 files changed

+416
-0
lines changed

6 files changed

+416
-0
lines changed

ai/devdocs-link-preview/Cards.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* Creates the Card to display documentation summary to user.
19+
*
20+
* @param {string} pageTitle Title of the page/card section.
21+
* @param {string} summary Page summary to display.
22+
* @return {!Card}
23+
*/
24+
function buildCard(pageTitle, summary, showRating = true) {
25+
26+
let cardHeader = CardService.newCardHeader()
27+
.setTitle('About this page');
28+
29+
let summarySection = CardService.newCardSection()
30+
.addWidget(CardService.newTextParagraph().setText(summary));
31+
32+
let feedbackSection = CardService.newCardSection()
33+
.setHeader('Rate this summary');
34+
35+
if (showRating) {
36+
let thumbsUpAction = CardService.newAction()
37+
.setFunctionName('onRatingClicked')
38+
.setParameters({
39+
'key': 'upVotes',
40+
'title': pageTitle,
41+
'pageSummary': summary
42+
});
43+
44+
let thumbsDownAction = CardService.newAction()
45+
.setFunctionName('onRatingClicked')
46+
.setParameters({
47+
'key': 'downVotes',
48+
'title': pageTitle,
49+
'pageSummary': summary
50+
});
51+
52+
let thumbsUpButton = CardService.newImageButton()
53+
.setIconUrl(
54+
'https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png'
55+
)
56+
.setAltText('Looks good')
57+
.setOnClickAction(thumbsUpAction);
58+
59+
let thumbsDownButton = CardService.newImageButton()
60+
.setIconUrl(
61+
'https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png'
62+
)
63+
.setAltText('Not great')
64+
.setOnClickAction(thumbsDownAction);
65+
66+
let ratingButtons = CardService.newButtonSet()
67+
.addButton(thumbsUpButton)
68+
.addButton(thumbsDownButton);
69+
feedbackSection.addWidget(ratingButtons)
70+
} else {
71+
feedbackSection.addWidget(CardService.newTextParagraph().setText("Thank you for your feedback."))
72+
}
73+
74+
75+
let card = CardService.newCardBuilder()
76+
.setHeader(cardHeader)
77+
.addSection(summarySection)
78+
.addSection(feedbackSection)
79+
.build();
80+
return card;
81+
}
82+
83+
/**
84+
* Creates a Card to let user know an error has occurred.
85+
*
86+
* @return {!Card}
87+
*/
88+
function buildErrorCard() {
89+
let cardHeader = CardService.newCardHeader()
90+
.setTitle('Uh oh! Something went wrong.')
91+
92+
let errorMessage = CardService.newTextParagraph()
93+
.setText(
94+
'It looks like Gemini got stage fright.');
95+
96+
let tryAgainButton = CardService.newTextButton()
97+
.setText('Try again')
98+
.setTextButtonStyle(CardService.TextButtonStyle.TEXT)
99+
.setOnClickAction( CardService.newAction()
100+
.setFunctionName('onLinkPreview'));
101+
102+
let buttonList = CardService.newButtonSet()
103+
.addButton(tryAgainButton);
104+
105+
let mainSection = CardService.newCardSection()
106+
.addWidget(errorMessage)
107+
.addWidget(buttonList);
108+
109+
let errorCard = CardService.newCardBuilder()
110+
.setHeader(cardHeader)
111+
.addSection(mainSection)
112+
.build();
113+
114+
return errorCard;
115+
}

ai/devdocs-link-preview/Helpers.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* Wraper around script properties to allow for a default value if unset.
19+
*/
20+
function scriptPropertyWithDefault(key, defaultValue = undefined) {
21+
const scriptProperties = PropertiesService.getScriptProperties();
22+
const value = scriptProperties.getProperty(key);
23+
if (value) {
24+
return value;
25+
}
26+
return defaultValue;
27+
}

ai/devdocs-link-preview/Main.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* Creates a link preview card for Google developer documentation links.
19+
*
20+
* @param {!Object} event
21+
* @return {!Card}
22+
*/
23+
function onLinkPreview(event) {
24+
const hostApp = event.hostApp;
25+
if (!event[hostApp].matchedUrl.url) {
26+
return;
27+
}
28+
const url = event[hostApp].matchedUrl.url;
29+
try {
30+
const info = getPageSummary(url);
31+
const card = buildCard(info.title, info.summary);
32+
const linkPreview = CardService.newLinkPreview()
33+
.setPreviewCard(card)
34+
.setTitle(info.title)
35+
.setLinkPreviewTitle(info.title);
36+
return linkPreview;
37+
} catch (error) {
38+
// Log the error
39+
console.error("Error occurred:", error);
40+
const errorCard = buildErrorCard();
41+
return CardService.newActionResponseBuilder()
42+
.setNavigation(CardService.newNavigation().updateCard(errorCard))
43+
.build();
44+
}
45+
}
46+
47+
/**
48+
* Action handler for a good rating .
49+
*
50+
* @param {!Object} e The event passed from click action.
51+
* @return {!Card}
52+
*/
53+
function onRatingClicked(e) {
54+
let key = e.parameters.key;
55+
let title = e.parameters.title;
56+
let pageSummary = e.parameters.pageSummary;
57+
58+
const properties = PropertiesService.getScriptProperties();
59+
let rating = Number(properties.getProperty(key) ?? 0);
60+
properties.setProperty(key, ++rating);
61+
62+
let card = buildCard(title, pageSummary, false);
63+
let linkPreview = CardService.newLinkPreview()
64+
.setPreviewCard(card)
65+
.setTitle(title)
66+
.setLinkPreviewTitle(title);
67+
68+
return linkPreview;
69+
}

ai/devdocs-link-preview/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Google Workspace Add-on - Developer Docs Link previews
2+
3+
4+
## Project Description
5+
6+
A Google Workspace Add-on that creates custom link previews for pages on the Google developer documentation site. The link preview uses AI to generate page summaries.
7+
8+
## Prerequisites
9+
10+
* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled
11+
12+
## Set up your environment
13+
14+
1. Create a Cloud Project
15+
1. Enable the Vertex AI API
16+
1. Create a Service Account and grant the role `Vertex AI User`
17+
1. Create a private key with type JSON. This will download the JSON file for use in the next section.
18+
1. Open a stand alone Apps Script Project
19+
1. From Project Settings, change project to GCP project number of Cloud Project from step 1
20+
1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value.
21+
1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.
22+
1. Add the project code to Apps Script
23+

ai/devdocs-link-preview/Vertex.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
const VERTEX_AI_LOCATION = scriptPropertyWithDefault('project_location', 'us-central1');
18+
const MODEL_ID = scriptPropertyWithDefault('model_id', 'gemini-1.5-flash-preview-0514');
19+
const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault('service_account_key');
20+
21+
/**
22+
* Invokes Gemini to extrac the title and summary of a given URL. Responses may be cached.
23+
*/
24+
function getPageSummary(targetUrl) {
25+
let cachedResponse = CacheService.getScriptCache().get(targetUrl);
26+
if (cachedResponse) {
27+
return JSON.parse(cachedResponse);
28+
}
29+
30+
const request = {
31+
contents: [
32+
{
33+
role: "user",
34+
parts: [
35+
{
36+
text: targetUrl
37+
}
38+
]
39+
}
40+
],
41+
systemInstruction: {
42+
parts: [
43+
{
44+
text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as "title" and the summary as "summary"`
45+
}
46+
]
47+
},
48+
generationConfig: {
49+
temperature: .2,
50+
candidateCount: 1,
51+
maxOutputTokens: 2048
52+
}
53+
}
54+
55+
const credentials = credentialsForVertexAI();
56+
57+
const fetchOptions = {
58+
method: 'POST',
59+
headers: {
60+
'Authorization': `Bearer ${credentials.accessToken}`
61+
},
62+
contentType: 'application/json',
63+
muteHttpExceptions: true,
64+
payload: JSON.stringify(request)
65+
}
66+
67+
const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` +
68+
`/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`
69+
const response = UrlFetchApp.fetch(url, fetchOptions);
70+
71+
const responseText = response.getContentText();
72+
console.log(responseText);
73+
if (response.getResponseCode() >= 400) {
74+
console.log(responseText);
75+
throw new Error("Unable to generate preview,");
76+
}
77+
const parsedResponse = JSON.parse(responseText);
78+
let modelResponse = parsedResponse.candidates[0].content.parts[0].text;
79+
const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\s\S]*)(?=^`{3}$)/gm);
80+
if (!jsonMatch) {
81+
throw new Error("Unable to generate preview,");
82+
}
83+
CacheService.getScriptCache().put(targetUrl, jsonMatch);
84+
return JSON.parse(jsonMatch[0]);
85+
}
86+
87+
88+
89+
/**
90+
* Gets credentials required to call Vertex API using a Service Account.
91+
* Requires use of Service Account Key stored with project
92+
*
93+
* @return {!Object} Containing the Cloud Project Id and the access token.
94+
*/
95+
function credentialsForVertexAI() {
96+
const credentials = SERVICE_ACCOUNT_KEY;
97+
if (!credentials) {
98+
throw new Error("service_account_key script property must be set.");
99+
}
100+
101+
const parsedCredentials = JSON.parse(credentials);
102+
const service = OAuth2.createService("Vertex")
103+
.setTokenUrl('https://oauth2.googleapis.com/token')
104+
.setPrivateKey(parsedCredentials['private_key'])
105+
.setIssuer(parsedCredentials['client_email'])
106+
.setPropertyStore(PropertiesService.getScriptProperties())
107+
.setScope("https://www.googleapis.com/auth/cloud-platform");
108+
return {
109+
projectId: parsedCredentials['project_id'],
110+
accessToken: service.getAccessToken(),
111+
}
112+
}

0 commit comments

Comments
 (0)