From df60a449386ac49d352ba800fe69b74aecd4e31d Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Thu, 27 Mar 2025 13:30:58 -0600 Subject: [PATCH] feat: vector embedding demo --- .vscode/settings.json | 6 + pnpm-lock.yaml | 12 + projects/vector-embeddings/.clasp.json | 16 ++ projects/vector-embeddings/README.md | 236 ++++++++++++++++++ ...t 2025-03-27 at 1.27.28\342\200\257PM.png" | Bin 0 -> 53552 bytes projects/vector-embeddings/build.js | 30 +++ projects/vector-embeddings/package.json | 19 ++ projects/vector-embeddings/polyfill.js | 1 + .../vector-embeddings/src/appsscript.json | 10 + projects/vector-embeddings/src/examples.js | 61 +++++ projects/vector-embeddings/src/index.ts | 1 + projects/vector-embeddings/src/internal.d.ts | 16 ++ projects/vector-embeddings/src/main.js | 133 ++++++++++ projects/vector-embeddings/src/tools.js | 52 ++++ projects/vector-embeddings/tsconfig.json | 23 ++ 15 files changed, 616 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 projects/vector-embeddings/.clasp.json create mode 100644 projects/vector-embeddings/README.md create mode 100644 "projects/vector-embeddings/Screenshot 2025-03-27 at 1.27.28\342\200\257PM.png" create mode 100644 projects/vector-embeddings/build.js create mode 100644 projects/vector-embeddings/package.json create mode 100644 projects/vector-embeddings/polyfill.js create mode 100644 projects/vector-embeddings/src/appsscript.json create mode 100644 projects/vector-embeddings/src/examples.js create mode 100644 projects/vector-embeddings/src/index.ts create mode 100644 projects/vector-embeddings/src/internal.d.ts create mode 100644 projects/vector-embeddings/src/main.js create mode 100644 projects/vector-embeddings/src/tools.js create mode 100644 projects/vector-embeddings/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cf2a9d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } + } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2012862..40e911e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,18 @@ importers: specifier: ^0.25.0 version: 0.25.0 + projects/vector-embeddings: + devDependencies: + '@google/clasp': + specifier: 3.0.2-alpha + version: 3.0.2-alpha(@types/node@22.13.10) + '@types/google-apps-script': + specifier: ^1.0.97 + version: 1.0.97 + esbuild: + specifier: ^0.25.0 + version: 0.25.0 + packages: '@ai-sdk/anthropic@1.1.15': diff --git a/projects/vector-embeddings/.clasp.json b/projects/vector-embeddings/.clasp.json new file mode 100644 index 0000000..71293a4 --- /dev/null +++ b/projects/vector-embeddings/.clasp.json @@ -0,0 +1,16 @@ +{ + "scriptId": "1BaRz4XASe09owPsDwClqaX4XowP-M5TmGBvDp3jAVhhpxEgfyBnkNZoz", + "rootDir": "dist", + "projectId": "jpoehnelt-internal", + "scriptExtensions": [ + ".js", + ".gs" + ], + "htmlExtensions": [ + ".html" + ], + "jsonExtensions": [ + ".json" + ], + "filePushOrder": [] +} \ No newline at end of file diff --git a/projects/vector-embeddings/README.md b/projects/vector-embeddings/README.md new file mode 100644 index 0000000..8221379 --- /dev/null +++ b/projects/vector-embeddings/README.md @@ -0,0 +1,236 @@ +# Harnessing the Power of Vector Embeddings in Google Apps Script with Vertex AI + +## Introduction + +In today's data-driven world, the ability to understand and process text semantically has become increasingly important. Vector embeddings provide a powerful way to represent text as numerical vectors, enabling semantic search, content recommendation, and other advanced natural language processing capabilities. This blog post explores how to leverage Google's Vertex AI to generate vector embeddings directly within Google Apps Script. + +## What are Vector Embeddings? + +Vector embeddings are numerical representations of text (or other data) in a high-dimensional space. Unlike traditional keyword-based approaches, embeddings capture semantic meaning, allowing us to measure similarity between texts based on their actual meaning rather than just matching keywords. + +For example, the phrases "I love programming" and "Coding is my passion" would be recognized as similar in an embedding space, despite having no words in common. + +## Why Use Vertex AI with Apps Script? + +Google Apps Script provides a powerful platform for automating tasks within Google Workspace. By combining it with Vertex AI's embedding capabilities, you can: + +1. Build semantic search functionality in Google Sheets or Docs +2. Create content recommendation systems +3. Implement intelligent document classification +4. Enhance chatbots and virtual assistants +5. Perform sentiment analysis and topic modeling + +## Implementation Guide + +### Prerequisites + +1. A Google Cloud Platform account with Vertex AI API enabled +2. A Google Apps Script project + +### Step 1: Set Up Your Project + +First, you'll need to set up your Apps Script project and configure it to use the Vertex AI API. Make sure to store your project ID in the script properties. You can do this by going to the Script Editor, clicking on the "Script properties" icon, and adding your project ID. + +### Step 2: Generate Embeddings + +The core functionality is generating embeddings from text. Here's how to implement it: + +```javascript +/** + * Generate embeddings for the given text. + * @param {string|string[]} text - The text to generate embeddings for. + * @returns {number[][]} - The generated embeddings. + */ +function batchedEmbeddings_( + text, + { model = "text-embedding-005" } = {} +) { + if (!Array.isArray(text)) { + text = [text]; + } + + const token = ScriptApp.getOAuthToken(); + const PROJECT_ID = PropertiesService.getScriptProperties().getProperty("PROJECT_ID"); + const REGION = "us-central1"; + + const requests = text.map((content) => ({ + url: `https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/publishers/google/models/${model}:predict`, + method: "post", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + muteHttpExceptions: true, + contentType: "application/json", + payload: JSON.stringify({ + instances: [{ content }], + parameters: { + autoTruncate: true, + }, + }), + })); + + const responses = UrlFetchApp.fetchAll(requests); + const results = responses.map((response) => { + if (response.getResponseCode() !== 200) { + throw new Error(response.getContentText()); + } + return JSON.parse(response.getContentText()); + }); + + return results.map((result) => result.predictions[0].embeddings.values); +} +``` + +### Step 3: Calculate Similarity Between Embeddings + +Once you have embeddings, you'll want to compare them to find similar content. The cosine similarity is a common metric for this purpose. It measures the cosine of the angle between two vectors. The cosine of the angle is calculated as the dot product of the vectors divided by the product of their magnitudes. The cosine similarity takes values between -1 (completely dissimilar) and 1 (completely similar) and is defined as `cosine similarity = dot product / (magnitude of x * magnitude of y)`. + +```javascript +/** + * Calculates the cosine similarity between two vectors. + * @param {number[]} x - The first vector. + * @param {number[]} y - The second vector. + * @returns {number} The cosine similarity value between -1 and 1. + */ +function similarity_(x, y) { + return dotProduct_(x, y) / (magnitude_(x) * magnitude_(y)); +} + +function dotProduct_(x, y) { + let result = 0; + for (let i = 0, l = Math.min(x.length, y.length); i < l; i += 1) { + result += x[i] * y[i]; + } + return result; +} + +function magnitude_(x) { + let result = 0; + for (let i = 0, l = x.length; i < l; i += 1) { + result += x[i] ** 2; + } + return Math.sqrt(result); +} +``` + +I tested out the code with a small corpus of texts and it worked well. + +```sh +🔍 Searching for "Hello world!" ... +🔥 1.00000 - "Hello world!" +✅ 0.71132 - "Lorem ipsum dolor ..." +👍 0.60433 - "Hello Justin" +👍 0.53294 - "I love dogs 🐕" +🤔 0.45529 - "The forecast is ..." +🤔 0.45220 - "Foo bar" +🤔 0.41263 - "Apps Script is a ..." +``` + +You might be wondering why `Hello Justin` is ranked lower than `Lorem ipsum ...` when searching for `Hello world!`. While `Hello Justin` contains the word `Hello`, the meaning of `Hello world!` is more similar to `Lorem ipsum ...` as both are common phrases used in the context of software development. This will vary by the model used and how it was trained. + +The full matrix of similarities is: + + +### Step 4: Building a Simple Semantic Search + +Here's how to create a basic semantic search function: + +```javascript +function semanticSearch(query, corpus) { + // Generate embedding for the query + const queryEmbedding = batchedEmbeddings_([query])[0]; + + // Create or use existing index + const index = corpus.map((text) => ({ + text, + embedding: batchedEmbeddings_([text])[0], + })); + + // Calculate similarities + const results = index.map(({ text, embedding }) => ({ + text, + similarity: similarity_(embedding, queryEmbedding), + })); + + // Sort by similarity (highest first) + return results.sort((a, b) => b.similarity - a.similarity); +} +``` + +## Real-World Applications + +### Example 1: Semantic Search in Google Sheets + +You can build a custom function that allows users to search through data in a spreadsheet based on meaning rather than exact keyword matches: + +```javascript +/** + * Custom function for Google Sheets to perform semantic search + * @param {string} query The search query + * @param {Range} dataRange The range containing the text to search through + * @param {number} limit Optional limit on number of results + * @return {string[][]} The search results with similarity scores + * @customfunction + */ +function SEMANTIC_SEARCH(query, dataRange, limit = 5) { + const corpus = dataRange.getValues().flat().filter(Boolean); + const results = semanticSearch(query, corpus); + + return results + .slice(0, limit) + .map(({ text, similarity }) => [text, similarity]); +} +``` + +### Example 2: Document Classification + +You can use embeddings to automatically categorize documents: + +```javascript +/** + * Document classification using embeddings + * @param {string} document The document to classify + * @param {Array} categories List of possible categories + * @return {string} The most similar category + */ +function classifyDocument(document = "I love dogs", categories = ["Software", "Animal", "Food"]) { + const docEmbedding = batchedEmbeddings_([document])[0]; + const categoryEmbeddings = batchedEmbeddings_(categories); + + const similarities = categoryEmbeddings.map((catEmbedding, index) => ({ + category: categories[index], + similarity: similarity_(docEmbedding, catEmbedding) + })); + + // Return the most similar category + const results = similarities.sort((a, b) => b.similarity - a.similarity)[0].category; + console.log(results); + return results; +} +``` + +When this is run with the default parameters, it will return `Animal`. + +## Performance Considerations + +When working with vector embeddings in Apps Script, keep these tips in mind: + +1. **Batch Processing**: Generate embeddings in batches to reduce API calls. +2. **Caching**: Store embeddings in properties service or a spreadsheet to avoid regenerating them. +3. **Dimensionality**: Consider using a lower dimensionality for faster processing if accuracy is less critical. +4. **Quota Limits**: Be mindful of Apps Script's quotas for UrlFetchApp calls and execution time. + +> **Note**: You will likely want to cache embeddings for performance and cost savings. + +## Conclusion + +Vector embeddings represent a powerful tool for bringing advanced natural language processing capabilities to your Google Workspace environment. By combining Google Apps Script with Vertex AI, you can create intelligent applications that understand the semantic meaning of text, enabling more sophisticated search, recommendation, and classification systems. + +The code examples provided in this blog post should help you get started with implementing vector embeddings in your own Apps Script projects. As you explore this technology further, you'll discover even more creative ways to leverage the power of AI within your workflows. + +## Resources + +- [Google Cloud Vertex AI Documentation](https://cloud.google.com/vertex-ai/docs) +- [Apps Script Documentation](https://developers.google.com/apps-script) +- [Text Embeddings API Reference](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) diff --git "a/projects/vector-embeddings/Screenshot 2025-03-27 at 1.27.28\342\200\257PM.png" "b/projects/vector-embeddings/Screenshot 2025-03-27 at 1.27.28\342\200\257PM.png" new file mode 100644 index 0000000000000000000000000000000000000000..be9407725da10d93fe95467b617230df03e3ecf7 GIT binary patch literal 53552 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

