Repository: jpoehnelt/apps-script
Pull request #2: feat: vector embedding demo
Author: login: jpoehnelt
Created: 2025-03-27T19:33:00Z | Merged: 2025-03-27T19:33:45Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced robust semantic search and document classification capabilities that enhance text analysis in spreadsheet and document workflows.
- **Documentation**
  - Added a comprehensive guide detailing how to leverage vector embedding techniques for semantic text processing.
- **Chores**
  - Improved development tooling and environment configurations for streamlined build, formatting, and deployment.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
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<string>} 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 0000000..be94077
Binary files /dev/null and "b/projects/vector-embeddings/Screenshot 2025-03-27 at 1.27.28\342\200\257PM.png" differ
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 <justin.poehnelt@gmail.com>",
+  "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<string>} corpus The corpus of text to search through
+ * @return {Array<Object>} 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<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<string>} 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


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @jpoehnelt, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a new project, `vector-embeddings`, which demonstrates how to use Google's Vertex AI to generate vector embeddings within Google Apps Script. The project includes code for generating embeddings, calculating similarity between embeddings, and building a simple semantic search function. It also provides examples of how to use vector embeddings in Google Sheets and for document classification. The changes include adding necessary dependencies, build scripts, and configuration files for the new project.

### Highlights
* **New Project: vector-embeddings**: Introduces a new project demonstrating vector embeddings with Google Apps Script and Vertex AI.
* **Embedding Generation**: Implements functions to generate vector embeddings from text using Vertex AI.
* **Similarity Calculation**: Includes functions to calculate the cosine similarity between two embeddings.
* **Semantic Search**: Provides a basic semantic search function that leverages vector embeddings.
* **Google Sheets Integration**: Demonstrates how to use vector embeddings in Google Sheets with a custom function.
* **Document Classification**: Shows how to use embeddings to automatically categorize documents.
* **Build Process**: Adds a build script using esbuild to bundle the Apps Script project.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **.vscode/settings.json**
  * Configures the default formatter to be `esbenp.prettier-vscode` for javascript files.
* **pnpm-lock.yaml**
  * Adds dependencies for the `vector-embeddings` project, including `@google/clasp`, `@types/google-apps-script`, and `esbuild`.
* **projects/vector-embeddings/.clasp.json**
  * Adds a clasp configuration file for the `vector-embeddings` project, specifying the script ID, root directory, project ID, and file extensions.
* **projects/vector-embeddings/README.md**
  * Creates a comprehensive README file explaining how to use vector embeddings in Google Apps Script with Vertex AI, including code examples and real-world applications.
* **projects/vector-embeddings/build.js**
  * Adds a build script using esbuild to bundle the Apps Script project, including minification and banner injection.
* **projects/vector-embeddings/package.json**
  * Creates a package.json file for the `vector-embeddings` project, specifying dependencies, scripts, and other metadata.
* **projects/vector-embeddings/polyfill.js**
  * Adds a polyfill to define `globalThis.window` for compatibility with certain libraries.
* **projects/vector-embeddings/src/appsscript.json**
  * Adds an appsscript.json file specifying the project's time zone, dependencies, exception logging, runtime version, and OAuth scopes.
* **projects/vector-embeddings/src/examples.js**
  * Adds example functions for semantic search, Google Sheets integration, and document classification.
* **projects/vector-embeddings/src/index.ts**
  * Creates an empty index.ts file, likely serving as an entry point for the TypeScript build process.
* **projects/vector-embeddings/src/internal.d.ts**
  * Adds a declaration file for internal functions used in the project, such as `batchedEmbeddings_`, `similarity_`, and others.
* **projects/vector-embeddings/src/main.js**
  * Implements the core logic for generating embeddings and performing semantic search using Vertex AI.
* **projects/vector-embeddings/src/tools.js**
  * Adds utility functions for calculating dot products, magnitudes, and cosine similarity.
* **projects/vector-embeddings/tsconfig.json**
  * Creates a tsconfig.json file to configure the TypeScript compiler options for the project.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

**Did you know?**

The concept of word embeddings, which vector embeddings are based on, gained prominence with the introduction of word2vec in 2013 by a team at Google, led by Tomas Mikolov.

state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T19:33:12Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces vector embedding functionality to Google Apps Script using Vertex AI. The code is well-structured and includes a comprehensive README with implementation details and examples. However, there are a few areas that could be improved for clarity and efficiency.

### Summary of Findings
* **Missing Error Handling in `semanticSearch`**: The `semanticSearch` function in `examples.js` does not handle potential errors during the embedding generation, which could lead to unexpected behavior.
* **Potential Performance Bottleneck in Similarity Calculations**: The similarity calculations are performed without caching, which could lead to performance issues when dealing with large datasets.
* **Inconsistent Variable Naming**: There are inconsistencies in variable naming conventions across different files (e.g., `PROJECT_ID` vs. `ProjectId`).

### Merge Readiness
The pull request introduces valuable functionality and is generally well-structured. However, addressing the error handling in `semanticSearch` and considering caching for similarity calculations would significantly improve the robustness and performance of the code. I am unable to approve this pull request, and recommend that others review and approve this code before merging. At a minimum, the high severity issues should be addressed before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-27T19:33:25Z

