├── .github
├── CODEOWNERS
├── linters
│ ├── .htmlhintrc
│ ├── .yaml-lint.yml
│ └── sun_checks.xml
├── sync-repo-settings.yaml
└── workflows
│ ├── automation.yml
│ ├── lint.yml
│ └── test.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── apps-script
├── BQMLForecasting
│ ├── README.md
│ ├── appsscript.json
│ ├── code.gs
│ └── images
│ │ ├── appsscript.png
│ │ ├── forecast.png
│ │ └── train.png
├── README.md
├── automl
│ └── naturallanguage.js
├── documentai
│ └── documentai.js
├── nl_api_entitysentiment.js
└── vision_api.gs
├── java
└── Docs2Speech
│ ├── README.md
│ ├── build.gradle
│ ├── gradlew.bat
│ ├── settings.gradle
│ └── src
│ └── main
│ └── java
│ ├── DocsToSpeech.java
│ └── SpeechClient.java
└── node
└── SmartAccountsBot
├── README.md
├── dialogflowFirebaseFulfilment
├── cardBuilder.js
├── index.js
└── package.json
└── sample.png
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
16 |
17 | .github/ @googleworkspace/workspace-devrel-dpe
18 |
--------------------------------------------------------------------------------
/.github/linters/.htmlhintrc:
--------------------------------------------------------------------------------
1 | {
2 | "tagname-lowercase": true,
3 | "attr-lowercase": true,
4 | "attr-value-double-quotes": true,
5 | "attr-value-not-empty": false,
6 | "attr-no-duplication": true,
7 | "doctype-first": false,
8 | "tag-pair": true,
9 | "tag-self-close": false,
10 | "spec-char-escape": false,
11 | "id-unique": true,
12 | "src-not-empty": true,
13 | "title-require": false,
14 | "alt-require": true,
15 | "doctype-html5": true,
16 | "id-class-value": false,
17 | "style-disabled": false,
18 | "inline-style-disabled": false,
19 | "inline-script-disabled": false,
20 | "space-tab-mixed-disabled": "space",
21 | "id-class-ad-disabled": false,
22 | "href-abs-or-rel": false,
23 | "attr-unsafe-chars": true,
24 | "head-script-disabled": false
25 | }
26 |
--------------------------------------------------------------------------------
/.github/linters/.yaml-lint.yml:
--------------------------------------------------------------------------------
1 | ---
2 | ###########################################
3 | # These are the rules used for #
4 | # linting all the yaml files in the stack #
5 | # NOTE: #
6 | # You can disable line with: #
7 | # # yamllint disable-line #
8 | ###########################################
9 | rules:
10 | braces:
11 | level: warning
12 | min-spaces-inside: 0
13 | max-spaces-inside: 0
14 | min-spaces-inside-empty: 1
15 | max-spaces-inside-empty: 5
16 | brackets:
17 | level: warning
18 | min-spaces-inside: 0
19 | max-spaces-inside: 0
20 | min-spaces-inside-empty: 1
21 | max-spaces-inside-empty: 5
22 | colons:
23 | level: warning
24 | max-spaces-before: 0
25 | max-spaces-after: 1
26 | commas:
27 | level: warning
28 | max-spaces-before: 0
29 | min-spaces-after: 1
30 | max-spaces-after: 1
31 | comments: disable
32 | comments-indentation: disable
33 | document-end: disable
34 | document-start:
35 | level: warning
36 | present: true
37 | empty-lines:
38 | level: warning
39 | max: 2
40 | max-start: 0
41 | max-end: 0
42 | hyphens:
43 | level: warning
44 | max-spaces-after: 1
45 | indentation:
46 | level: warning
47 | spaces: consistent
48 | indent-sequences: true
49 | check-multi-line-strings: false
50 | key-duplicates: enable
51 | line-length:
52 | level: warning
53 | max: 120
54 | allow-non-breakable-words: true
55 | allow-non-breakable-inline-mappings: true
56 | new-line-at-end-of-file: disable
57 | new-lines:
58 | type: unix
59 | trailing-spaces: disable
--------------------------------------------------------------------------------
/.github/linters/sun_checks.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
65 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
82 |
83 |
84 |
86 |
87 |
88 |
94 |
95 |
96 |
97 |
100 |
101 |
102 |
103 |
104 |
108 |
109 |
110 |
111 |
112 |
114 |
115 |
116 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
135 |
137 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
185 |
186 |
187 |
189 |
191 |
192 |
193 |
194 |
196 |
197 |
198 |
199 |
201 |
202 |
203 |
204 |
206 |
207 |
208 |
209 |
211 |
212 |
213 |
214 |
216 |
217 |
218 |
219 |
221 |
222 |
223 |
224 |
226 |
227 |
228 |
229 |
231 |
232 |
233 |
234 |
236 |
237 |
238 |
239 |
241 |
242 |
243 |
244 |
246 |
247 |
248 |
249 |
251 |
253 |
255 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
287 |
288 |
289 |
292 |
293 |
294 |
295 |
301 |
302 |
303 |
304 |
308 |
309 |
310 |
311 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
326 |
327 |
328 |
329 |
330 |
331 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
347 |
348 |
349 |
350 |
353 |
354 |
355 |
356 |
357 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
371 |
372 |
373 |
374 |
--------------------------------------------------------------------------------
/.github/sync-repo-settings.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # .github/sync-repo-settings.yaml
16 | # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings for app options.
17 | rebaseMergeAllowed: true
18 | squashMergeAllowed: true
19 | mergeCommitAllowed: false
20 | deleteBranchOnMerge: true
21 | branchProtectionRules:
22 | - pattern: main
23 | isAdminEnforced: false
24 | requiresStrictStatusChecks: false
25 | requiredStatusCheckContexts:
26 | # .github/workflows/test.yml with a job called "test"
27 | - "test"
28 | # .github/workflows/lint.yml with a job called "lint"
29 | - "lint"
30 | # Google bots below
31 | - "cla/google"
32 | - "snippet-bot check"
33 | - "header-check"
34 | - "conventionalcommits.org"
35 | requiredApprovingReviewCount: 1
36 | requiresCodeOwnerReviews: true
37 | permissionRules:
38 | - team: workspace-devrel-dpe
39 | permission: admin
40 | - team: workspace-devrel
41 | permission: push
42 |
--------------------------------------------------------------------------------
/.github/workflows/automation.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | ---
15 | name: Automation
16 | on: [ push, pull_request, workflow_dispatch ]
17 | jobs:
18 | dependabot:
19 | runs-on: ubuntu-latest
20 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }}
21 | env:
22 | PR_URL: ${{github.event.pull_request.html_url}}
23 | GITHUB_TOKEN: ${{secrets.GOOGLEWORKSPACE_BOT_TOKEN}}
24 | steps:
25 | - name: approve
26 | run: gh pr review --approve "$PR_URL"
27 | - name: merge
28 | run: gh pr merge --auto --squash --delete-branch "$PR_URL"
29 | default-branch-migration:
30 | # this job helps with migrating the default branch to main
31 | # it pushes main to master if master exists and main is the default branch
32 | # it pushes master to main if master is the default branch
33 | runs-on: ubuntu-latest
34 | if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}
35 | steps:
36 | - uses: actions/checkout@v2
37 | with:
38 | fetch-depth: 0
39 | # required otherwise GitHub blocks infinite loops in pushes originating in an action
40 | token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
41 | - name: Set env
42 | run: |
43 | # set DEFAULT BRANCH
44 | echo "DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" >> "$GITHUB_ENV";
45 |
46 | # set HAS_MASTER_BRANCH
47 | if [ -n "$(git ls-remote --heads origin master)" ]; then
48 | echo "HAS_MASTER_BRANCH=true" >> "$GITHUB_ENV"
49 | else
50 | echo "HAS_MASTER_BRANCH=false" >> "$GITHUB_ENV"
51 | fi
52 | env:
53 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | - name: configure git
55 | run: |
56 | git config --global user.name 'googleworkspace-bot'
57 | git config --global user.email 'googleworkspace-bot@google.com'
58 | - if: ${{ env.DEFAULT_BRANCH == 'main' && env.HAS_MASTER_BRANCH == 'true' }}
59 | name: Update master branch from main
60 | run: |
61 | git checkout -B master
62 | git reset --hard origin/main
63 | git push origin master
64 | - if: ${{ env.DEFAULT_BRANCH == 'master'}}
65 | name: Update main branch from master
66 | run: |
67 | git checkout -B main
68 | git reset --hard origin/master
69 | git push origin main
70 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Lint
16 | on: [push, pull_request]
17 | jobs:
18 | lint:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v2
22 | - run: |
23 | echo "No lint checks";
24 | exit 1;
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Test
16 | on: [push, pull_request]
17 | jobs:
18 | test:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v2
22 | - run: |
23 | echo "No tests";
24 | exit 1;
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .DS_Store
3 | **/env/
4 | **/help/
5 | **/lib/
6 | **/target/
7 | **/deactivate
8 | **/client_secret.json
9 | service-account.json
10 | **/.idea/
11 | **/*.iml
12 |
13 | # Logs
14 | logs
15 | *.log
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 | lerna-debug.log*
20 |
21 | # Diagnostic reports (https://nodejs.org/api/report.html)
22 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
23 |
24 | # Runtime data
25 | pids
26 | *.pid
27 | *.seed
28 | *.pid.lock
29 |
30 | # Directory for instrumented libs generated by jscoverage/JSCover
31 | lib-cov
32 |
33 | # Coverage directory used by tools like istanbul
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 | .nyc_output
39 |
40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
41 | .grunt
42 |
43 | # Bower dependency directory (https://bower.io/)
44 | bower_components
45 |
46 | # node-waf configuration
47 | .lock-wscript
48 |
49 | # Compiled binary addons (https://nodejs.org/api/addons.html)
50 | build/Release
51 |
52 | # Dependency directories
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 | web_modules/
58 |
59 | # TypeScript cache
60 | *.tsbuildinfo
61 |
62 | # Optional npm cache directory
63 | .npm
64 |
65 | # Optional eslint cache
66 | .eslintcache
67 |
68 | # Microbundle cache
69 | .rpt2_cache/
70 | .rts2_cache_cjs/
71 | .rts2_cache_es/
72 | .rts2_cache_umd/
73 |
74 | # Optional REPL history
75 | .node_repl_history
76 |
77 | # Output of 'npm pack'
78 | *.tgz
79 |
80 | # Yarn Integrity file
81 | .yarn-integrity
82 |
83 | # dotenv environment variables file
84 | .env
85 | .env.test
86 |
87 | # parcel-bundler cache (https://parceljs.org/)
88 | .cache
89 | .parcel-cache
90 |
91 | # Next.js build output
92 | .next
93 | out
94 |
95 | # Nuxt.js build / generate output
96 | .nuxt
97 | dist
98 |
99 | # Gatsby files
100 | .cache/
101 | # Comment in the public line in if your project uses Gatsby and not Next.js
102 | # https://nextjs.org/blog/next-9-1#public-directory-support
103 | # public
104 |
105 | # vuepress build output
106 | .vuepress/dist
107 |
108 | # Serverless directories
109 | .serverless/
110 |
111 | # FuseBox cache
112 | .fusebox/
113 |
114 | # DynamoDB Local files
115 | .dynamodb/
116 |
117 | # TernJS port file
118 | .tern-port
119 |
120 | # Stores VSCode versions used for testing VSCode extensions
121 | .vscode-test
122 |
123 | # yarn v2
124 | .yarn/cache
125 | .yarn/unplugged
126 | .yarn/build-state.yml
127 | .yarn/install-state.gz
128 | .pnp.*
129 |
130 | # Compiled class file
131 | *.class
132 |
133 | # Log file
134 | *.log
135 |
136 | # BlueJ files
137 | *.ctxt
138 |
139 | # Mobile Tools for Java (J2ME)
140 | .mtj.tmp/
141 |
142 | # Package Files #
143 | *.jar
144 | *.war
145 | *.nar
146 | *.ear
147 | *.zip
148 | *.tar.gz
149 | *.rar
150 |
151 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
152 | hs_err_pid*
153 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google Workspace ML Integration Samples
2 |
3 | Welcome to the Google Workspace ML Integrations samples repository. Here you
4 | will find a collection of code samples and utilities that integrate
5 | both Google Workspace APIs and Google Cloud AI/ML technologies.
6 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Report a security issue
2 |
3 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use
4 | [https://g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here on
5 | GitHub (including using GitHub Security Advisory). The Google Security Team will
6 | respond within 5 working days of your report on [https://g.co/vulnz](https://g.co/vulnz).
7 |
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/README.md:
--------------------------------------------------------------------------------
1 | # BQML Forecasting with Sheets
2 |
3 | This code sample shows how to use time-series forecasting models in [BigQuery Machine Learning](https://cloud.google.com/bigquery-ml/docs/introduction?utm_campaign=CDR_kwe_aiml_time-series-forecasting_011521&utm_source=external&utm_medium=web) directly from Sheets.
4 |
5 | ## Usage
6 |
7 | 1. Select data you'd like to use to train your model, and choose **BQML > Train** from the menu.
8 |
9 | Your data must be formatted with 2 columns, the first containing the date and/or time, and the second with the numeric value to forecast.
10 |
11 | 
12 |
13 | 2. When the training is finished, select the number of time steps you'd like to forecast, and choose **BQML > Forecast** from the menu. The script will populate the range. If no range is selected, it will prompt you for the number of time steps.
14 |
15 | 
16 |
17 | ## Getting started
18 |
19 | 1. Load an existing Google Sheet, or add data to a new Google Sheet (as easy as typing [sheet.new](sheet.new) in your browser).
20 | 2. From your spreadsheet, choose **Extensions > Apps Script**.
21 | 3. From the **Project Settings** in the left panel, check the box to "Show 'appsscript.json' manifest file in editor."
22 | 4. Paste in the contents of `code.gs` from this repo into the Apps Script project.
23 | 5. Paste in the contents of `appsscript.json` from this repo into the Apps Script project.
24 |
25 | 
26 |
27 | 6. Save both files, and close the Apps Script window.
28 | 7. The first time you train a model, it will prompt you for your GCP project ID. It will use a default BigQuery dataset ID of `sheets_forecast` that you can optionally override in the **BQML > Configure** menu.
29 |
30 | ## How it works
31 |
32 | ### Training
33 |
34 | First, the script will extract rows from the selected range and insert them into a temporary table in [BigQuery](https://cloud.google.com/bigquery?utm_campaign=CDR_kwe_aiml_time-series-forecasting_011521&utm_source=external&utm_medium=web).
35 |
36 | Then, the script runs a BQML query to create an ARIMA [time series model](https://cloud.google.com/bigquery-ml/docs/reference/standard-sql/bigqueryml-syntax-create-time-series?utm_campaign=CDR_kwe_aiml_time-series-forecasting_011521&utm_source=external&utm_medium=web) that looks something like this:
37 |
38 | ```sql
39 | CREATE OR REPLACE MODEL
40 | `sheets_forecast.sheets_forecast_model` OPTIONS( MODEL_TYPE='ARIMA',
41 | TIME_SERIES_TIMESTAMP_COL='datetime',
42 | TIME_SERIES_DATA_COL='data') AS
43 | SELECT
44 | *
45 | FROM
46 | `sheets_forecast.sheets_forecast_training_data`
47 | ```
48 |
49 | The query can be further customized with other options, such as including holidays into the model.
50 |
51 | ### Forecasting
52 |
53 | To make a forecast, the script sets the `horizon` parameter in the BQML forecast query to the number of rows selected in the sheet. Two fields are extracted from the forecast: the datetime formatted as a string with the date, time, and time zone; and the forecasted value for that datetime. The query looks like this (assuming 3 rows are selected):
54 |
55 | ```sql
56 | SELECT
57 | FORMAT_TIMESTAMP("%FT%T%Ez", forecast_timestamp),
58 | forecast_value
59 | FROM
60 | ML.FORECAST(MODEL `sheets_forecast.sheets_forecast_model`,
61 | STRUCT(3 AS horizon))
62 | ```
63 |
64 | The selected range is then updated with the values returned from the query.
65 |
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/appsscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "UTC",
3 | "dependencies": {
4 | "enabledAdvancedServices": [
5 | {
6 | "userSymbol": "BigQuery",
7 | "version": "v2",
8 | "serviceId": "bigquery"
9 | },
10 | {
11 | "userSymbol": "Sheets",
12 | "version": "v4",
13 | "serviceId": "sheets"
14 | }
15 | ]
16 | },
17 | "exceptionLogging": "STACKDRIVER",
18 | "runtimeVersion": "V8"
19 | }
20 |
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/code.gs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 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 DATASET_PROPERTY = 'datasetId';
18 | const PROJECT_PROPERTY = 'projectId';
19 | const INTEGER_PROPERTY = 'integer';
20 |
21 | const DATETIME_COLUMN = 'datetime';
22 | const DATA_COLUMN = 'data';
23 |
24 | const DATASET_NAME = 'sheets_forecast';
25 | const TABLE_NAME = 'sheets_forecast_training_data';
26 | const MODEL_NAME = 'sheets_forecast_model';
27 |
28 | // =============================================================================
29 | // UI
30 | // =============================================================================
31 |
32 | /**
33 | * Create menu items linked to functions
34 | */
35 | function onOpen() {
36 | SpreadsheetApp.getUi()
37 | .createMenu('BQML')
38 | .addItem('Train', 'train')
39 | .addItem('Forecast', 'forecast')
40 | .addSeparator()
41 | .addSubMenu(SpreadsheetApp.getUi().createMenu('Configure')
42 | .addItem('Project', 'configureProject')
43 | .addItem('Dataset', 'configureDataset'))
44 | .addToUi();
45 | }
46 |
47 | // =============================================================================
48 | // Configuration
49 | // =============================================================================
50 |
51 | /**
52 | * Get configuration information, and prompt user if not defined
53 | */
54 | function getConfiguration() {
55 | properties = PropertiesService.getUserProperties();
56 | let projectId = properties.getProperty(PROJECT_PROPERTY);
57 | if (projectId == null) {
58 | projectId = configureProject();
59 | }
60 | let datasetId = properties.getProperty(DATASET_PROPERTY);
61 | if (datasetId == null) {
62 | datasetId = DATASET_NAME; // Use default if the user hasn't specified one
63 | }
64 | return [projectId, datasetId];
65 | }
66 |
67 | /**
68 | * Prompt user for the GCP project details
69 | */
70 | function configureProject() {
71 | const response = SpreadsheetApp.getUi().prompt('GCP Project ID:');
72 | if (response.getSelectedButton() == SpreadsheetApp.getUi().Button.OK) {
73 | const project = response.getResponseText();
74 | Logger.log('Project configured: ' + response.getResponseText());
75 | PropertiesService.getUserProperties().
76 | setProperty(PROJECT_PROPERTY, project);
77 | return project;
78 | }
79 |
80 | Logger.log('Project was not configured.');
81 | return null;
82 | }
83 |
84 | /**
85 | * Prompt user for the BigQuery dataset details
86 | */
87 | function configureDataset() {
88 | const response = SpreadsheetApp.getUi().prompt('BigQuery dataset name:');
89 | if (response.getSelectedButton() == SpreadsheetApp.getUi().Button.OK) {
90 | const dataset = response.getResponseText();
91 | PropertiesService.getUserProperties().
92 | setProperty(DATASET_PROPERTY, dataset);
93 | Logger.log('Dataset configured: ' + dataset);
94 | return dataset;
95 | }
96 |
97 | Logger.log('Dataset was not configured.');
98 | return null;
99 | }
100 |
101 | // =============================================================================
102 | // Schema creation
103 | // =============================================================================
104 |
105 | /**
106 | * Create BigQuery dataset if it doesn't exist already
107 | */
108 | function createDatasetIfNotExists() {
109 | [projectId, datasetId] = getConfiguration();
110 |
111 | try {
112 | BigQuery.Datasets.get(projectId, datasetId);
113 | } catch (e) {
114 | Logger.log('Dataset does not exist. Creating new dataset with ID: ' +
115 | datasetId);
116 | const dataset = BigQuery.newDataset();
117 | const reference = BigQuery.newDatasetReference();
118 | reference.datasetId = datasetId;
119 | dataset.datasetReference = reference;
120 | BigQuery.Datasets.insert(dataset, projectId);
121 | }
122 | return datasetId;
123 | }
124 |
125 | /**
126 | * Create BigQuery table if it doesn't exist already
127 | */
128 | function createTableIfNotExists() {
129 | [projectId, datasetId] = getConfiguration();
130 |
131 | try {
132 | BigQuery.Tables.get(projectId, datasetId, TABLE_NAME);
133 | } catch (e) {
134 | Logger.log('Table does not exist. Creating new table with ID: ' +
135 | TABLE_NAME);
136 |
137 | // Create time-series and data field schema
138 | const dateSchema = BigQuery.newTableFieldSchema();
139 | dateSchema.name = DATETIME_COLUMN;
140 | dateSchema.type = 'timestamp';
141 | const dataSchema = BigQuery.newTableFieldSchema();
142 | dataSchema.name = DATA_COLUMN;
143 | dataSchema.type = 'float';
144 |
145 | // Create table schema with field schema
146 | const schema = BigQuery.newTableSchema();
147 | schema.fields = [dateSchema, dataSchema];
148 |
149 | // Create table reference with table name
150 | const reference = BigQuery.newTableReference();
151 | reference.tableId = TABLE_NAME;
152 |
153 | // Create table with schema and reference
154 | const table = BigQuery.newTable();
155 | table.schema = schema;
156 | table.tableReference = reference;
157 |
158 | // Issue command to create table in BigQuery
159 | BigQuery.Tables.insert(table, projectId, datasetId);
160 | }
161 | return TABLE_NAME;
162 | }
163 |
164 | // =============================================================================
165 | // Forecasting
166 | // =============================================================================
167 |
168 | /**
169 | * Create a forecasting model based on the input data
170 | */
171 | function train() {
172 | const CREATE_OPTIONS = {
173 | 'MODEL_TYPE': 'ARIMA',
174 | 'TIME_SERIES_TIMESTAMP_COL': DATETIME_COLUMN,
175 | 'TIME_SERIES_DATA_COL': DATA_COLUMN,
176 | };
177 |
178 | const project = getConfiguration()[0];
179 | const dataset = createDatasetIfNotExists();
180 | const table = project + '.' + dataset + '.' + createTableIfNotExists();
181 | const model = project + '.' + dataset + '.' + MODEL_NAME;
182 |
183 | const range = SpreadsheetApp.getActiveRange();
184 | if (!isValidTrainingData(range)) {
185 | return;
186 | }
187 |
188 | // Populate temporary table in BigQuery with selected data from sheet
189 | const inputs = SpreadsheetApp.getActiveRange().getValues();
190 | populateTable(project, table, [DATETIME_COLUMN, DATA_COLUMN], inputs);
191 |
192 | // Create a new model using training data in BigQuery
193 | const request = {
194 | query: 'CREATE OR REPLACE MODEL `' + model + '` ' +
195 | getOptionsStr(CREATE_OPTIONS) + ' AS SELECT * FROM `' + table + '`',
196 | useLegacySql: false,
197 | };
198 |
199 | runQuery(request, project);
200 | }
201 |
202 | /**
203 | * Basic validation function that checks range size and first row contents
204 | */
205 | function isValidTrainingData(range) {
206 | if (range.getNumRows() < 2 || range.getNumColumns() != 2) {
207 | SpreadsheetApp.getUi().alert('Multiple rows, each with 2 columns ' +
208 | '(date/time and number), must be selected.');
209 | return false;
210 | }
211 |
212 | const inputs = SpreadsheetApp.getActiveRange().getValues();
213 | const firstDate = new Date(inputs[0][0]);
214 | const firstNumber = new Number(inputs[0][1]);
215 |
216 | if (!firstDate instanceof Date || isNaN(firstDate)) {
217 | SpreadsheetApp.getUi().alert('1st column must be a date/time');
218 | return false;
219 | }
220 |
221 | if (!firstNumber instanceof Number || isNaN(firstNumber)) {
222 | SpreadsheetApp.getUi().alert('2nd column must be a number');
223 | return false;
224 | }
225 |
226 | // Check if using integers (to round forecasts later if needed)
227 | PropertiesService.getUserProperties().
228 | setProperty(INTEGER_PROPERTY, Number.isInteger(firstNumber.valueOf()));
229 |
230 | return true;
231 | }
232 |
233 | /**
234 | * Forecast starting one time step forward from the last trained date.
235 | * Returns a number of forecasts equal to the length of the selected range.
236 | * Each forecast contains the forecast date and predicted value.
237 | */
238 | function forecast() {
239 | const project = getConfiguration()[0];
240 | const dataset = createDatasetIfNotExists();
241 | const model = project + '.' + dataset + '.' + MODEL_NAME;
242 |
243 | try {
244 | BigQuery.Models.get(project, dataset, MODEL_NAME);
245 | } catch (e) {
246 | SpreadsheetApp.getUi().
247 | alert('Model not found. You must train a model before forecasting.');
248 | return;
249 | }
250 |
251 | const range = SpreadsheetApp.getActiveRange();
252 |
253 | let numRows;
254 | if (range.getNumRows() == 1 && range.getNumColumns() == 1) {
255 | numRows = getHorizon();
256 | } else {
257 | numRows = range.getNumRows();
258 | }
259 | const numColumns = 2;
260 |
261 |
262 | // Forecast a time series using the trained model
263 | request = {
264 | query: 'SELECT FORMAT_TIMESTAMP("%FT%T%Ez", forecast_timestamp), ' +
265 | 'forecast_value FROM ML.FORECAST(MODEL `' +
266 | model + '`, STRUCT(' + numRows + ' AS horizon))',
267 | useLegacySql: false,
268 | };
269 | const response = runQuery(request, project);
270 |
271 | // Extract forecasts from response
272 | const forecasts = [];
273 | const integers = PropertiesService.getUserProperties().
274 | getProperty(INTEGER_PROPERTY);
275 | const timezone = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
276 | for (const item of response) {
277 | // Extract forecast date and adjust for local time zone
278 | const utcDate = new Date(item.f[0].v);
279 | let offset = Utilities.formatDate(utcDate, timezone, 'XXX');
280 | offset = offset === 'Z' ? 'GMT+00:00' : 'GMT' + offset;
281 | const formattedDate = Utilities.formatDate(utcDate,
282 | 'UTC', 'EEE MMM d HH:mm:ss \'' + offset + '\' y');
283 | const date = new Date(formattedDate);
284 |
285 | // Round forecast if training data uses integers
286 | const forecast = integers === 'true' ?
287 | Math.round(item.f[1].v) : item.f[1].v;
288 |
289 | forecasts.push([date, forecast]);
290 | }
291 |
292 | // Write values back to sheet
293 | range.offset(0, 0, numRows, numColumns).setValues(forecasts);
294 | }
295 |
296 | /**
297 | * Prompt user for the forecast horizon if not inferred from selected range
298 | */
299 | function getHorizon() {
300 | const response = SpreadsheetApp.getUi().
301 | prompt('Number of time steps to forecast:');
302 | if (response.getSelectedButton() == SpreadsheetApp.getUi().Button.OK) {
303 | return response.getResponseText();
304 | }
305 |
306 | Logger.log('User did not select a horizon. Defaulting to 1 time step.');
307 | return 1;
308 | }
309 |
310 | // =============================================================================
311 | // Utilities
312 | // =============================================================================
313 |
314 | /**
315 | * Insert data from sheet as records into BigQuery table, for use in BQML models
316 | */
317 | function populateTable(project, table, columnNames, data) {
318 | // Delete existing rows from temporary table
319 | const request =
320 | { query: 'DELETE FROM `' + table + '` WHERE TRUE', useLegacySql: false };
321 | runQuery(request, project);
322 |
323 | // Insert records in batches, to avoid exceeding resource constraints
324 | const BATCH_SIZE = 500;
325 | for (let i = 0, j = data.length; i < j; i += BATCH_SIZE) {
326 | const batch = data.slice(i, i + BATCH_SIZE);
327 |
328 | const request = {
329 | query: 'INSERT `' + table + '` (' + columnNames.join() +
330 | ') VALUES ' + getValuesStr(batch),
331 | useLegacySql: false,
332 | };
333 | runQuery(request, project);
334 | }
335 | }
336 |
337 | /**
338 | * Converts an array into comma-separated strings for use in SQL statement
339 | */
340 | function getValuesStr(inputs) {
341 | const values = [];
342 | for (let i = 0; i < inputs.length; i++) {
343 | input = inputs[i];
344 | values.push('(' + inputs[i].map((x) => formatInput(x)).join() + ')');
345 | }
346 | return values.join();
347 | }
348 |
349 | /**
350 | * Formats variables for use in SQL:
351 | * Dates are converted to strings.
352 | * Strings are wrapped with quotes.
353 | * Numbers are left alone.
354 | */
355 | function formatInput(input) {
356 | if (input instanceof Date) {
357 | // Use local date, but shift to UTC to avoid any daylight savings issues.
358 | // When forecasting, the UTC date will be converted back to the local date.
359 | const timezone = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
360 | return '\'' + Utilities.formatDate(input, timezone,
361 | 'yyyy-MM-dd HH:mm:ss') + '\'';
362 | } else if (input instanceof String) {
363 | return '\'' + input + '\'';
364 | }
365 | return input;
366 | }
367 |
368 | /**
369 | * Creates SQL clause with options for model creation (e.g. MODEL_TYPE='ARIMA')
370 | */
371 | function getOptionsStr(modelOptions) {
372 | options = [];
373 | for (const o in modelOptions) {
374 | options.push(o + '=\'' + modelOptions[o] + '\'');
375 | }
376 | return 'OPTIONS(' + options.join() + ')';
377 | }
378 |
379 | /**
380 | * Runs a BigQuery query and logs the results in a spreadsheet.
381 | */
382 | function runQuery(request, projectId) {
383 | // @ts-ignore
384 | let queryResults = BigQuery.Jobs.query(request, projectId);
385 | const jobId = queryResults.jobReference.jobId;
386 |
387 | // Check on status of the Query Job.
388 | let sleepTimeMs = 500;
389 | while (!queryResults.jobComplete) {
390 | Utilities.sleep(sleepTimeMs);
391 | sleepTimeMs *= 2;
392 | queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
393 | }
394 |
395 | // Get all the rows of results.
396 | let rows = queryResults.rows;
397 | while (queryResults.pageToken) {
398 | queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
399 | pageToken: queryResults.pageToken,
400 | });
401 | rows = rows.concat(queryResults.rows);
402 | }
403 | return rows;
404 | }
405 |
406 |
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/images/appsscript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleworkspace/ml-integration-samples/721d72fa937ede136f8a4d058657b6a81672ab63/apps-script/BQMLForecasting/images/appsscript.png
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/images/forecast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleworkspace/ml-integration-samples/721d72fa937ede136f8a4d058657b6a81672ab63/apps-script/BQMLForecasting/images/forecast.png
--------------------------------------------------------------------------------
/apps-script/BQMLForecasting/images/train.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleworkspace/ml-integration-samples/721d72fa937ede136f8a4d058657b6a81672ab63/apps-script/BQMLForecasting/images/train.png
--------------------------------------------------------------------------------
/apps-script/README.md:
--------------------------------------------------------------------------------
1 | # Apps Script URL Fetch Samples
2 |
3 | In this directory, we illustrate how to call Cloud ML APIs with the Apps Script URLFetchApp.
4 |
--------------------------------------------------------------------------------
/apps-script/automl/naturallanguage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2022 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 | * http://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 | * Set variables for GCP project containing AutoML NL model.
19 | */
20 | var PROJECT_ID = 'YOUR_PROJECT_ID';
21 | var MODEL_ID = 'YOUR_MODEL_ID';
22 |
23 | /**
24 | * Creates variables for the service account key and email address.
25 | * Authorization to AutoML requires a GCP service account with role of 'AutoML Predictor'.
26 | */
27 | // Sets variable for service account key.
28 | var PRIVATE_KEY = 'YOUR_PRIVATE_KEY';
29 | // Sets variable for service account email address.
30 | var CLIENT_EMAIL = 'YOUR_SVCACCT_EMAIL';
31 |
32 | /**
33 | * Calls the AutoML NL service API with a string
34 | * @param {String} line the line of string
35 | * @return {Object} prediction for the string based on model type
36 | */
37 | function retrieveSentiment (line) {
38 | var service = getService();
39 | if (service.hasAccess()) {
40 | var apiEndPoint = 'https://automl.googleapis.com/v1beta1/projects/' +
41 | PROJECT_ID + '/locations/us-central1/models/' +
42 | MODEL_ID + ':predict';
43 | // Creates a structure with the text and request type.
44 | var nlData = {
45 | payload: {
46 | textSnippet: {
47 | content: line,
48 | mime_type: 'text/plain'
49 | },
50 | }
51 | };
52 | // Packages all of the options and the data together for the call.
53 | var nlOptions = {
54 | method : 'post',
55 | contentType: 'application/json',
56 | headers: {
57 | Authorization: 'Bearer ' + service.getAccessToken()
58 | },
59 | payload : JSON.stringify(nlData)
60 | };
61 | // And makes the call.
62 | var response = UrlFetchApp.fetch(apiEndPoint, nlOptions);
63 | var nlData = JSON.parse(response);
64 | return nlData;
65 | } else {
66 | Logger.log(service.getLastError());
67 | }
68 | }
69 |
70 | /**
71 | * Reset the authorization state, so that it can be re-tested.
72 | */
73 | function reset() {
74 | var service = getService();
75 | service.reset();
76 | }
77 |
78 | /**
79 | * Configures the service.
80 | */
81 | function getService() {
82 | return OAuth2.createService('GCP')
83 | // Set the endpoint URL.
84 | .setTokenUrl('https://accounts.google.com/o/oauth2/token')
85 |
86 | // Set the private key and issuer.
87 | .setPrivateKey(PRIVATE_KEY)
88 | .setIssuer(CLIENT_EMAIL)
89 |
90 | // Set the property store where authorized tokens should be persisted.
91 | .setPropertyStore(PropertiesService.getScriptProperties())
92 |
93 | // Set the scope. This must match one of the scopes configured during the
94 | // setup of domain-wide delegation.
95 | .setScope(['https://www.googleapis.com/auth/cloud-platform']);
96 | }
97 |
--------------------------------------------------------------------------------
/apps-script/documentai/documentai.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2022 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 | * http://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 | * This code sample shows how to the call the Cloud Document AI API using Apps
19 | * Script with OAuth. Before running this code please see the documentation
20 | * guide on getting set up with Document AI processors:
21 | * https://cloud.google.com/document-ai/docs/create-processor.
22 | */
23 | const PROJECT_ID = '';
24 | const PROJECT_NUMBER = '';
25 | const LOCATION = ''; // Format is 'us' or 'eu'
26 | const PROCESSOR_ID = ''; // Create processor in Cloud Console
27 |
28 | const TEST_FILE_ID = ''; // Drive ID of an image or pdf to use for testing
29 |
30 | /**
31 | * Creates variables for the service account key and email address.
32 | * Authorization to DocAI requires a GCP service account with a Document AI role.
33 | */
34 | // Sets variable for service account privat3e key.
35 | const PRIVATE_KEY = '';
36 |
37 | // Sets variable for service account email address.
38 | const CLIENT_EMAIL = 'YOUR-SERVICE-ACCOUNT@appspot.gserviceaccount.com';
39 |
40 | function processDocument(docBytes) {
41 | var service = getService();
42 | if (!service.hasAccess()) {
43 | Logger.log(service.getLastError());
44 | return;
45 | }
46 | var apiEndPoint = 'https://' + LOCATION + '-documentai.googleapis.com/v1beta3/projects/' + PROJECT_NUMBER
47 | + '/locations/' + LOCATION + '/processors/' + PROCESSOR_ID + ':process';
48 |
49 | // Processor name, should look like: `projects/${projectId}/locations/${location}/processors/${processorId}`;
50 | const name = '';
51 |
52 | // Creates a structure with the text and request type.
53 | var requestData = {
54 | name,
55 | document: {
56 | content: docBytes,
57 | mime_type: 'application/pdf'
58 | },
59 | };
60 |
61 | // Packages all of the options and the data together for the call.
62 | var options = {
63 | method: 'post',
64 | contentType: 'application/json',
65 | headers: {
66 | Authorization: 'Bearer ' + service.getAccessToken()
67 | },
68 | payload: JSON.stringify(requestData)
69 | };
70 | // And makes the call.
71 | var response = UrlFetchApp.fetch(apiEndPoint, options);
72 | var data = JSON.parse(response);
73 | return data;
74 | }
75 |
76 | /**
77 | * Reset the authorization state, so that it can be re-tested.
78 | */
79 | function reset() {
80 | var service = getService();
81 | service.reset();
82 | }
83 |
84 | /**
85 | * Configures the service.
86 | */
87 | function getService() {
88 | return OAuth2.createService('GCP')
89 | // Set the endpoint URL.
90 | .setTokenUrl('https://accounts.google.com/o/oauth2/token')
91 |
92 | // Set the private key and issuer.
93 | .setPrivateKey(PRIVATE_KEY)
94 | .setIssuer(CLIENT_EMAIL)
95 |
96 | // Set the property store where authorized tokens should be persisted.
97 | .setPropertyStore(PropertiesService.getScriptProperties())
98 |
99 | // Set the scope. This must match one of the scopes configured during the
100 | // setup of domain-wide delegation.
101 | .setScope(['https://www.googleapis.com/auth/cloud-platform']);
102 | }
103 |
104 | function test() {
105 | var file = DriveApp.getFileById(TEST_FILE_ID);
106 | console.log(file.getName());
107 | var docBytes = Utilities.base64Encode(file.getBlob().getBytes());
108 | var response = processDocument(docBytes);
109 | console.log(JSON.stringify(response, null, 4));
110 | }
111 |
--------------------------------------------------------------------------------
/apps-script/nl_api_entitysentiment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2022 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 | * http://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 | // Sets API key for accessing Cloud Natural Language API.
18 | var myApiKey = "YOUR_API_KEY_HERE";
19 |
20 | /**
21 | * Calls the Cloud Natural Language API with a string of text to analyze
22 | * entities and sentiment present in the string.
23 | * @param {String} line - the string for entity sentiment analysis
24 | * @return {Object} the entities and related sentiment present in the string
25 | */
26 | function retrieveEntitySentiment(line) {
27 | var apiKey = myApiKey;
28 | var apiEndpoint = 'https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=' + apiKey;
29 | // Creates a JSON request, with text string, language, type and encoding
30 | var nlData = {
31 | document: {
32 | language: 'en-us',
33 | type: 'PLAIN_TEXT',
34 | content: line,
35 | },
36 | encodingType: 'UTF8',
37 | };
38 | // Packages all of the options and the data together for the API call.
39 | var nlOptions = {
40 | method: 'post',
41 | contentType: 'application/json',
42 | payload: JSON.stringify(nlData),
43 | };
44 | // Makes the API call.
45 | var response = UrlFetchApp.fetch(apiEndpoint, nlOptions);
46 | return JSON.parse(response);
47 | };
48 |
--------------------------------------------------------------------------------
/apps-script/vision_api.gs:
--------------------------------------------------------------------------------
1 |
2 | API_KEY = "your API key"
3 |
4 | function CloudVisionAPI(image_bytes) {
5 |
6 | const payload = JSON.stringify({
7 | requests: [{
8 | image: {
9 | content: Utilities.base64Encode(image_bytes)
10 | },
11 | features: [
12 | {
13 | type:"LABEL_DETECTION",
14 | maxResults: 10
15 | }
16 | ]
17 | }]
18 | });
19 |
20 |
21 | const url = 'https://vision.googleapis.com/v1/images:annotate?key=' + API_KEY;
22 |
23 | const response = UrlFetchApp.fetch(url, {
24 | method: 'POST',
25 | contentType: 'application/json',
26 | payload: payload,
27 | muteHttpExceptions: true
28 | }).getContentText();
29 |
30 | const json_resp = JSON.parse(response)
31 |
32 | Logger.log(json_resp)
33 |
34 | var labels = []
35 |
36 | json_resp.responses[0].labelAnnotations.forEach(function (label) {
37 | labels.push(label.description)
38 | })
39 |
40 | return labels
41 | }
42 |
--------------------------------------------------------------------------------
/java/Docs2Speech/README.md:
--------------------------------------------------------------------------------
1 | # Docs2Speech Demo
2 |
3 | Turn your meeting notes into an audio file you can list to on the go! This application
4 | converts a Google Doc into an .mp3 file using the [Cloud Text-to-Speech API][text-speech-api]. We use the
5 | [Google Docs API][docs-api] to extract the text of the document then produce a high quality
6 | recording of natural human speech.
7 |
8 | # Set up instructions
9 |
10 | 1. Create a GCP project and enable the Google Docs API and the Cloud Text-to-Speech API in the
11 | [cloud console][cloud-console].
12 |
13 | 1. Replace the variable `DOCUMENT_ID` placeholder in DocsToSpeech.java with the ID of the
14 | file you wish to transcribe. For more information on document ids, please see [here][doc-ids].
15 |
16 | 1. You will need to authenticate with OAuth to access your Google Doc. Follow the instructions
17 | [here][docs-java] on how to get a client configuration (credentials.json) and save it to this directory.
18 |
19 | 1. ```gradle run``` and you should see an output file stored to your local directory. The first
20 | time you run this, you should be asked to authenticate in the browser.
21 |
22 | # Next steps
23 |
24 | We suggest expanding this sample to read multiple files from a Drive folder or uploading the
25 | output files to Google Cloud Storage.
26 |
27 | [cloud-console]: https://console.cloud.google.com/
28 | [docs-java]: https://developers.google.com/docs/api/quickstart/java
29 | [docs-ids]: https://developers.google.com/docs/api/how-tos/overview
30 | [docs-api]: https://cloud.google.com/text-to-speech/docs
31 | [text-speech-api]: https://cloud.google.com/text-to-speech/docs
32 |
--------------------------------------------------------------------------------
/java/Docs2Speech/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 | apply plugin: 'application'
3 |
4 | mainClassName = 'DocsToSpeech'
5 | sourceCompatibility = 1.8
6 | targetCompatibility = 1.8
7 | version = '1.0'
8 |
9 | repositories {
10 | mavenCentral()
11 | }
12 |
13 | dependencies {
14 | compile 'com.google.api-client:google-api-client:1.23.0'
15 | compile 'com.google.cloud:google-cloud-texttospeech:0.74.0-beta'
16 | compile 'com.google.guava:guava:27.0.1-jre'
17 | compile 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
18 | compile files('libs/java_docs_trusted_tester_lib.jar')
19 | }
20 |
21 | run {
22 | standardInput = System.in
23 | }
24 |
--------------------------------------------------------------------------------
/java/Docs2Speech/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/java/Docs2Speech/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * The settings file is used to specify which projects to include in your build.
5 | *
6 | * Detailed information about configuring a multi-project build in Gradle can be found
7 | * in the user guide at https://docs.gradle.org/4.8/userguide/multi_project_builds.html
8 | */
9 |
10 | rootProject.name = 'Docs2Speech'
11 |
--------------------------------------------------------------------------------
/java/Docs2Speech/src/main/java/DocsToSpeech.java:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import com.google.api.client.auth.oauth2.Credential;
16 | import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
17 | import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
18 | import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
19 | import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
20 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
21 | import com.google.api.client.http.javanet.NetHttpTransport;
22 | import com.google.api.client.json.JsonFactory;
23 | import com.google.api.client.json.jackson2.JacksonFactory;
24 | import com.google.api.client.util.store.FileDataStoreFactory;
25 | import com.google.api.services.docs.v1.Docs;
26 | import com.google.api.services.docs.v1.DocsScopes;
27 | import com.google.api.services.docs.v1.model.Document;
28 | import com.google.api.services.docs.v1.model.ParagraphElement;
29 | import com.google.api.services.docs.v1.model.StructuralElement;
30 | import com.google.api.services.docs.v1.model.TableCell;
31 | import com.google.api.services.docs.v1.model.TableRow;
32 | import com.google.api.services.docs.v1.model.TextRun;
33 | import com.google.protobuf.ByteString;
34 |
35 | import java.io.FileOutputStream;
36 | import java.io.IOException;
37 | import java.io.InputStream;
38 | import java.io.InputStreamReader;
39 | import java.io.OutputStream;
40 | import java.security.GeneralSecurityException;
41 | import java.util.Collections;
42 | import java.util.List;
43 |
44 | /**
45 | * DocsToSpeech is a utility that reads a Google Doc and generate an audio file of the text
46 | * content.
47 | */
48 | public class DocsToSpeech {
49 | private static final String APPLICATION_NAME = "DocsToSpeech Demo";
50 | private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
51 | private static final String TOKENS_DIRECTORY_PATH = "tokens";
52 | private static final String DOCUMENT_ID = "YOUR_DOCUMENT_ID";
53 |
54 | /**
55 | * Global instance of the scopes required by this quickstart.
56 | * If modifying these scopes, delete your previously saved tokens/ folder.
57 | */
58 | private static final List SCOPES = Collections.singletonList(
59 | DocsScopes.DOCUMENTS_READONLY);
60 | private static final String CREDENTIALS_FILE_PATH = "/credentials.json";
61 |
62 | /**
63 | * Creates an authorized Credential object.
64 | *
65 | * @param httpTransport The network HTTP Transport.
66 | * @return An authorized Credential object.
67 | * @throws IOException If the credentials.json file cannot be found.
68 | */
69 | private static Credential getCredentials(final NetHttpTransport httpTransport)
70 | throws IOException {
71 | // Load credentials.
72 | InputStream in = DocsToSpeech.class.getResourceAsStream(CREDENTIALS_FILE_PATH);
73 | GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
74 | new InputStreamReader(in));
75 |
76 | // Build flow and trigger user authorization request.
77 | GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
78 | httpTransport, JSON_FACTORY, clientSecrets, SCOPES)
79 | .setDataStoreFactory(new FileDataStoreFactory(
80 | new java.io.File(TOKENS_DIRECTORY_PATH)))
81 | .setAccessType("offline")
82 | .build();
83 | LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
84 | return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
85 | }
86 |
87 | private static String readParagraphElement(ParagraphElement element) {
88 | TextRun run = element.getTextRun();
89 | if (run == null || run.getContent() == null) {
90 | // The TextRun can be null if there is an inline object.
91 | return "";
92 | }
93 | return run.getContent();
94 | }
95 |
96 | /**
97 | * Recurses through a list of Structural Elements to read a document's text where text may be
98 | * in nested elements.
99 | *
100 | * @param elements Structural Elements to inspect
101 | */
102 | private static String readStructrualElements(List elements) {
103 | StringBuilder sb = new StringBuilder();
104 | for (StructuralElement element : elements) {
105 | if (element.getParagraph() != null) {
106 | for (ParagraphElement paragraphElement : element.getParagraph().getElements()) {
107 | sb.append(readParagraphElement(paragraphElement));
108 | }
109 | } else if (element.getTable() != null) {
110 | // The text in table cells are in nested Structural Elements and tables may be
111 | // nested.
112 | for (TableRow row : element.getTable().getTableRows()) {
113 | for (TableCell cell : row.getTableCells()) {
114 | sb.append(readStructrualElements(cell.getContent()));
115 | }
116 | }
117 | } else if (element.getTableOfContents() != null) {
118 | // The text in the TOC is also in a Structural Element.
119 | sb.append(readStructrualElements(element.getTableOfContents().getContent()));
120 | }
121 | }
122 | return sb.toString();
123 | }
124 |
125 | public static void main(String... args) throws IOException, GeneralSecurityException {
126 | // Build a new authorized API client service.
127 | final NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
128 | Docs service = new Docs.Builder(httpTransport, JSON_FACTORY,
129 | getCredentials(httpTransport))
130 | .setApplicationName(APPLICATION_NAME)
131 | .build();
132 | Document doc = service.documents().get(DOCUMENT_ID).execute();
133 | SpeechClient speechClient = new SpeechClient();
134 | // The text of the document is contained in the body's content which is the top level list
135 | // of Structural Elements.
136 | String docText = readStructrualElements(doc.getBody().getContent());
137 |
138 | ByteString audioBytes = speechClient.createAudio(docText);
139 | // Write the response to an output file.
140 | try (OutputStream out = new FileOutputStream("output.mp3")) {
141 | out.write(audioBytes.toByteArray());
142 | System.out.println("Audio content written to file \"output.mp3\"");
143 | } catch (Exception e) {
144 | e.printStackTrace();
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/java/Docs2Speech/src/main/java/SpeechClient.java:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import com.google.cloud.texttospeech.v1.AudioConfig;
16 | import com.google.cloud.texttospeech.v1.AudioEncoding;
17 | import com.google.cloud.texttospeech.v1.SsmlVoiceGender;
18 | import com.google.cloud.texttospeech.v1.SynthesisInput;
19 | import com.google.cloud.texttospeech.v1.SynthesizeSpeechResponse;
20 | import com.google.cloud.texttospeech.v1.TextToSpeechClient;
21 | import com.google.cloud.texttospeech.v1.VoiceSelectionParams;
22 | import com.google.protobuf.ByteString;
23 |
24 | import java.io.IOException;
25 |
26 | /**
27 | * SpeechClient is a wrapper for the Cloud TextToSpeech API.
28 | */
29 | class SpeechClient {
30 | private TextToSpeechClient client;
31 |
32 | public SpeechClient() {
33 | // Instantiates a client
34 | try {
35 | client = TextToSpeechClient.create();
36 | } catch (IOException e) {
37 | e.printStackTrace();
38 | }
39 | }
40 |
41 | /**
42 | * Creates the audio of the provided text and returns the mp3 file bytes.
43 | * @param text the text to synthesize
44 | */
45 | ByteString createAudio(String text) {
46 | // Set the text input to be synthesized
47 | SynthesisInput input = SynthesisInput.newBuilder()
48 | .setText(text)
49 | .build();
50 |
51 | VoiceSelectionParams voice = VoiceSelectionParams.newBuilder()
52 | .setLanguageCode("en-US")
53 | .setSsmlGender(SsmlVoiceGender.NEUTRAL)
54 | .build();
55 |
56 | // Select the type of audio file you want returned
57 | AudioConfig audioConfig = AudioConfig.newBuilder()
58 | .setAudioEncoding(AudioEncoding.MP3)
59 | .build();
60 |
61 | // Perform the text-to-speech request on the text input with the selected voice parameters
62 | // and audio file type
63 | SynthesizeSpeechResponse response = client.synthesizeSpeech(input, voice,
64 | audioConfig);
65 |
66 | // Get the audio contents from the response
67 | ByteString audioContents = response.getAudioContent();
68 | return audioContents;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/node/SmartAccountsBot/README.md:
--------------------------------------------------------------------------------
1 | # SmartAccountsBot
2 | Smart accounts bot is an example Dialogflow chat bot that extracts information
3 | a sales team uses in a Google Sheet. The user can quickly figure out the best
4 | contact for any account requests they might encounter.
5 |
6 | Co-Authors: Anu Srivastava, Lee Boonstra
7 |
8 | [](https://www.youtube.com/watch?v=n99sQBtYulQ)
9 |
10 | ## Prerequisites
11 |
12 | The set up instructions are below assume experience with Google Cloud and Dialogflow.
13 | We suggest completing this [codelab][codelab] on fulfillment with Dialogflow prior to
14 | setting up this demo.
15 |
16 | [codelab]: https://codelabs.developers.google.com/codelabs/dialogflow-assistant-tvguide/index.html?index=..%2F..index#3
17 |
18 | ## Set up
19 |
20 | 1. Download the zip file of the Dialogflow agent and import into your own project
21 | in the Dialogflow console.
22 |
23 | 1. Find the GCP project for your agent and enable the Maps Static API and create
24 | an API key. Copy this API key into the `cardBuilder.js` file.
25 |
26 | 1. Enable fulfillment through Dialoglow console and run the default function for set up.
27 | Next deploy this function to your project.
28 | `gcloud functions deploy dialogflowFirebaseFulfillment`
29 |
30 | 1. If your code encounters permissions issues for the source sheet, make a copy of
31 | the sheet in your Drive:
32 | `https://docs.google.com/spreadsheets/d/1HBcfIJMv7xhucMnAFrrXzjmRvbwz1iVpc-rfb26RAFg/copy`
33 | Then share the Sheet with the service account your function runs as. You an find the
34 | account in the `Settings` page of the Dialogflow console for your agent.
35 |
36 | For questions or set up help, tweet at us: @ladysign and @asrivas_dev
37 |
--------------------------------------------------------------------------------
/node/SmartAccountsBot/dialogflowFirebaseFulfilment/cardBuilder.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2022 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 | * http://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 ACCOUNT_IMAGE_URL = 'https://www.gstatic.com/images/icons/material/system_gm/1x/account_circle_black_18dp.png';
18 | const ACCOUNTS_SHEET_URL = 'https://docs.google.com/spreadsheets/d/1HBcfIJMv7xhucMnAFrrXzjmRvbwz1iVpc-rfb26RAFg/edit';
19 | const ACCOUNTS_SHEET_ID = '1HBcfIJMv7xhucMnAFrrXzjmRvbwz1iVpc-rfb26RAFg';
20 | const MAPS_IMAGE_BASE_URL = 'https://maps.googleapis.com/maps/api/staticmap?';
21 | const API_KEY = 'YOUR_API_KEY';
22 |
23 | const LOCATION_VARS = {
24 | 'AMS' : 'center=Claude%20Debussylaan%2034,%201082%20MD%20Amsterdam,%20Netherlands&zoom=14&size=400x400',
25 | 'MTV' : 'center=Googleplex&zoom=14&size=200x200',
26 | 'SFO' : 'center=345%20Spear%20Street,%20San%20Francisco,%20CA&zoom=14&size=200x200',
27 | 'LON' : 'center=6%20Pancras%20Square,%20Kings%20Cross,%20London%20N1C%204AG,%20UK&zoom=14&size=200x200',
28 | 'NYC' : 'center=111%208th%20Ave,%20New%20York,%20NY%2010011&zoom=14&size=200x200'
29 | }
30 |
31 | const createContactCard = async (sheets, contact, cluster,
32 | account, role, skill) => {
33 | const contactRow = await getContactInfo(sheets, contact);
34 | const email = contactRow[1];
35 | const location = contactRow[2];
36 |
37 | // For production use, we recommend restricting and signing your API Key
38 | // See: https://developers.google.com/maps/documentation/maps-static/get-api-key
39 | const mapsImageURL = MAPS_IMAGE_BASE_URL + LOCATION_VARS[location] + '&key=' + API_KEY;
40 |
41 | const cardHeader = {
42 | title: account + ' ' + role + ' ' + 'Contact',
43 | subtitle: skill + ': ' + contact,
44 | imageUrl: ACCOUNT_IMAGE_URL,
45 | imageStyle: 'IMAGE',
46 | };
47 |
48 | const clusterWidget = {
49 | keyValue: {
50 | content: 'Cluster',
51 | bottomLabel: cluster,
52 | },
53 | };
54 |
55 | const emailWidget = {
56 | keyValue: {
57 | content: 'Email',
58 | bottomLabel: email,
59 | },
60 | };
61 |
62 | const locationWidget = {
63 | keyValue: {
64 | content: 'Location',
65 | bottomLabel: location,
66 | },
67 | };
68 |
69 | const mapImageWidget = {
70 | 'image': {
71 | 'imageUrl': mapsImageURL,
72 | 'onClick': {
73 | 'openLink': {
74 | 'url': mapsImageURL,
75 | },
76 | },
77 | },
78 | };
79 |
80 | const infoSection = {widgets: [clusterWidget, emailWidget,
81 | locationWidget, mapImageWidget]};
82 |
83 | return {
84 | 'hangouts': {
85 | 'name': 'Contact Card',
86 | 'header': cardHeader,
87 | 'sections': [infoSection],
88 | },
89 | };
90 | };
91 |
92 | const createErrorCard = (accountName, errorMessage) => {
93 | console.log(`in createErrorCard`);
94 | const cardHeader = {
95 | title: 'Account Information Not Found',
96 | subtitle: 'Account requested: ' + accountName,
97 | imageUrl: ACCOUNT_IMAGE_URL,
98 | imageStyle: 'IMAGE',
99 | };
100 |
101 | const errorWidget = {
102 | textParagraph: {
103 | text: errorMessage,
104 | },
105 | };
106 |
107 | const textWidget = {
108 | textParagraph: {
109 | text: 'Please check the account data is up to date',
110 | },
111 | };
112 |
113 | const buttonWidget = {
114 | buttons: [
115 | {
116 | textButton: {
117 | text: 'Account Data',
118 | onClick: {
119 | openLink: {
120 | url: ACCOUNTS_SHEET_URL,
121 | },
122 | },
123 | },
124 | },
125 | ],
126 | };
127 |
128 | const infoSection = {widgets: [errorWidget, textWidget, buttonWidget]};
129 | return {
130 | 'hangouts': {
131 | 'name': 'No Owner Found',
132 | 'header': cardHeader,
133 | 'sections': [infoSection],
134 | },
135 | };
136 | };
137 |
138 | /**
139 | * Looks up the email address for the given contact name.
140 | *
141 | * @param {Object} sheets The Sheets client
142 | * @param {*} name The name of the contact to look up
143 | */
144 | async function getContactInfo(sheets, name) {
145 | const response = await sheets.spreadsheets.values.get({
146 | spreadsheetId: ACCOUNTS_SHEET_ID,
147 | range: 'Contacts!A:D',
148 | });
149 | const contactTable = response.data.values;
150 | const contactRow = contactTable.find((entry) => entry[0] === name);
151 | return contactRow;
152 | }
153 |
154 | exports.createContactCard = createContactCard;
155 | exports.createErrorCard = createErrorCard;
156 |
--------------------------------------------------------------------------------
/node/SmartAccountsBot/dialogflowFirebaseFulfilment/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2022 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 | * http://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 | // See https://github.com/dialogflow/dialogflow-fulfillment-nodejs
18 | // for Dialogflow fulfillment library docs, samples, and to report issues
19 | 'use strict';
20 |
21 | const functions = require('firebase-functions');
22 | const {google} = require('googleapis');
23 | const cardBuilder = require('./cardBuilder');
24 |
25 | const {
26 | WebhookClient,
27 | Payload,
28 | } = require('dialogflow-fulfillment');
29 |
30 | process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
31 |
32 | const ACCOUNTS_SHEET_ID = '1HBcfIJMv7xhucMnAFrrXzjmRvbwz1iVpc-rfb26RAFg';
33 |
34 | const CLUSTER_RANGES = {
35 | 'CLUSTER1': 'Clusters!C1:D27',
36 | 'CLUSTER2': 'Clusters!E1:F27',
37 | 'CLUSTER3': 'Clusters!G1:H27',
38 | 'CLUSTER4': 'Clusters!I1:J27',
39 | };
40 |
41 | const SKILL_ROWS = {
42 | 'Infra': 7,
43 | 'Hybrid': 8,
44 | 'Data&Analytics': 9,
45 | 'Data Management': 10,
46 | 'Security': 11,
47 | 'Networking': 12,
48 | 'AI / ML': 13,
49 | 'G Suite': 14,
50 | 'SAP': 15,
51 | };
52 |
53 | const ACCOUNT_MANAGER = 'ACCOUNT MANAGER';
54 | const ENGINEER = 'ENGINEER';
55 | // If modifying these scopes, delete token.json.
56 | const SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
57 | // The file token.json stores the user's access and refresh tokens, and is
58 | // created automatically when the authorization flow completes for the first
59 | // time.
60 |
61 | /**
62 | * Authenticates the Sheets API client for read-only access.
63 | *
64 | * @return {Object} sheets client
65 | */
66 | async function getSheetsClient() {
67 | // Should change this to file.only probably
68 | const auth = await google.auth.getClient({
69 | scopes: [SCOPES],
70 | });
71 | return await google.sheets({version: 'v4', auth});
72 | }
73 |
74 | /**
75 | * Finds the Googler for the requested parameters.
76 | * @param {*} agent the Dialogflow agent
77 | */
78 | async function findGoogler(agent) {
79 | const role = agent.parameters.role;
80 | const account = agent.parameters.account;
81 | const skill = agent.parameters.skill;
82 | const sheets = await getSheetsClient();
83 |
84 | await lookupContact(agent, sheets, account, skill, role);
85 | }
86 |
87 | /**
88 | * Looks up the appropriate contact name.
89 | * @param {Object} agent The Dialogflow agent.
90 | * @param {*} sheets The sheets service.
91 | * @param {String} account The company name.
92 | * @param {String} skill The specialization.
93 | * @param {String} role Must be either 'Account Manager' or 'Engineer'.
94 | */
95 | async function lookupContact(agent, sheets, account, skill, role) {
96 | // TODO: Refactor to return cluster values to save a 2nd read
97 | // in getContactName.
98 | let errorMessage = '';
99 | let cardJSON = {};
100 | const cluster = await getCluster(sheets, account);
101 |
102 | if (cluster === '') {
103 | errorMessage = `Cluster not found.`;
104 | console.error(errorMessage);
105 | cardJSON = cardBuilder.createErrorCard(account, errorMessage);
106 | addHangoutsCustomPayload(agent, cardJSON);
107 | return;
108 | }
109 | const skillIndex = getSkillRow(skill);
110 | if (skillIndex == -1) {
111 | errorMessage = `Skill not found.`;
112 | console.error(errorMessage);
113 | cardJSON = cardBuilder.createErrorCard(account, errorMessage);
114 | addHangoutsCustomPayload(agent, cardJSON);
115 | return;
116 | }
117 | const contact = await getContactName(sheets, cluster, role, skillIndex);
118 |
119 | if (contact !== '') {
120 | agent.add(`Please contact: ${contact}`);
121 | cardJSON = await cardBuilder.createContactCard(sheets, contact, cluster, account,
122 | role, skill);
123 | } else {
124 | errorMessage = `No contact person found.`;
125 | console.error(errorMessage);
126 | agent.add(errorMessage);
127 | cardJSON = cardBuilder.createErrorCard(account, errorMessage);
128 | }
129 |
130 | addHangoutsCustomPayload(agent, cardJSON);
131 | return contact;
132 | }
133 |
134 | /**
135 | * Adds the Hangouts Card response to the agent.
136 | *
137 | * @param {*} agent The Dialogflow agent
138 | * @param {*} cardJSON The structure of the Hangouts chat card.
139 | */
140 | function addHangoutsCustomPayload(agent, cardJSON) {
141 | const payload = new Payload(
142 | 'hangouts',
143 | cardJSON,
144 | {rawPayload: true, sendAsMessage: true},
145 | );
146 | agent.add(payload);
147 | }
148 |
149 | /**
150 | * Looks up which cluster the company belongs to
151 | * @param {sheets_v4.Sheets} sheets The sheets service.
152 | * @param {String} account The company name.
153 | * @return {String} The cluster name.
154 | */
155 | async function getCluster(sheets, account) {
156 | // eslint-disable-next-line guard-for-in
157 | for (const [clusterKey, range] of Object.entries(CLUSTER_RANGES)) {
158 | const response = await sheets.spreadsheets.values.get({
159 | spreadsheetId: ACCOUNTS_SHEET_ID,
160 | range,
161 | });
162 | const values = response.data.values;
163 | // TODO: change to map & filter
164 | for (let i = 0; i < values.length; i++) {
165 | const row = values[i];
166 | if (row[0] == account || row[1] == account) {
167 | return clusterKey;
168 | }
169 | }
170 | }
171 | return '';
172 | }
173 |
174 | /**
175 | * Returns the row index for the given specialization.
176 | * This maps to Column A in the accounts Sheet.
177 | * @param {String} skill The specialization
178 | * @return {Integer} the index for the specialization or -1 if not found.
179 | */
180 | function getSkillRow(skill) {
181 | for (const key in SKILL_ROWS) {
182 | if (skill == key) {
183 | return SKILL_ROWS[skill];
184 | }
185 | }
186 | return -1;
187 | }
188 |
189 | /**
190 | * Looks up the appropriate contact name in the cluster
191 | * @param {*} sheets The Sheets service.
192 | * @param {String} clusterKey The cluster key name.
193 | * @param {String} role Must be either 'Account Manager' or 'Engineer'
194 | * @param {Integer} skillIndex the index for the specialization.
195 | */
196 | async function getContactName(sheets, clusterKey, role, skillIndex) {
197 | const range = CLUSTER_RANGES[clusterKey];
198 | const response = await sheets.spreadsheets.values.get({
199 | spreadsheetId: ACCOUNTS_SHEET_ID,
200 | range,
201 | });
202 | const values = response.data.values;
203 | const roleIndex = (role == ACCOUNT_MANAGER) ? 0 : 1;
204 | // Skills are indexed at 1.
205 | return values[skillIndex - 1][roleIndex];
206 | }
207 |
208 | exports.dialogflowFirebaseFulfillment = functions.https.onRequest(
209 | (request, response) => {
210 | const agent = new WebhookClient({request, response});
211 | console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
212 | console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
213 |
214 | function welcome(agent) {
215 | agent.add(`Welcome to my agent!`);
216 | }
217 |
218 | function fallback(agent) {
219 | agent.add(`I didn't understand`);
220 | agent.add(`I'm sorry, can you try again?`);
221 | }
222 | // Run the proper function handler based on the matched Dialogflow intent name
223 | const intentMap = new Map();
224 | intentMap.set('Default Welcome Intent', welcome);
225 | intentMap.set('Default Fallback Intent', fallback);
226 | intentMap.set('Look up Googler', findGoogler);
227 | agent.handleRequest(intentMap);
228 | });
229 |
--------------------------------------------------------------------------------
/node/SmartAccountsBot/dialogflowFirebaseFulfilment/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dialogflowFirebaseFulfillment",
3 | "description": "This is Dialogflow Hangouts demo.",
4 | "version": "0.0.1",
5 | "private": true,
6 | "license": "Apache Version 2.0",
7 | "author": "Anu Srivastava",
8 | "engines": {
9 | "node": "8"
10 | },
11 | "scripts": {
12 | "start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
13 | "deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
14 | },
15 | "dependencies": {
16 | "actions-on-google": "^2.12.0",
17 | "dialogflow": "^1.2.0",
18 | "dialogflow-fulfillment": "^0.6.1",
19 | "firebase-admin": "^8.12.1",
20 | "firebase-functions": "^3.6.1",
21 | "googleapis": "^39.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/node/SmartAccountsBot/sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/googleworkspace/ml-integration-samples/721d72fa937ede136f8a4d058657b6a81672ab63/node/SmartAccountsBot/sample.png
--------------------------------------------------------------------------------