C=7?`>`Ha^kM`IUve;JJO5nhWP5=b|S00u*L|u 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 BJ?{Vj literal 0 HcmV?d00001 diff --git a/projects/vector-embeddings/build.js b/projects/vector-embeddings/build.js new file mode 100644 index 0000000..a852867 --- /dev/null +++ b/projects/vector-embeddings/build.js @@ -0,0 +1,30 @@ +import fs from "fs"; +import esbuild from "esbuild"; +import path from "path"; + +const outdir = "dist"; +const sourceRoot = "src"; + +await esbuild.build({ + entryPoints: ["./src/index.ts"], + bundle: true, + outdir, + sourceRoot, + platform: "node", + format: "esm", + plugins: [], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, + entryNames: "zzz_bundle_[name]", + chunkNames: "zzz_chunk_[name]", + // See mocks in https://github.com/mjmlio/mjml/tree/master/packages/mjml-browser +}); + +const passThroughFiles = ["main.js", "examples.js", "tools.js", "appsscript.json"]; + +await Promise.all( + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)) + ) +); diff --git a/projects/vector-embeddings/package.json b/projects/vector-embeddings/package.json new file mode 100644 index 0000000..0d339fa --- /dev/null +++ b/projects/vector-embeddings/package.json @@ -0,0 +1,19 @@ +{ + "name": "@repository/vector-embeddings", + "version": "0.1.0", + "scripts": { + "build": "node build.js", + "check": "tsc --noEmit", + "push": "DEBUG=clasp:* clasp push -f" + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "3.0.2-alpha", + "@types/google-apps-script": "^1.0.97", + "esbuild": "^0.25.0" + }, + "type": "module", + "dependencies": {}, + "private": true +} diff --git a/projects/vector-embeddings/polyfill.js b/projects/vector-embeddings/polyfill.js new file mode 100644 index 0000000..0f6d813 --- /dev/null +++ b/projects/vector-embeddings/polyfill.js @@ -0,0 +1 @@ +globalThis.window = globalThis; \ No newline at end of file diff --git a/projects/vector-embeddings/src/appsscript.json b/projects/vector-embeddings/src/appsscript.json new file mode 100644 index 0000000..4737ce9 --- /dev/null +++ b/projects/vector-embeddings/src/appsscript.json @@ -0,0 +1,10 @@ +{ + "timeZone": "America/Denver", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/script.external_request" + ] +} diff --git a/projects/vector-embeddings/src/examples.js b/projects/vector-embeddings/src/examples.js new file mode 100644 index 0000000..dc24fae --- /dev/null +++ b/projects/vector-embeddings/src/examples.js @@ -0,0 +1,61 @@ +/** + * Basic semantic search function + * @param {string} query The search query + * @param {Array} corpus The corpus of text to search through + * @return {Array} The search results with similarity scores + */ +function semanticSearch(query, corpus) { + // Generate embedding for the query + const queryEmbedding = batchedEmbeddings_([query])[0]; + + // Create or use existing index + const index = corpus.map((text) => ({ + text, + embedding: batchedEmbeddings_([text])[0], + })); + + // Calculate similarities + const results = index.map(({ text, embedding }) => ({ + text, + similarity: similarity_(embedding, queryEmbedding), + })); + + // Sort by similarity (highest first) + return results.sort((a, b) => b.similarity - a.similarity); +} + +/** + * Custom function for Google Sheets to perform semantic search + * @param {string} query The search query + * @param {GoogleAppsScript.Spreadsheet.Range} dataRange The range containing the text to search through + * @param {number} limit Optional limit on number of results + * @return {Array>} The search results with similarity scores + * @customfunction + */ +function SEMANTIC_SEARCH(query, dataRange, limit = 5) { + const corpus = dataRange.getValues().flat().filter(Boolean); + const results = semanticSearch(query, corpus); + + return results + .slice(0, limit) + .map(({ text, similarity }) => [text, similarity]); +} + +/** + * Document classification using embeddings + * @param {string} document The document to classify + * @param {Array} categories List of possible categories + * @return {string} The most similar category + */ +function classifyDocument(document = "I love dogs", categories = ["Software", "Animal", "Food"]) { + const docEmbedding = batchedEmbeddings_([document])[0]; + const categoryEmbeddings = batchedEmbeddings_(categories); + + const similarities = categoryEmbeddings.map((catEmbedding, index) => ({ + category: categories[index], + similarity: similarity_(docEmbedding, catEmbedding) + })); + + // Return the most similar category + return similarities.sort((a, b) => b.similarity - a.similarity)[0].category; +} diff --git a/projects/vector-embeddings/src/index.ts b/projects/vector-embeddings/src/index.ts new file mode 100644 index 0000000..693da49 --- /dev/null +++ b/projects/vector-embeddings/src/index.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/projects/vector-embeddings/src/internal.d.ts b/projects/vector-embeddings/src/internal.d.ts new file mode 100644 index 0000000..38f53d6 --- /dev/null +++ b/projects/vector-embeddings/src/internal.d.ts @@ -0,0 +1,16 @@ +declare function batchedEmbeddings_( + text: string | string[], + { model }?: { model?: string } +): number[][]; + +declare function similarity_(x: number[], y: number[]): number; + +declare function truncate(text: string, maxLength: number): string; + +declare const similarityEmoji_: (value: number) => string; + +declare const dotProduct_: (x: number[], y: number[]) => number; + +declare const magnitude_: (x: number[]) => number; + + diff --git a/projects/vector-embeddings/src/main.js b/projects/vector-embeddings/src/main.js new file mode 100644 index 0000000..a8f3874 --- /dev/null +++ b/projects/vector-embeddings/src/main.js @@ -0,0 +1,133 @@ +const PROJECT_ID = + PropertiesService.getScriptProperties().getProperty("PROJECT_ID"); +const MODEL_ID = "text-embedding-005"; +const REGION = "us-central1"; + +/** + * Generate embeddings for the given text. + * @param {string|string[]} text - The text to generate embeddings for. + * @returns {number[][]} - The generated embeddings. + */ +function batchedEmbeddings_( + text, + { model = MODEL_ID, outputDimensionality = 768 } = {} +) { + if (!Array.isArray(text)) { + text = [text]; + } + + // Request body + // { + // "instances": [ + // { "content": "TEXT", + // "task_type": "TASK_TYPE", + // "title": "TITLE" + // }, + // ], + // "parameters": { + // "autoTruncate": AUTO_TRUNCATE, + // "outputDimensionality": OUTPUT_DIMENSIONALITY + // } + // } + + const token = ScriptApp.getOAuthToken(); + + // TODO chunk in instances of 5 + const requests = text.map((content) => ({ + url: `https://us-central1-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/publishers/google/models/${model}:predict`, + method: "post", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + muteHttpExceptions: true, + contentType: "application/json", + payload: JSON.stringify({ + instances: [{ content }], + parameters: { + autoTruncate: true, + outputDimensionality, + }, + }), + })); + + const responses = UrlFetchApp.fetchAll(requests); + + const results = responses.map((response) => { + if (response.getResponseCode() !== 200) { + throw new Error(response.getContentText()); + } + + return JSON.parse(response.getContentText()); + }); + + // Response body + // { + // "predictions": [ + // { + // "embeddings": { + // "statistics": { + // "truncated": boolean, + // "token_count": integer + // }, + // "values": [ number ] + // } + // } + // ] + // } + + return results.map((result) => result.predictions[0].embeddings.values); +} + + +function main() { + const corpus = [ + "Hello world!", + "Hello Justin", + "Apps Script is a platform for building custom business apps", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", + "Foo bar", + "I love dogs 🐕", + "The forecast is going to be sunny with a 100% chance of sunshine", + ]; + const index = corpus.map((text) => ({ + text, + embedding: batchedEmbeddings_([text])[0], + })); + + const search = corpus[0]; + const searchEmbedding = batchedEmbeddings_([search])[0]; + + const results = index.map(({ text, embedding }) => ({ + text, + similarity: similarity_(embedding, searchEmbedding), + })); + + const sortedResults = results.sort((a, b) => b.similarity - a.similarity); + + Logger.log(`🔍 searching for "${truncate_(search, 100)}" ...`); + for (const { text, similarity } of sortedResults) { + const truncated = truncate_(text, 100); + Logger.log(`${similarityEmoji_(similarity)} ${similarity.toFixed(5)} - "${truncated}"`); + } + + // Generate matrix of similarities + const matrix = index.map(({ embedding }) => + index.map(({ embedding: other }) => similarity_(embedding, other)) + ); + + const headers = ["", ...corpus].map((text) => truncate_(text, 12).padEnd(15)); + const rows = matrix.map((row, i) => { + return [ + truncate_(corpus[i], 12).padEnd(15), + ...row.map((value) => { + const emoji = similarityEmoji_(value); + return `${emoji} ${String(value.toFixed(2)).padEnd(9)}`; + }), + ].join("\t"); + }); + + // Output matrix using ascii table + Logger.log([headers.join("\t"), ...rows].join("\n")); +} + diff --git a/projects/vector-embeddings/src/tools.js b/projects/vector-embeddings/src/tools.js new file mode 100644 index 0000000..590db43 --- /dev/null +++ b/projects/vector-embeddings/src/tools.js @@ -0,0 +1,52 @@ +/** + * Calculates the dot product of two vectors. + * @param {number[]} x - The first vector. + * @param {number[]} y - The second vector. + * @returns {number} The dot product of the two vectors. + */ +function dotProduct_(x, y) { + let result = 0; + for (let i = 0, l = Math.min(x.length, y.length); i < l; i += 1) { + result += x[i] * y[i]; + } + return result; +} + +/** + * Calculates the magnitude of a vector. + * @param {number[]} x - The vector. + * @returns {number} The magnitude of the vector. + */ +function magnitude_(x) { + let result = 0; + for (let i = 0, l = x.length; i < l; i += 1) { + result += x[i] ** 2; + } + return Math.sqrt(result); +} + +/** + * Calculates the cosine similarity between two vectors. + * @param {number[]} x - The first vector. + * @param {number[]} y - The second vector. + * @returns {number} The cosine similarity value between -1 and 1. + */ +function similarity_(x, y) { + return dotProduct_(x, y) / (magnitude_(x) * magnitude_(y)); +} + +function truncate_(text, maxLength) { + return text.slice(0, maxLength) + (text.length > maxLength ? "..." : ""); +} + +const similarityEmoji_ = (value) => { + if (value >= 0.9) + return "🔥"; // Very high similarity + else if (value >= 0.7) + return "✅"; // High similarity + else if (value >= 0.5) + return "👍"; // Medium similarity + else if (value >= 0.3) + return "🤔"; // Low similarity + else return "❌"; // Very low similarity +}; diff --git a/projects/vector-embeddings/tsconfig.json b/projects/vector-embeddings/tsconfig.json new file mode 100644 index 0000000..848a1f3 --- /dev/null +++ b/projects/vector-embeddings/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ES2022", + "lib": [ + "esnext" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [ + "@types/google-apps-script" + ], + "experimentalDecorators": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file