Review comments:
diff_hunk: @@ -0,0 +1,61 @@
+/**
+ * Basic semantic search function
+ * @param {string} query The search query
+ * @param {Array<string>} corpus The corpus of text to search through
+ * @return {Array<Object>} 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);
path: projects/vector-embeddings/src/examples.js
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `semanticSearch` function lacks error handling for the `batchedEmbeddings_` calls. If the embedding generation fails, the function will throw an error. Consider adding a try-catch block to handle potential errors and provide more informative feedback to the user.  How would you handle this?
created_at: 2025-03-27T19:33:25Z
author_association: NONE
diff_hunk: @@ -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
path: projects/vector-embeddings/src/main.js
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

This TODO comment indicates an area for potential optimization. Consider implementing chunking to avoid exceeding API limits and improve performance when processing large amounts of text.  How would you implement this chunking?
created_at: 2025-03-27T19:33:25Z
author_association: NONE
diff_hunk: @@ -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 ? "..." : "");
+}
path: projects/vector-embeddings/src/tools.js
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a check for `null` or `undefined` text values to prevent errors when `truncate_` is called with invalid input.  How would you implement this?
created_at: 2025-03-27T19:33:25Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-27T19:33:09Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces several new configuration files, build scripts, documentation, and source code modules to support a Google Apps Script project centered on vector embeddings. It adds settings for VSCode formatting, project configuration for clasp, TypeScript, and Apps Script runtime, and a comprehensive README guide that details semantic search and text similarity functions. Additionally, new source files implement core vector operations, embedding generation via an API call, and compatibility fixes with a polyfill.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `.vscode/settings.json` | Added VSCode settings to set Prettier as the default formatter. |
| `projects/vector-embeddings/.clasp.json`, `…/package.json`, `…/src/appsscript.json`, `…/tsconfig.json` | Introduced configuration files for Google Apps Script, project metadata, OAuth scopes, and TypeScript compiler options. |
| `projects/vector-embeddings/README.md` | Added comprehensive documentation on vector embeddings, semantic search, and example functions. |
| `projects/vector-embeddings/build.js` | Added an esbuild-based bundling script to compile TypeScript and copy necessary files. |
| `projects/vector-embeddings/src/index.ts`, `…/src/internal.d.ts`, `…/src/main.js`, `…/src/examples.js`, `…/src/tools.js` | Introduced new source files defining vector embedding functions, semantic search logic, similarity calculations, type declarations, and utility vector operations. |
| `projects/vector-embeddings/polyfill.js` | Added assignment for `globalThis.window` to ensure compatibility across different environments. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant SS as SEMANTIC_SEARCH
    participant BE as batchedEmbeddings_
    participant GC as Google Cloud Vertex AI
    participant VT as Vector Tools
    
    U->>SS: Initiate semantic search with query & corpus
    SS->>BE: Request text embeddings
    BE->>GC: Call AI API for embeddings
    GC-->>BE: Return embeddings
    BE-->>SS: Deliver embeddings
    SS->>VT: Compute similarity scores
    VT-->>SS: Return similarity data
    SS->>U: Return sorted results
```

## Poem
> Oh, I’m a bunny, hopping with delight,  
> New code sprouts like carrots in the light.  
> Embeddings bloom in fields of text so grand,  
> Semantic searches at my command.  
> I nibble bugs and polish every byte,  
> Hoppity cheers for code shining bright!  
> 🐰✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd2e691865a3d4a6a40c86f2367639f2ded958e6 and df60a449386ac49d352ba800fe69b74aecd4e31d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`
* `projects/vector-embeddings/Screenshot 2025-03-27 at 1.27.28 PM.png` is excluded by `!**/*.png`

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `.vscode/settings.json` (1 hunks)
* `projects/vector-embeddings/.clasp.json` (1 hunks)
* `projects/vector-embeddings/README.md` (1 hunks)
* `projects/vector-embeddings/build.js` (1 hunks)
* `projects/vector-embeddings/package.json` (1 hunks)
* `projects/vector-embeddings/polyfill.js` (1 hunks)
* `projects/vector-embeddings/src/appsscript.json` (1 hunks)
* `projects/vector-embeddings/src/examples.js` (1 hunks)
* `projects/vector-embeddings/src/index.ts` (1 hunks)
* `projects/vector-embeddings/src/internal.d.ts` (1 hunks)
* `projects/vector-embeddings/src/main.js` (1 hunks)
* `projects/vector-embeddings/src/tools.js` (1 hunks)
* `projects/vector-embeddings/tsconfig.json` (1 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=jpoehnelt/apps-script&utm_content=2):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-27T19:33:36Z
body: ## Deploying apps-script with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>df60a44</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://3f0ed13c.apps-script.pages.dev'>https://3f0ed13c.apps-script.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://feat-embeddings.apps-script.pages.dev'>https://feat-embeddings.apps-script.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/9cfedefc6185f3dad8ab91241b401135/pages/view/apps-script/3f0ed13c-b65d-4a2e-a255-93922cc28ab6)

author_association: NONE