diff --git a/vertex-ai-firebase-angular-example/README.md b/vertex-ai-firebase-angular-example/README.md index f77e3ec..95d8b2b 100644 --- a/vertex-ai-firebase-angular-example/README.md +++ b/vertex-ai-firebase-angular-example/README.md @@ -7,9 +7,19 @@ Here's an example of the running application: ## How to get started -1. Create and configure a project in Firebase. Follow the directions for [Step 1](https://firebase.google.com/docs/vertex-ai/get-started?platform=web) to create an project and a web app. Do not follow the instructions for adding the sdks, that has already been done for this repository. +1. Create and configure a project in Firebase. Follow the directions for [Step 1](https://firebase.google.com/docs/vertex-ai/get-started?platform=web) to create a project and a web app. Do not follow the instructions for adding the sdks, that has already been done for this repository. 1. Clone this repository or download the code to your local machine 1. `cd` into the root folder (e.g., `cd vertex-ai-firebase-angular`) +1. Take your project settings from Firebase Console and add them to `src/environments.ts` + ``` + export const environment = { + production: true, + firebase: { + /* project settings */ + }, + }; + ``` + 1. Install the dependencies with `npm install` 1. Update the Firebase project settings in `environment.ts`. 1. Run this example with `ng serve` \ No newline at end of file diff --git a/vertex-ai-firebase-angular-example/example-screenshot.png b/vertex-ai-firebase-angular-example/example-screenshot.png index 7e6c20c..8719ca3 100644 Binary files a/vertex-ai-firebase-angular-example/example-screenshot.png and b/vertex-ai-firebase-angular-example/example-screenshot.png differ diff --git a/vertex-ai-firebase-angular-example/public/products/apples.jpg b/vertex-ai-firebase-angular-example/public/products/apples.jpg new file mode 100644 index 0000000..61c1aac Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/apples.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/bananas.jpg b/vertex-ai-firebase-angular-example/public/products/bananas.jpg new file mode 100644 index 0000000..fe588d0 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/bananas.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/bread.jpg b/vertex-ai-firebase-angular-example/public/products/bread.jpg new file mode 100644 index 0000000..e6beded Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/bread.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/cheese.jpg b/vertex-ai-firebase-angular-example/public/products/cheese.jpg new file mode 100644 index 0000000..492818c Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/cheese.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/chicken.jpg b/vertex-ai-firebase-angular-example/public/products/chicken.jpg new file mode 100644 index 0000000..48f5532 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/chicken.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/eggs.jpg b/vertex-ai-firebase-angular-example/public/products/eggs.jpg new file mode 100644 index 0000000..c92086e Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/eggs.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/milk.jpg b/vertex-ai-firebase-angular-example/public/products/milk.jpg new file mode 100644 index 0000000..3303648 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/milk.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/oranges.jpg b/vertex-ai-firebase-angular-example/public/products/oranges.jpg new file mode 100644 index 0000000..a6ea967 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/oranges.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/rice.jpg b/vertex-ai-firebase-angular-example/public/products/rice.jpg new file mode 100644 index 0000000..97077a4 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/rice.jpg differ diff --git a/vertex-ai-firebase-angular-example/public/products/yogurt.jpg b/vertex-ai-firebase-angular-example/public/products/yogurt.jpg new file mode 100644 index 0000000..cf017f4 Binary files /dev/null and b/vertex-ai-firebase-angular-example/public/products/yogurt.jpg differ diff --git a/vertex-ai-firebase-angular-example/src/app/ai.service.ts b/vertex-ai-firebase-angular-example/src/app/ai.service.ts index 0ad1de1..fa82f19 100644 --- a/vertex-ai-firebase-angular-example/src/app/ai.service.ts +++ b/vertex-ai-firebase-angular-example/src/app/ai.service.ts @@ -5,28 +5,147 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import { Injectable, Inject } from '@angular/core'; +import { Injectable, Inject, inject } from "@angular/core"; import { FirebaseApp } from "@angular/fire/app"; -import { getVertexAI, getGenerativeModel, GenerativeModel } from "firebase/vertexai"; +import { + getVertexAI, + getGenerativeModel, + GenerativeModel, + ChatSession, + FunctionDeclarationsTool, + ObjectSchemaInterface, + Schema, +} from "@angular/fire/vertexai"; +import { ProductService } from "./product.service"; +import { Product } from "./product"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class AiService { - readonly model: GenerativeModel; + private readonly model: GenerativeModel; + private readonly products: ProductService = inject(ProductService); + private readonly chat: ChatSession; + + constructor(@Inject("FIREBASE_APP") private firebaseApp: FirebaseApp) { + const productsToolSet: FunctionDeclarationsTool = { + functionDeclarations: [ + { + name: "getNumberOfProducts", + description: + "Get a count of the number of products available in the inventory.", + }, + { + name: "getProducts", + description: + "Get an array of the products with the name and price of each product.", + }, + { + name: "addToCart", + description: "Add one or more products to the cart.", + parameters: Schema.object({ + properties: { + productsToAdd: Schema.array({ + items: Schema.object({ + description: "A single product with its name and price.", + properties: { + name: Schema.string({ + description: "The name of the product.", + }), + price: Schema.number({ + description: "The numerical price of the product.", + }), + }, + // Specify which properties within each product object are required + required: ["name", "price"], + }), + }), + }, + }) as ObjectSchemaInterface, + }, + ], + }; - constructor(@Inject('FIREBASE_APP') private firebaseApp: FirebaseApp) { // Initialize the Vertex AI service const vertexAI = getVertexAI(this.firebaseApp); + const systemInstruction = + "Welcome to ng-produce. You are a superstar agent for this ecommerce store. you will assist users by answering questions about the inventory and event being able to add items to the cart."; // Initialize the generative model with a model that supports your use case - this.model = getGenerativeModel(vertexAI, { model: "gemini-2.0-flash" }); + this.model = getGenerativeModel(vertexAI, { + model: "gemini-2.0-flash", + systemInstruction: systemInstruction, + tools: [productsToolSet], + }); + + this.chat = this.model.startChat(); } async ask(prompt: string) { - const result = await this.model.generateContent(prompt); + let result = await this.chat.sendMessage(prompt); + const functionCalls = result.response.functionCalls(); + + if (functionCalls && functionCalls.length > 0) { + for (const functionCall of functionCalls) { + switch (functionCall.name) { + case "getNumberOfProducts": { + const functionResult = this.getNumberOfProducts(); + result = await this.chat.sendMessage([ + { + functionResponse: { + name: functionCall.name, + response: { numberOfItems: functionResult }, + }, + }, + ]); + break; + } + case "getProducts": { + const functionResult = this.getProducts(); + result = await this.chat.sendMessage([ + { + functionResponse: { + name: functionCall.name, + response: { products: functionResult }, + }, + }, + ]); + break; + } + case "addToCart": { + console.log(functionCall.args); + + const args = functionCall.args as { productsToAdd: Product[]} + + const functionResult = this.addToCart(args.productsToAdd); + + result = await this.chat.sendMessage([ + { + functionResponse: { + name: functionCall.name, + response: { numberOfProductsAdded: functionResult }, + }, + } + ]); + break; + } + } + } + } + + return result.response.text(); + } + + getProducts() { + return this.products.getProducts(); + } + getNumberOfProducts() { + return this.getProducts().length; + } - const response = result.response; - return response.text(); + addToCart(productsToAdd: Product[]) { + for (let i = 0; i < productsToAdd.length; i++) { + this.products.addToCart(productsToAdd[i]); + } } } diff --git a/vertex-ai-firebase-angular-example/src/app/app.component.css b/vertex-ai-firebase-angular-example/src/app/app.component.css new file mode 100644 index 0000000..84fbff8 --- /dev/null +++ b/vertex-ai-firebase-angular-example/src/app/app.component.css @@ -0,0 +1,155 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--primary); + color: white; +} + +.page-header h1{ + font-weight: bolder; + color: inherit; +} + +section { + padding: 20px; + font-family: sans-serif; +} + +h1 { + color: #333; +} + +ul { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat( + auto-fill, + minmax(200px, 1fr) + ); + gap: 16px; +} +.product-listing { + padding-top: 0px; + padding-left: 0px; +} + +.product-card { + border: 1px solid #ddd; + padding: 16px; + border-radius: 8px; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease-in-out; + text-align: center; +} +.product-card .product-info { + text-align: start; +} + +.product-card .product-price { + font-weight: bolder; + font-size: 20px; +} +.product-card .product-name { + margin-bottom: 10px; +} + +.product-card h3 { + margin-top: 0; + margin-bottom: 8px; + font-size: 1.1em; + color: #0056b3; +} + +.product-card p { + margin: 0; + color: #555; + font-size: 1em; +} + +.product-card .add-to-cart-btn { + border-radius: 5px; + background-color: var(--primary); + color: white; + display: block; + width: 100%; +} + +.product-card .add-to-cart-btn:hover { + cursor: pointer; + transform: translateY(-5px); +} + +.container { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 20px; + padding: 20px; + background: var(--secondary); +} + +.agent-window { + border: 1px solid #ccc; + border-radius: 5px; + height: 500px; + display: flex; + flex-direction: column; + background: white; +} + +.agent-window .user-question-label { + margin-bottom: 10px; +} + +.chat-history { + flex-grow: 1; + overflow-y: auto; + padding: 10px; +} + +.chat-input { + padding: 10px; + border: 1px solid #ccc; + width: 60%; +} + +.submit-btn { + border-radius: 5px; + background-color: var(--primary); + color: white; +} + +.chat-message { + margin-bottom: 10px; + padding: 8px; + border-radius: 5px; +} + +.user-message { + background-color: #e0f0ff; + text-align: right; + } + +.chat-message, .user-message, .agent-message { + border-radius: 5px; + padding: 15px; + margin-bottom: 10px; +} + +.agent-message { + background-color: #f0f0f0; + text-align: left; +} + +.control-section { + display: flex; +} diff --git a/vertex-ai-firebase-angular-example/src/app/app.component.ts b/vertex-ai-firebase-angular-example/src/app/app.component.ts index 0c38f95..185e9e4 100644 --- a/vertex-ai-firebase-angular-example/src/app/app.component.ts +++ b/vertex-ai-firebase-angular-example/src/app/app.component.ts @@ -5,38 +5,108 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import { Component, inject, signal } from '@angular/core'; -import { AiService } from './ai.service'; +import { Component, effect, ElementRef, inject, signal, viewChild } from "@angular/core"; +import { Product } from "./product"; +import { ProductService } from "./product.service"; +import { CurrencyPipe, NgOptimizedImage } from "@angular/common"; +import { Message } from "./message"; +import { AiService } from "./ai.service"; @Component({ - selector: 'app-root', + selector: "app-root", + standalone: true, + imports: [CurrencyPipe, NgOptimizedImage], template: ` -
-

Vertex AI in Firebase Angular Example

-

Here's a story created using Vertex AI in Firebase

-

- @if (story() === '') { - Loading... - } - @else { - {{story()}} - } +

+
+
+
    + @for(product of productList(); track product.name){ +
  • + {{ product.name }} +
    +

    {{ product.price | currency }}

    +

    {{ product.name }}

    + +
    +
  • + } +
+
+
+
+ @for(message of messageHistory(); track message){ +

{{ message.text }}

+ } +
+
+ + +
+
`, - styles: ` - .title, .content { - border: solid 1px black; - border-radius: 5px; - padding: 20px; - } - `, + styleUrl: "./app.component.css", }) export class AppComponent { - private readonly aiService = inject(AiService); - readonly story = signal(''); + private readonly ai = inject(AiService); + readonly products = inject(ProductService); + readonly messageHistory = signal([]); + readonly productList = signal([]); + + private readonly chatHistoryContainer = viewChild("chatHistoryContainer"); + + private readonly scrollEffect = effect(() => { + if (this.messageHistory().length > 0) { + const container = this.chatHistoryContainer(); + if (container) { + container.nativeElement.scrollTo(0, container.nativeElement.scrollHeight + 600); + } + } + }); + constructor() { - this.aiService.ask('Tell me a story about a magic backpack').then(text => this.story.set(text)); + this.productList.set(this.products.getProducts()); + } + + addToCart(product: Product) { + this.products.addToCart(product); + } + + async submitMessage(msg: HTMLInputElement) { + const question = msg.value; + + if (!question) return; + + msg.value = ""; + + this.messageHistory.update((history) => [ + ...history, + { sender: "user", text: question }, + ]); + + const response = await this.ai.ask(question); + this.messageHistory.update((history) => [ + ...history, + { sender: "agent", text: response }, + ]); } } diff --git a/vertex-ai-firebase-angular-example/src/app/message.ts b/vertex-ai-firebase-angular-example/src/app/message.ts new file mode 100644 index 0000000..825367d --- /dev/null +++ b/vertex-ai-firebase-angular-example/src/app/message.ts @@ -0,0 +1,11 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface Message { + sender: 'user' | 'agent'; + text: string; +} diff --git a/vertex-ai-firebase-angular-example/src/app/product.service.ts b/vertex-ai-firebase-angular-example/src/app/product.service.ts new file mode 100644 index 0000000..2bbf2fe --- /dev/null +++ b/vertex-ai-firebase-angular-example/src/app/product.service.ts @@ -0,0 +1,47 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { computed, Injectable, signal } from "@angular/core"; +import { Product } from "./product"; + +@Injectable({ + providedIn: "root" +}) +export class ProductService { + readonly productCart = signal([]); + + readonly productCartTotal = computed(() => { + return this.productCart().reduce((total, product) => { + return total + product.price; + }, 0); + }); + + private readonly products: Product[] = [ + { name: "Apple", price: 0.99, image: "products/apples.jpg" }, + { name: "Banana", price: 0.59, image: "products/bananas.jpg" }, + { name: "Orange", price: 0.79, image: "products/oranges.jpg" }, + { name: "Milk", price: 3.99, image: "products/milk.jpg" }, + { name: "Bread", price: 2.49, image: "products/bread.jpg" }, + { name: "Eggs", price: 4.99, image: "products/eggs.jpg" }, + { name: "Cheese", price: 5.99, image: "products/cheese.jpg" }, + { name: "Yogurt", price: 1.99, image: "products/yogurt.jpg" }, + { name: "Chicken", price: 7.99, image: "products/chicken.jpg" }, + { name: "Rice", price: 2.99, image: "products/rice.jpg" }, + ]; + + getProducts(): Product[] { + return this.products; + } + + getCart(): Product[] { + return this.productCart(); + } + + addToCart(product: Product) { + this.productCart.update((cart) => [...cart, product]); + } +} diff --git a/vertex-ai-firebase-angular-example/src/app/product.ts b/vertex-ai-firebase-angular-example/src/app/product.ts new file mode 100644 index 0000000..9f2de54 --- /dev/null +++ b/vertex-ai-firebase-angular-example/src/app/product.ts @@ -0,0 +1,12 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface Product { + name: string; + price: number; + image: string; +} \ No newline at end of file diff --git a/vertex-ai-firebase-angular-example/src/styles.css b/vertex-ai-firebase-angular-example/src/styles.css index f0ecbe3..9b281f2 100644 --- a/vertex-ai-firebase-angular-example/src/styles.css +++ b/vertex-ai-firebase-angular-example/src/styles.css @@ -5,9 +5,25 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ +body, h1, h2, h3, p, ul, li { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +ul { + list-style: none; +} -/* You can add global styles to this file, and also import other style files */ * { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 10px; -} \ No newline at end of file +} + +:root { + --primary: #eb8b3b; + --secondary: #f9f5f0; +}