diff --git a/samples/README.md b/samples/README.md index 333412ade..c106a1468 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,4 +1,12 @@ # Genkit samples -These samples have migrated to the Firebase -[`quickstart-nodejs` repo](https://github.com/firebase/quickstart-nodejs). +Take a look at some samples of Genkit in use: + +- [js-coffee-shop](js-coffee-shop/): "AI barista", demonstrating simple + LLM usage +- [js-menu](js-menu/): Progressively more sophisticated versions of a + menu understanding app +- [chatbot](chatbot/): A simple chatbot with a JavaScript frontend +- [js-angular](js-angular/): Demo of streaming to an Angular frontend +- [js-schoolAgent](js-schoolAgent/): A simple school assistant system with a routing agent and specialized agents +- [prompts](prompts/): Shows off several prompting techniques diff --git a/samples/chatbot/.gitignore b/samples/chatbot/.gitignore new file mode 100644 index 000000000..7951405f8 --- /dev/null +++ b/samples/chatbot/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/samples/chatbot/README.md b/samples/chatbot/README.md new file mode 100644 index 000000000..a2f1728bf --- /dev/null +++ b/samples/chatbot/README.md @@ -0,0 +1,35 @@ +# Chatbot + +This is a simple chatbot. You can pick which model to use. + +Prerequisite + +- install Genkit (`npm i -g genkit`) +- Google Cloud project with Vertex AI API enabled (https://pantheon.corp.google.com/apis/library/aiplatform.googleapis.com) +- gcloud CLI installed (https://cloud.google.com/sdk/docs/install-sdk) +- to use Llama 3.1 405b enable it in the Vertex AI [Model Garden](https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama3-405b-instruct-maas) + +The sample is using Vertex AI, so you'll need to auth: + +```bash +gcloud auth login +gcloud auth application-default login --project YOUR_PROJECT +``` + +Clone this code + +``` +git clone https://github.com/firebase/genkit +cd genkit/samples/chatbot +``` + +Install deps and run the chatbot app + +```bash +npm run setup +npm start +``` + +Point your browser to http://localhost:4200/ + +Inspect runs in http://localhost:4000/ diff --git a/samples/chatbot/eval.json b/samples/chatbot/eval.json new file mode 100644 index 000000000..ca1c84cba --- /dev/null +++ b/samples/chatbot/eval.json @@ -0,0 +1,10 @@ +[ + { + "conversationId": "1234", + "prompt": "tell me a joke" + }, + { + "conversationId": "2345", + "prompt": "wtite a python program that prints out weather for the current location" + } +] diff --git a/samples/chatbot/genkit-app/.editorconfig b/samples/chatbot/genkit-app/.editorconfig new file mode 100644 index 000000000..59d9a3a3e --- /dev/null +++ b/samples/chatbot/genkit-app/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/samples/chatbot/genkit-app/.gitignore b/samples/chatbot/genkit-app/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/samples/chatbot/genkit-app/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/samples/chatbot/genkit-app/README.md b/samples/chatbot/genkit-app/README.md new file mode 100644 index 000000000..0aeb2095b --- /dev/null +++ b/samples/chatbot/genkit-app/README.md @@ -0,0 +1,27 @@ +# GenkitApp + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/samples/chatbot/genkit-app/angular.json b/samples/chatbot/genkit-app/angular.json new file mode 100644 index 000000000..07bb6ad5f --- /dev/null +++ b/samples/chatbot/genkit-app/angular.json @@ -0,0 +1,105 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "genkit-app": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/genkit-app", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css" + ], + "scripts": [ + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "genkit-app:build:production" + }, + "development": { + "buildTarget": "genkit-app:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/samples/chatbot/genkit-app/package.json b/samples/chatbot/genkit-app/package.json new file mode 100644 index 000000000..465379b0b --- /dev/null +++ b/samples/chatbot/genkit-app/package.json @@ -0,0 +1,44 @@ +{ + "name": "genkit-app", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.0.0", + "@angular/cdk": "^18.0.1", + "@angular/common": "^18.0.0", + "@angular/compiler": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/material": "^18.0.1", + "@angular/platform-browser": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", + "@angular/router": "^18.0.0", + "marked": "^12.0.2", + "ngx-markdown": "^18.0.0", + "prismjs": "^1.29.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.3", + "genkit": "^0.9.0-rc || ^0.9" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.0.2", + "@angular/cli": "^18.0.2", + "@angular/compiler-cli": "^18.0.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.4.2" + } +} diff --git a/samples/chatbot/genkit-app/public/favicon.ico b/samples/chatbot/genkit-app/public/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/samples/chatbot/genkit-app/public/favicon.ico differ diff --git a/samples/chatbot/genkit-app/src/app/app.component.html b/samples/chatbot/genkit-app/src/app/app.component.html new file mode 100644 index 000000000..78c45a375 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.component.html @@ -0,0 +1,36 @@ + + +
+ + Chat with an LLM + + + +
+ + description + +
+ +
+ +
+
diff --git a/samples/chatbot/genkit-app/src/app/app.component.scss b/samples/chatbot/genkit-app/src/app/app.component.scss new file mode 100644 index 000000000..0e0b80557 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +app-bar { + border-bottom: 1px solid var(--divider-color); + grid-area: header; +} + +article { + grid-area: content; +} + +.wrapper { + display: grid; + grid-template: + 'header' auto + 'content' 1fr + / 1fr; + height: 100vh; +} + +.home-link { + align-items: center; + color: var(--mat-app-color); + display: flex; + gap: 8px; + + img { + height: 22px; + padding-left: 4px; + } +} + +.mat-toolbar { + background: #d7e3ff; + color: #005cbb; + gap: 4px; +} + +nav { + --mdc-secondary-navigation-tab-container-height: 64px; + --mat-tab-header-divider-height: 0; + margin-left: 32px; +} + +.preview-badge { + margin-left: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } +} diff --git a/samples/chatbot/genkit-app/src/app/app.component.spec.ts b/samples/chatbot/genkit-app/src/app/app.component.spec.ts new file mode 100644 index 000000000..02dac7e71 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.component.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'genkit-app' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('genkit-app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain( + 'Hello, genkit-app' + ); + }); +}); diff --git a/samples/chatbot/genkit-app/src/app/app.component.ts b/samples/chatbot/genkit-app/src/app/app.component.ts new file mode 100644 index 000000000..39d4f4ea0 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.component.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTabNavPanel, MatTabsModule } from '@angular/material/tabs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ + CommonModule, + MatToolbarModule, + RouterOutlet, + MatIconModule, + MatTabNavPanel, + MatButtonModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + RouterLink, + RouterLinkActive, + ], + templateUrl: './app.component.html', + styleUrl: './app.component.scss', +}) +export class AppComponent {} diff --git a/samples/chatbot/genkit-app/src/app/app.config.ts b/samples/chatbot/genkit-app/src/app/app.config.ts new file mode 100644 index 000000000..7c47634a3 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.config.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideMarkdown } from 'ngx-markdown'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimationsAsync(), + provideMarkdown(), + ], +}; diff --git a/samples/chatbot/genkit-app/src/app/app.routes.ts b/samples/chatbot/genkit-app/src/app/app.routes.ts new file mode 100644 index 000000000..d59a096c2 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/app.routes.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Routes } from '@angular/router'; +import { ChatbotComponent } from './samples/chatbot/chatbot.component'; + +export const routes: Routes = [ + { + path: 'home', + component: ChatbotComponent, + }, + { path: '**', redirectTo: '/home' }, +]; diff --git a/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.html b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.html new file mode 100644 index 000000000..782825689 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.html @@ -0,0 +1,58 @@ + + +
+ @if (llmIndex === undefined) { +

Choose an LLM

+ @for (name of llmNames; track name; let index = $index) { + + } + } @else { +

Chat with {{ llmNames[llmIndex] }}

+ +
+
+ {{ entry.text }} +
+
+ + + +
+
+ + @if (error) { +
{{ error }}
+ } + +
+ + Chat input. Press Enter to submit (Shift+Enter for line + break) + + + +
+ } +
diff --git a/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.scss b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.scss new file mode 100644 index 000000000..02802405d --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.scss @@ -0,0 +1,75 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.wrapper { + margin-left: auto; + margin-right: auto; + padding: 20px; + width: 800px; +} + +.user-bubble { + background-color: bisque; + border: 1px solid #ccc; + border-radius: 10px; + margin-bottom: 20px; + margin-left: auto; + margin-right: 0; + min-width: 300px; + padding: 20px; + white-space: pre-wrap; + width: 80%; +} + +.model-bubble { + border: 1px solid #ccc; + border-radius: 10px; + margin-bottom: 20px; + min-width: 300px; + padding: 20px; + width: 80%; + &.llm-0 { + background-color: rgb(218, 247, 237); + } + &.llm-1 { + background-color: rgb(244, 216, 247); + } + + .text { + white-space: pre-wrap; + } + + .model-name { + font-size: small; + font-weight: 100; + margin-bottom: 11px; + } +} + +.input-field { + min-width: 400px; + vertical-align: top; + width: 730px; +} + +.error { + background-color: pink; + border: 1px solid red; + border-radius: 15px; + margin: 20px 0; + overflow: auto; + padding: 20px; +} diff --git a/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts new file mode 100644 index 000000000..c79a6e1f7 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChatbotComponent } from './chatbot.component'; + +describe('ChatbotComponent', () => { + let component: ChatbotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChatbotComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatbotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.ts b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.ts new file mode 100644 index 000000000..293bdf106 --- /dev/null +++ b/samples/chatbot/genkit-app/src/app/samples/chatbot/chatbot.component.ts @@ -0,0 +1,157 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormControl, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatRadioModule } from '@angular/material/radio'; +import { streamFlow } from 'genkit/client'; +import { MarkdownModule } from 'ngx-markdown'; + +const url = 'http://127.0.0.1:3400/chatbotFlow'; + +interface ToolResponse { + name: string; + ref: string; + output?: unknown; +} + +interface InputSchema { + role: 'user'; + text?: string; + toolResponse?: ToolResponse; +} + +interface ToolRequest { + name: string; + ref: string; + input?: unknown; +} +interface OutputSchema { + role: 'model'; + text?: string; + toolRequest?: ToolRequest; +} + +@Component({ + selector: 'app-chatbot', + standalone: true, + providers: [provideNativeDateAdapter()], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatButtonModule, + MatIconModule, + MatProgressBarModule, + MatDatepickerModule, + MarkdownModule, + MatRadioModule, + ], + templateUrl: './chatbot.component.html', + styleUrl: './chatbot.component.scss', +}) +export class ChatbotComponent { + history: (InputSchema | OutputSchema)[] = []; + error?: string; + input?: string; + loading = false; + id = Date.now() + '' + Math.floor(Math.random() * 1000000000); + llmIndex: number | undefined; + + llmNames = ['Gemini 1.5 Flash', 'Llama 3.1 405b']; + + chatFormControl = new FormControl( + 'write a function that adds two number together', + [Validators.required] + ); + + ask(input?: string) { + const text = this.chatFormControl.value!.trim(); + if (!text) return; + this.history.push({ role: 'user', text: text }); + this.chatFormControl.setValue(''); + this.chatFormControl.disable(); + this.callFlow({ role: 'user', text }); + this.loading = true; + } + + async callFlow(input: InputSchema) { + this.error = undefined; + this.loading = true; + try { + const response = await streamFlow({ + url, + input: { + prompt: input, + conversationId: this.id, + llmIndex: this.llmIndex, + }, + }); + + let textBlock: OutputSchema | undefined = undefined; + for await (const chunk of response.stream()) { + for (const content of chunk.content) { + if (content.text) { + if (!textBlock) { + textBlock = { role: 'model', text: content.text! }; + this.history.push(textBlock); + } else { + textBlock.text += content.text!; + } + } + } + } + + this.loading = false; + this.chatFormControl.enable(); + + await response.output(); + } catch (e) { + this.loading = false; + this.chatFormControl.enable(); + if ((e as any).cause) { + this.error = `${(e as any).cause}`; + } else { + this.error = `${e}`; + } + } + } + + keyPress(event: KeyboardEvent) { + if (event.key === 'Enter') { + if (event.ctrlKey || event.shiftKey) { + this.input += '\n'; + } else { + this.ask(this.input); + } + } + } +} diff --git a/samples/chatbot/genkit-app/src/index.html b/samples/chatbot/genkit-app/src/index.html new file mode 100644 index 000000000..822a28173 --- /dev/null +++ b/samples/chatbot/genkit-app/src/index.html @@ -0,0 +1,35 @@ + + + + + + + GenkitApp + + + + + + + + + + diff --git a/samples/chatbot/genkit-app/src/main.ts b/samples/chatbot/genkit-app/src/main.ts new file mode 100644 index 000000000..b1be530a2 --- /dev/null +++ b/samples/chatbot/genkit-app/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); diff --git a/samples/chatbot/genkit-app/src/styles.scss b/samples/chatbot/genkit-app/src/styles.scss new file mode 100644 index 000000000..0ff0b59e1 --- /dev/null +++ b/samples/chatbot/genkit-app/src/styles.scss @@ -0,0 +1,55 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +:root { + --header-height: 65px; + --container-border-radius: 20px; + --input-border-radius: 8px; +} + +html, +body { + height: 100%; +} + +body { + background-color: var(--app-background); + color: var(--mat-app-text-color); + margin: 0; +} + +hr { + border-bottom: 1px solid var(--divider-color); + border-width: 0 0 1px; + margin: 12px 0; +} + +a { + color: var(--link-color); + text-decoration: none; +} + +pre { + margin: 0; + white-space: pre-wrap; +} + +// Helper for filling available space in flex layouts +.flex-spacer { + flex: 1; +} diff --git a/samples/chatbot/genkit-app/tsconfig.app.json b/samples/chatbot/genkit-app/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/samples/chatbot/genkit-app/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/samples/chatbot/genkit-app/tsconfig.json b/samples/chatbot/genkit-app/tsconfig.json new file mode 100644 index 000000000..437984834 --- /dev/null +++ b/samples/chatbot/genkit-app/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/samples/chatbot/genkit-app/tsconfig.spec.json b/samples/chatbot/genkit-app/tsconfig.spec.json new file mode 100644 index 000000000..47e3dd755 --- /dev/null +++ b/samples/chatbot/genkit-app/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/samples/chatbot/package.json b/samples/chatbot/package.json new file mode 100644 index 000000000..5cfb52ff9 --- /dev/null +++ b/samples/chatbot/package.json @@ -0,0 +1,17 @@ +{ + "scripts": { + "start": "concurrently npm:start:server npm:start:ng", + "setup": "npm i && cd server && npm i && cd ../genkit-app && npm i", + "start:server": "cd server && npm run genkit:dev", + "start:ng": "cd genkit-app && npm start" + }, + "name": "js-angular", + "version": "1.0.0", + "description": "This is a simple UI for streaming RPG character generator.", + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "concurrently": "^8.2.2" + } +} diff --git a/samples/chatbot/server/package.json b/samples/chatbot/server/package.json new file mode 100644 index 000000000..4f13d4997 --- /dev/null +++ b/samples/chatbot/server/package.json @@ -0,0 +1,29 @@ +{ + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "genkit:dev": "genkit start -- npm run dev", + "dev": "tsx --watch src/index.ts", + "build": "tsc", + "build:watch": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "name": "js-angular", + "version": "1.0.0", + "description": "This is a simple UI for streaming RPG character generator.", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.0-rc || ^0.9", + "@genkit-ai/vertexai": "^0.9.0-rc || ^0.9", + "express": "^4.21.0", + "partial-json": "^0.1.7", + "zod": "^3.23.8" + }, + "devDependencies": { + "genkit-cli": "^0.9.0-rc || ^0.9", + "typescript": "^5.4.5", + "tsx": "^4.19.2" + } +} diff --git a/samples/chatbot/server/src/index.ts b/samples/chatbot/server/src/index.ts new file mode 100644 index 000000000..7bd2144bf --- /dev/null +++ b/samples/chatbot/server/src/index.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash, vertexAI } from '@genkit-ai/vertexai'; +import { + VertexAIEvaluationMetricType, + vertexAIEvaluation, +} from '@genkit-ai/vertexai/evaluation'; +import { llama31, vertexAIModelGarden } from '@genkit-ai/vertexai/modelgarden'; +import { ModelReference, PartSchema, genkit, run } from 'genkit'; +import { GenerateResponseChunkSchema } from 'genkit/model'; +import { z } from 'zod'; +import { inMemoryStore } from './memory.js'; + +export const AgentInput = z.object({ + conversationId: z.string(), + prompt: z.union([z.string(), PartSchema, z.array(PartSchema)]), + config: z.record(z.string(), z.any()).optional(), + llmIndex: z.number(), +}); + +const ai = genkit({ + plugins: [ + vertexAI({ + location: 'us-central1', + }), + vertexAIModelGarden({ + location: 'us-central1', + models: [llama31], + }), + vertexAIEvaluation({ + location: 'us-central1', + metrics: [ + VertexAIEvaluationMetricType.SAFETY, + VertexAIEvaluationMetricType.FLUENCY, + ], + }), + ], +}); + +const llms: ModelReference[] = [gemini15Flash, llama31]; + +const historyStore = inMemoryStore(); + +export const chatbotFlow = ai.defineStreamingFlow( + { + name: 'chatbotFlow', + inputSchema: AgentInput, + outputSchema: z.string(), + streamSchema: GenerateResponseChunkSchema, + }, + async (request, streamingCallback) => { + // Retrieve conversation history. + const history = await run( + 'retrieve-history', + request.conversationId, + async () => { + return (await historyStore?.load(request.conversationId)) || []; + } + ); + + // Run the user prompt (with history) through the primary LLM. + const mainResp = await ai.generate({ + prompt: request.prompt, + messages: history, + model: llms[request.llmIndex], + streamingCallback, + }); + + // Save history. + await run( + 'save-history', + { + conversationId: request.conversationId, + history: mainResp.messages, + }, + async () => { + await historyStore?.save(request.conversationId, mainResp.messages); + } + ); + return mainResp.text; + } +); + +ai.startFlowServer({ + flows: [chatbotFlow], +}); diff --git a/samples/chatbot/server/src/memory.ts b/samples/chatbot/server/src/memory.ts new file mode 100644 index 000000000..4c3561f01 --- /dev/null +++ b/samples/chatbot/server/src/memory.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageData } from 'genkit'; + +const chatHistory: Record = {}; + +export interface HistoryStore { + load(id: string): Promise; + save(id: string, history: MessageData[]): Promise; +} + +export function inMemoryStore(): HistoryStore { + return { + async load(id: string): Promise { + return chatHistory[id]; + }, + async save(id: string, history: MessageData[]) { + chatHistory[id] = history; + }, + }; +} diff --git a/samples/chatbot/server/tsconfig.json b/samples/chatbot/server/tsconfig.json new file mode 100644 index 000000000..efbb566bf --- /dev/null +++ b/samples/chatbot/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "include": ["src"], + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + } +} diff --git a/samples/js-angular/.gitignore b/samples/js-angular/.gitignore new file mode 100644 index 000000000..7951405f8 --- /dev/null +++ b/samples/js-angular/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/samples/js-angular/README.md b/samples/js-angular/README.md new file mode 100644 index 000000000..4e02b6f32 --- /dev/null +++ b/samples/js-angular/README.md @@ -0,0 +1,24 @@ +# Angular and Genkit streaming sample + +This is a simple UI for streaming RPG character generator. + +To build: + +```bash +npm i +npm run build +``` + +The sample is using Vertex AI, so you'll need to auth: + +```bash +gcloud auth application-default login +``` + +To run the sample: + +```bash +npm start +``` + +Point your browser to http://localhost:4200/ diff --git a/samples/js-angular/genkit-app/.editorconfig b/samples/js-angular/genkit-app/.editorconfig new file mode 100644 index 000000000..59d9a3a3e --- /dev/null +++ b/samples/js-angular/genkit-app/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/samples/js-angular/genkit-app/.gitignore b/samples/js-angular/genkit-app/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/samples/js-angular/genkit-app/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/samples/js-angular/genkit-app/README.md b/samples/js-angular/genkit-app/README.md new file mode 100644 index 000000000..0aeb2095b --- /dev/null +++ b/samples/js-angular/genkit-app/README.md @@ -0,0 +1,27 @@ +# GenkitApp + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/samples/js-angular/genkit-app/angular.json b/samples/js-angular/genkit-app/angular.json new file mode 100644 index 000000000..8762ae48e --- /dev/null +++ b/samples/js-angular/genkit-app/angular.json @@ -0,0 +1,99 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "genkit-app": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/genkit-app", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "genkit-app:build:production" + }, + "development": { + "buildTarget": "genkit-app:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/samples/js-angular/genkit-app/package.json b/samples/js-angular/genkit-app/package.json new file mode 100644 index 000000000..89cd74b7f --- /dev/null +++ b/samples/js-angular/genkit-app/package.json @@ -0,0 +1,41 @@ +{ + "name": "genkit-app", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.0.0", + "@angular/cdk": "^18.0.1", + "@angular/common": "^18.0.0", + "@angular/compiler": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "@angular/material": "^18.0.1", + "@angular/platform-browser": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", + "@angular/router": "^18.0.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.3", + "genkit": "^0.9.0-rc || ^0.9" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.0.2", + "@angular/cli": "^18.0.2", + "@angular/compiler-cli": "^18.0.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.4.2" + } +} diff --git a/samples/js-angular/genkit-app/public/favicon.ico b/samples/js-angular/genkit-app/public/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/samples/js-angular/genkit-app/public/favicon.ico differ diff --git a/samples/js-angular/genkit-app/src/app/app.component.html b/samples/js-angular/genkit-app/src/app/app.component.html new file mode 100644 index 000000000..0165f5b0f --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.component.html @@ -0,0 +1,36 @@ + + + diff --git a/samples/js-angular/genkit-app/src/app/app.component.scss b/samples/js-angular/genkit-app/src/app/app.component.scss new file mode 100644 index 000000000..0e0b80557 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +app-bar { + border-bottom: 1px solid var(--divider-color); + grid-area: header; +} + +article { + grid-area: content; +} + +.wrapper { + display: grid; + grid-template: + 'header' auto + 'content' 1fr + / 1fr; + height: 100vh; +} + +.home-link { + align-items: center; + color: var(--mat-app-color); + display: flex; + gap: 8px; + + img { + height: 22px; + padding-left: 4px; + } +} + +.mat-toolbar { + background: #d7e3ff; + color: #005cbb; + gap: 4px; +} + +nav { + --mdc-secondary-navigation-tab-container-height: 64px; + --mat-tab-header-divider-height: 0; + margin-left: 32px; +} + +.preview-badge { + margin-left: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } +} diff --git a/samples/js-angular/genkit-app/src/app/app.component.spec.ts b/samples/js-angular/genkit-app/src/app/app.component.spec.ts new file mode 100644 index 000000000..02dac7e71 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.component.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'genkit-app' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('genkit-app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain( + 'Hello, genkit-app' + ); + }); +}); diff --git a/samples/js-angular/genkit-app/src/app/app.component.ts b/samples/js-angular/genkit-app/src/app/app.component.ts new file mode 100644 index 000000000..39d4f4ea0 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.component.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTabNavPanel, MatTabsModule } from '@angular/material/tabs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ + CommonModule, + MatToolbarModule, + RouterOutlet, + MatIconModule, + MatTabNavPanel, + MatButtonModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + RouterLink, + RouterLinkActive, + ], + templateUrl: './app.component.html', + styleUrl: './app.component.scss', +}) +export class AppComponent {} diff --git a/samples/js-angular/genkit-app/src/app/app.config.ts b/samples/js-angular/genkit-app/src/app/app.config.ts new file mode 100644 index 000000000..3d04dfa9c --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.config.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimationsAsync(), + ], +}; diff --git a/samples/js-angular/genkit-app/src/app/app.routes.ts b/samples/js-angular/genkit-app/src/app/app.routes.ts new file mode 100644 index 000000000..bd70f607f --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/app.routes.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Routes } from '@angular/router'; +import { HomeComponent } from './home/home.component'; +import { ChatbotComponent } from './samples/chatbot/chatbot.component'; +import { StreamingJSONComponent } from './samples/streaming-json/streaming-json.component'; + +export const routes: Routes = [ + { + path: 'home', + component: HomeComponent, + }, + { + path: 'samples/streaming-json', + component: StreamingJSONComponent, + }, + { + path: 'samples/chatbot', + component: ChatbotComponent, + }, + { path: '**', redirectTo: '/home' }, +]; diff --git a/samples/js-angular/genkit-app/src/app/home/home.component.html b/samples/js-angular/genkit-app/src/app/home/home.component.html new file mode 100644 index 000000000..f57987dcc --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/home/home.component.html @@ -0,0 +1,32 @@ + + +
+

Samples

+ + + +
diff --git a/samples/js-angular/genkit-app/src/app/home/home.component.scss b/samples/js-angular/genkit-app/src/app/home/home.component.scss new file mode 100644 index 000000000..da80fa8b3 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/home/home.component.scss @@ -0,0 +1,19 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.wrapper { + padding: 20px; +} diff --git a/samples/js-angular/genkit-app/src/app/home/home.component.spec.ts b/samples/js-angular/genkit-app/src/app/home/home.component.spec.ts new file mode 100644 index 000000000..19eda49ae --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/home/home.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/samples/js-angular/genkit-app/src/app/home/home.component.ts b/samples/js-angular/genkit-app/src/app/home/home.component.ts new file mode 100644 index 000000000..f1e1997c1 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/home/home.component.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [RouterLink, RouterLinkActive], + templateUrl: './home.component.html', + styleUrl: './home.component.scss', +}) +export class HomeComponent {} diff --git a/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.html b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.html new file mode 100644 index 000000000..00829ec6d --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.html @@ -0,0 +1,77 @@ + + +
+

Chat with Agent Smith

+ +
+
+ {{ entry.text }} +
+
+ {{ entry.text }} +
+
+
+

+ sunny_snowing + {{ getWeatherLocation(entry.toolRequest) }} 27°C +

+
Warn sunny day with a mix of sun and snow.
+
+
+ + Choose a date + + MM/DD/YYYY + + + + + + + + +
+
+ Oops... unknown tool {{ entry.toolRequest.name }} +
+
+
+
+
+ +
+ + Chat input + + + + +
+
diff --git a/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.scss b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.scss new file mode 100644 index 000000000..da62c1c12 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.scss @@ -0,0 +1,55 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.wrapper { + margin-left: auto; + margin-right: auto; + padding: 20px; + width: 800px; +} + +.user-bubble { + background-color: #eeddee; + border: 1px solid #ccc; + border-radius: 10px; + margin-bottom: 20px; + margin-left: auto; + margin-right: 0; + min-width: 300px; + padding: 20px; + white-space: pre-wrap; + width: 80%; +} + +.model-bubble { + background-color: #ddddee; + border: 1px solid #ccc; + border-radius: 10px; + margin-bottom: 20px; + min-width: 300px; + padding: 20px; + width: 80%; + + .text { + white-space: pre-wrap; + } +} + +.input-field { + min-width: 400px; + vertical-align: top; + width: 730px; +} diff --git a/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts new file mode 100644 index 000000000..c79a6e1f7 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChatbotComponent } from './chatbot.component'; + +describe('ChatbotComponent', () => { + let component: ChatbotComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChatbotComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatbotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.ts b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.ts new file mode 100644 index 000000000..98e454219 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/chatbot/chatbot.component.ts @@ -0,0 +1,158 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormControl, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { + MatDatepickerInputEvent, + MatDatepickerModule, +} from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { streamFlow } from 'genkit/client'; + +const url = 'http://127.0.0.1:3400/chatbotFlow'; + +interface ToolResponse { + name: string; + ref: string; + output?: unknown; +} + +interface InputSchema { + role: 'user'; + text?: string; + toolResponse?: ToolResponse; +} + +interface ToolRequest { + name: string; + ref: string; + input?: unknown; +} +interface OutputSchema { + role: 'model'; + text?: string; + toolRequest?: ToolRequest; +} + +@Component({ + selector: 'app-chatbot', + standalone: true, + providers: [provideNativeDateAdapter()], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatButtonModule, + MatIconModule, + MatProgressBarModule, + MatDatepickerModule, + ], + templateUrl: './chatbot.component.html', + styleUrl: './chatbot.component.scss', +}) +export class ChatbotComponent { + history: (InputSchema | OutputSchema)[] = []; + error?: string; + input?: string; + loading = false; + id = Date.now() + '' + Math.floor(Math.random() * 1000000000); + + chatFormControl = new FormControl('', [Validators.required]); + + ask(input?: string) { + const text = this.chatFormControl.value!.trim(); + if (!text) return; + this.history.push({ role: 'user', text: text }); + this.chatFormControl.setValue(''); + this.chatFormControl.disable(); + this.callFlow({ role: 'user', text }); + this.loading = true; + } + + async callFlow(input: InputSchema) { + this.error = undefined; + this.loading = true; + try { + const response = await streamFlow({ + url, + input: { + prompt: input, + conversationId: this.id, + }, + }); + + let textBlock: OutputSchema | undefined = undefined; + for await (const chunk of response.stream()) { + for (const content of chunk.content) { + if (content.text) { + if (!textBlock) { + textBlock = { role: 'model', text: content.text! }; + this.history.push(textBlock); + } else { + textBlock.text += content.text!; + } + } + if (content.toolRequest) { + this.history.push({ + role: 'model', + toolRequest: content.toolRequest, + }); + } + } + } + + this.loading = false; + this.chatFormControl.enable(); + } catch (e) { + this.loading = false; + this.chatFormControl.enable(); + if ((e as any).cause) { + this.error = `${(e as any).cause}`; + } else { + this.error = `${e}`; + } + } + } + + getWeatherLocation(toolRequest: ToolRequest) { + return (toolRequest.input as any).location; + } + + datePicked(toolRequest: ToolRequest, event: MatDatepickerInputEvent) { + this.callFlow({ + role: 'user', + toolResponse: { + name: toolRequest.name, + ref: toolRequest.ref, + output: `${event.value}`, + }, + }); + } +} diff --git a/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.html b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.html new file mode 100644 index 000000000..e32ce2ae0 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.html @@ -0,0 +1,36 @@ + + +
+

Stream JSON from LLM

+ This is a Game Character Generator.
+ How many game chatacters do you need? + + + +
Loading...
+
+ {{ error }} +
+
+
+ {{ character.name }} +
    +
  • {{ ability }}
  • +
+
+
+
diff --git a/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.scss b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.scss new file mode 100644 index 000000000..70fcce28e --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.wrapper { + padding: 20px; +} + +.characters { + margin-top: 20px; +} diff --git a/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.spec.ts b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.spec.ts new file mode 100644 index 000000000..d85373105 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StreamingJSONComponent } from './streaming-json.component'; + +describe('StreamingJSONComponent', () => { + let component: StreamingJSONComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StreamingJSONComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(StreamingJSONComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.ts b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.ts new file mode 100644 index 000000000..9d39d3148 --- /dev/null +++ b/samples/js-angular/genkit-app/src/app/samples/streaming-json/streaming-json.component.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { streamFlow } from 'genkit/client'; + +const url = 'http://127.0.0.1:3400/streamCharacters'; + +@Component({ + selector: 'app-streaming-json', + standalone: true, + imports: [FormsModule, CommonModule, MatButtonModule], + templateUrl: './streaming-json.component.html', + styleUrl: './streaming-json.component.scss', +}) +export class StreamingJSONComponent { + count: string = '3'; + characters: any = undefined; + error?: string = undefined; + loading: boolean = false; + + async callFlow() { + this.characters = undefined; + this.error = undefined; + this.loading = true; + try { + const response = streamFlow({ + url, + input: parseInt(this.count), + }); + for await (const chunk of response.stream()) { + this.characters = chunk; + } + console.log('streamConsumer done', await response.output()); + this.loading = false; + } catch (e) { + this.loading = false; + if ((e as any).cause) { + this.error = `${(e as any).cause}`; + } else { + this.error = `${e}`; + } + } + } +} diff --git a/samples/js-angular/genkit-app/src/index.html b/samples/js-angular/genkit-app/src/index.html new file mode 100644 index 000000000..822a28173 --- /dev/null +++ b/samples/js-angular/genkit-app/src/index.html @@ -0,0 +1,35 @@ + + + + + + + GenkitApp + + + + + + + + + + diff --git a/samples/js-angular/genkit-app/src/main.ts b/samples/js-angular/genkit-app/src/main.ts new file mode 100644 index 000000000..b1be530a2 --- /dev/null +++ b/samples/js-angular/genkit-app/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); diff --git a/samples/js-angular/genkit-app/src/styles.scss b/samples/js-angular/genkit-app/src/styles.scss new file mode 100644 index 000000000..0ff0b59e1 --- /dev/null +++ b/samples/js-angular/genkit-app/src/styles.scss @@ -0,0 +1,55 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +:root { + --header-height: 65px; + --container-border-radius: 20px; + --input-border-radius: 8px; +} + +html, +body { + height: 100%; +} + +body { + background-color: var(--app-background); + color: var(--mat-app-text-color); + margin: 0; +} + +hr { + border-bottom: 1px solid var(--divider-color); + border-width: 0 0 1px; + margin: 12px 0; +} + +a { + color: var(--link-color); + text-decoration: none; +} + +pre { + margin: 0; + white-space: pre-wrap; +} + +// Helper for filling available space in flex layouts +.flex-spacer { + flex: 1; +} diff --git a/samples/js-angular/genkit-app/tsconfig.app.json b/samples/js-angular/genkit-app/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/samples/js-angular/genkit-app/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/samples/js-angular/genkit-app/tsconfig.json b/samples/js-angular/genkit-app/tsconfig.json new file mode 100644 index 000000000..437984834 --- /dev/null +++ b/samples/js-angular/genkit-app/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/samples/js-angular/genkit-app/tsconfig.spec.json b/samples/js-angular/genkit-app/tsconfig.spec.json new file mode 100644 index 000000000..47e3dd755 --- /dev/null +++ b/samples/js-angular/genkit-app/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/samples/js-angular/package.json b/samples/js-angular/package.json new file mode 100644 index 000000000..07f36dae4 --- /dev/null +++ b/samples/js-angular/package.json @@ -0,0 +1,17 @@ +{ + "name": "js-angular", + "version": "1.0.0", + "description": "This is a simple UI for streaming RPG character generator.", + "scripts": { + "start": "concurrently npm:start:server npm:start:ng", + "start:server": "cd server && npm run genkit:dev", + "start:ng": "cd genkit-app && npm run start", + "setup": "npm i && cd server && npm i && cd ../genkit-app && npm i" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "concurrently": "^8.2.2" + } +} diff --git a/samples/js-angular/server/package.json b/samples/js-angular/server/package.json new file mode 100644 index 000000000..428c00d1c --- /dev/null +++ b/samples/js-angular/server/package.json @@ -0,0 +1,27 @@ +{ + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "genkit:dev": "genkit start -- tsx --watch src/index.ts", + "build": "tsc", + "build:watch": "tsc --watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "name": "js-angular", + "version": "1.0.0", + "description": "This is a simple UI for streaming RPG character generator.", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.0-rc || ^0.9", + "@genkit-ai/vertexai": "^0.9.0-rc || ^0.9", + "express": "^4.21.0", + "partial-json": "^0.1.7" + }, + "devDependencies": { + "genkit-cli": "^0.9.0-rc || ^0.9", + "typescript": "^5.4.5", + "tsx": "^4.19.2" + } +} diff --git a/samples/js-angular/server/src/agent.ts b/samples/js-angular/server/src/agent.ts new file mode 100644 index 000000000..3ee2f03b5 --- /dev/null +++ b/samples/js-angular/server/src/agent.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GenerateResponse, + Genkit, + MessageData, + ModelArgument, + PartSchema, + ToolArgument, + run, + z, +} from 'genkit'; +import { GenerateResponseSchema } from 'genkit/model'; + +export interface HistoryStore { + load(id: string): Promise; + save(id: string, history: MessageData[]): Promise; +} + +export const AgentInput = z.object({ + conversationId: z.string(), + prompt: z.union([z.string(), PartSchema, z.array(PartSchema)]), + config: z.record(z.string(), z.any()).optional(), +}); + +type AgentFn = ( + request: z.infer, + history: MessageData[] | undefined +) => Promise>; + +export function defineAgent( + ai: Genkit, + { + name, + tools, + model, + historyStore, + systemPrompt, + returnToolRequests, + }: { + name: string; + systemPrompt?: string; + tools?: ToolArgument[]; + model: ModelArgument; + historyStore?: HistoryStore; + returnToolRequests?: boolean; + }, + customFn?: AgentFn +) { + return ai.defineFlow( + { name, inputSchema: AgentInput, outputSchema: GenerateResponseSchema }, + async (request, streamingCallback) => { + const history = await run( + 'retrieve-history', + request.conversationId, + async () => { + let history = request.conversationId + ? await historyStore?.load(request.conversationId) + : undefined; + if (!history && systemPrompt) { + history = [ + { + role: 'system', + content: [ + { + text: systemPrompt, + }, + ], + }, + ]; + } + return history; + } + ); + const resp = customFn + ? await customFn(request, history) + : await ai.generate({ + prompt: request.prompt, + messages: history, + model, + tools, + returnToolRequests, + streamingCallback, + }); + await run( + 'save-history', + { conversationId: request.conversationId, history: resp.messages }, + async () => { + request.conversationId + ? await historyStore?.save(request.conversationId, resp.messages) + : undefined; + } + ); + return resp.toJSON(); + } + ); +} diff --git a/samples/js-angular/server/src/chatbot.ts b/samples/js-angular/server/src/chatbot.ts new file mode 100644 index 000000000..14f661406 --- /dev/null +++ b/samples/js-angular/server/src/chatbot.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageData } from '@genkit-ai/ai/model'; +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { z } from 'zod'; +import { defineAgent, HistoryStore } from './agent.js'; +import { ai } from './genkit.js'; + +const weatherTool = ai.defineTool( + { + name: 'weatherTool', + description: 'use this tool to display weather', + inputSchema: z.object({ + date: z + .string() + .describe('date (use datePicker tool if user did not specify)'), + location: z.string().describe('location (ZIP, city, etc.)'), + }), + outputSchema: z.string().optional(), + }, + async () => undefined +); + +const datePicker = ai.defineTool( + { + name: 'datePicker', + description: + 'user can use this UI tool to enter a date (prefer this over asking the user to enter the date manually)', + inputSchema: z.object({ + ignore: z.string().describe('ignore this (set to undefined)').optional(), + }), + outputSchema: z.string().optional(), + }, + async () => undefined +); + +export const chatbotFlow = defineAgent(ai, { + name: 'chatbotFlow', + model: gemini15Flash, + tools: [weatherTool, datePicker], + returnToolRequests: true, + systemPrompt: + 'You are a helpful agent. You have the personality of Agent Smith from Matrix. ' + + 'There are tools/functions at your disposal, ' + + 'feel free to call them. If you think a tool/function can help but you do ' + + 'not have sufficient context make sure to ask clarifying questions.', + historyStore: inMemoryStore(), +}); + +const chatHistory: Record = {}; + +function inMemoryStore(): HistoryStore { + return { + async load(id: string): Promise { + return chatHistory[id]; + }, + async save(id: string, history: MessageData[]) { + chatHistory[id] = history; + }, + }; +} diff --git a/samples/js-angular/server/src/genkit.ts b/samples/js-angular/server/src/genkit.ts new file mode 100644 index 000000000..5da87fb17 --- /dev/null +++ b/samples/js-angular/server/src/genkit.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import vertexAI from '@genkit-ai/vertexai'; +import { genkit } from 'genkit'; + +export const ai = genkit({ + plugins: [vertexAI()], +}); diff --git a/samples/js-angular/server/src/index.ts b/samples/js-angular/server/src/index.ts new file mode 100644 index 000000000..e5b080060 --- /dev/null +++ b/samples/js-angular/server/src/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { chatbotFlow } from './chatbot.js'; +import { ai } from './genkit.js'; +import { streamCharacters } from './jsonStreaming.js'; + +ai.startFlowServer({ + flows: [chatbotFlow, streamCharacters], +}); diff --git a/samples/js-angular/server/src/jsonStreaming.ts b/samples/js-angular/server/src/jsonStreaming.ts new file mode 100644 index 000000000..df0611100 --- /dev/null +++ b/samples/js-angular/server/src/jsonStreaming.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { z } from 'genkit'; +import { Allow, parse } from 'partial-json'; +import { ai } from './genkit.js'; + +const GameCharactersSchema = z.object({ + characters: z + .array( + z + .object({ + name: z.string().describe('Name of a character'), + abilities: z + .array(z.string()) + .describe('Various abilities (strength, magic, archery, etc.)'), + }) + .describe('Game character') + ) + .describe('Characters'), +}); + +export const streamCharacters = ai.defineStreamingFlow( + { + name: 'streamCharacters', + inputSchema: z.number(), + outputSchema: z.string(), + streamSchema: GameCharactersSchema, + }, + async (count, streamingCallback) => { + if (!streamingCallback) { + throw new Error('this flow only works in streaming mode'); + } + + const { response, stream } = await ai.generateStream({ + model: gemini15Flash, + output: { + format: 'json', + schema: GameCharactersSchema, + }, + config: { + temperature: 1, + }, + prompt: `Respond as JSON only. Generate ${count} different RPG game characters.`, + }); + + let buffer = ''; + for await (const chunk of stream) { + buffer += chunk.content[0].text!; + if (buffer.length > 10) { + streamingCallback(parse(maybeStripMarkdown(buffer), Allow.ALL)); + } + } + + return (await response).text; + } +); + +const markdownRegex = /^\s*(```json)?((.|\n)*?)(```)?\s*$/i; +function maybeStripMarkdown(withMarkdown: string) { + const mdMatch = markdownRegex.exec(withMarkdown); + if (!mdMatch) { + return withMarkdown; + } + return mdMatch[2]; +} diff --git a/samples/js-angular/server/tsconfig.json b/samples/js-angular/server/tsconfig.json new file mode 100644 index 000000000..efbb566bf --- /dev/null +++ b/samples/js-angular/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "include": ["src"], + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + } +} diff --git a/samples/js-coffee-shop/README.md b/samples/js-coffee-shop/README.md new file mode 100644 index 000000000..618a80f5e --- /dev/null +++ b/samples/js-coffee-shop/README.md @@ -0,0 +1,6 @@ +## Running the sample + +```bash +npm i +genkit start +``` diff --git a/samples/js-coffee-shop/package.json b/samples/js-coffee-shop/package.json new file mode 100644 index 000000000..b32e5b1b5 --- /dev/null +++ b/samples/js-coffee-shop/package.json @@ -0,0 +1,35 @@ +{ + "name": "coffee-shop", + "version": "1.0.0", + "description": "Genkit samples for a coffeeshop", + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "genkit:dev": "genkit start -- tsx --watch src/index.ts", + "compile": "tsc", + "build": "npm run build:clean && npm run compile", + "build:clean": "rimraf ./lib", + "build:watch": "tsc --watch", + "build-and-run": "npm run build && node lib/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.0-rc || ^0.9", + "genkitx-chromadb": "^0.9.0-rc || ^0.9", + "@genkit-ai/dev-local-vectorstore": "^0.9.0-rc || ^0.9", + "@genkit-ai/firebase": "^0.9.0-rc || ^0.9", + "@genkit-ai/googleai": "^0.9.0-rc || ^0.9", + "genkitx-ollama": "^0.9.0-rc || ^0.9", + "genkitx-pinecone": "^0.9.0-rc || ^0.9", + "@genkit-ai/evaluator": "^0.9.0-rc || ^0.9", + "@genkit-ai/vertexai": "^0.9.0-rc || ^0.9", + "zod": "^3.22.4" + }, + "devDependencies": { + "genkit-cli": "^0.9.0-rc || ^0.9", + "rimraf": "^6.0.1", + "typescript": "^5.3.3" + } +} diff --git a/samples/js-coffee-shop/src/index.ts b/samples/js-coffee-shop/src/index.ts new file mode 100644 index 000000000..5e6745b12 --- /dev/null +++ b/samples/js-coffee-shop/src/index.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { genkit, z } from 'genkit'; + +const ai = genkit({ + plugins: [googleAI()], +}); + +// This example generates greetings for a customer at our new AI-powered coffee shop, +// demonstrating how to use prompts in Genkit flows. + +// A flow to greet a customer by name + +const CustomerNameSchema = z.object({ + customerName: z.string(), +}); + +const simpleGreetingPrompt = ai.definePrompt( + { + name: 'simpleGreeting', + model: gemini15Flash, + input: { schema: CustomerNameSchema }, + output: { + format: 'text', + }, + }, + ` +You're a barista at a nice coffee shop. +A regular customer named {{customerName}} enters. +Greet the customer in one sentence, and recommend a coffee drink. +` +); + +export const simpleGreetingFlow = ai.defineFlow( + { + name: 'simpleGreeting', + inputSchema: CustomerNameSchema, + outputSchema: z.string(), + }, + async (input) => (await simpleGreetingPrompt(input)).text +); + +// Another flow to recommend a drink based on the time of day and a previous order. +// This prompt uses multiple messages, alternating roles +// to make the response more conversational. + +const CustomerTimeAndHistorySchema = z.object({ + customerName: z.string(), + currentTime: z.string(), + previousOrder: z.string(), +}); + +const greetingWithHistoryPrompt = ai.definePrompt( + { + name: 'greetingWithHistory', + model: gemini15Flash, + input: { schema: CustomerTimeAndHistorySchema }, + output: { + format: 'text', + }, + }, + ` +{{role "user"}} +Hi, my name is {{customerName}}. The time is {{currentTime}}. Who are you? + +{{role "model"}} +I am Barb, a barista at this nice underwater-themed coffee shop called Krabby Kooffee. +I know pretty much everything there is to know about coffee, +and I can cheerfully recommend delicious coffee drinks to you based on whatever you like. + +{{role "user"}} +Great. Last time I had {{previousOrder}}. +I want you to greet me in one sentence, and recommend a drink. +` +); + +export const greetingWithHistoryFlow = ai.defineFlow( + { + name: 'greetingWithHistory', + inputSchema: CustomerTimeAndHistorySchema, + outputSchema: z.string(), + }, + async (input) => (await greetingWithHistoryPrompt(input)).text +); + +// A flow to quickly test all the above flows +// Run on the CLI with `$ genkit flow:run testAllCoffeeFlows` +// View the trace in the Developer UI to see the llm responses. + +export const testAllCoffeeFlows = ai.defineFlow( + { + name: 'testAllCoffeeFlows', + outputSchema: z.object({ + pass: z.boolean(), + error: z.string().optional(), + }), + }, + async () => { + const test1 = simpleGreetingFlow({ customerName: 'Sam' }); + const test2 = greetingWithHistoryFlow({ + customerName: 'Sam', + currentTime: '09:45am', + previousOrder: 'Caramel Macchiato', + }); + + return Promise.all([test1, test2]) + .then((unused) => { + return { pass: true }; + }) + .catch((e: Error) => { + return { pass: false, error: e.toString() }; + }); + } +); diff --git a/samples/js-coffee-shop/src/input.json b/samples/js-coffee-shop/src/input.json new file mode 100644 index 000000000..d669609ad --- /dev/null +++ b/samples/js-coffee-shop/src/input.json @@ -0,0 +1,7 @@ +{ + "start": { + "customerName": "sam", + "currentTime": "12:30AM", + "previousOrders": ["Americano"] + } +} diff --git a/samples/js-coffee-shop/tsconfig.json b/samples/js-coffee-shop/tsconfig.json new file mode 100644 index 000000000..e51f33ae3 --- /dev/null +++ b/samples/js-coffee-shop/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "noImplicitReturns": true, + "noUnusedLocals": false, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + }, + "compileOnSave": true, + "include": ["src"] +} diff --git a/samples/js-menu/README.md b/samples/js-menu/README.md new file mode 100644 index 000000000..8200d81b6 --- /dev/null +++ b/samples/js-menu/README.md @@ -0,0 +1,27 @@ +## Menu Understanding Sample Application + +This sample demonstrates an application that can understand a restaurant menu and answer relevant questions about the items on the menu. + +There are 5 iterations of this sample application, growing in complexity and demonstrating utilization of many different Genkit features. + +To test each one out, open the Developer UI and exercise the prompts and flows. Each step contains one or more `example.json` files which you can use as inputs. + +### Prerequisites + +This example uses Vertex AI for language models and embeddings. + +### Prompts and Flows + +1. This step shows how to define prompts in code that can accept user input to their templates. +2. This step illustrates how to wrap your llm calls and other application code into flows with strong input and output schemas. + It also adds an example of tool usage to load the menu from a data file. +3. This step adds session history and supports a multi-turn chat with the model. +4. This step ingests the menu items into a vector database and uses retrieval to include releveant menu items in the prompt. +5. This step illustrates how to combine models with different modalities. It uses a vision model to ingest the menu items from a photograph. + +## Running the sample + +```bash +npm i +genkit start +``` diff --git a/samples/js-menu/data/menu.jpeg b/samples/js-menu/data/menu.jpeg new file mode 100644 index 000000000..ae096cd7b Binary files /dev/null and b/samples/js-menu/data/menu.jpeg differ diff --git a/samples/js-menu/data/menu.json b/samples/js-menu/data/menu.json new file mode 100644 index 000000000..d46739205 --- /dev/null +++ b/samples/js-menu/data/menu.json @@ -0,0 +1,97 @@ +[ + { + "title": "Mozzarella Sticks", + "price": 8, + "description": "Crispy fried mozzarella sticks served with marinara sauce." + }, + { + "title": "Chicken Wings", + "price": 10, + "description": "Crispy fried chicken wings tossed in your choice of sauce." + }, + { + "title": "Nachos", + "price": 12, + "description": "Crispy tortilla chips topped with melted cheese, chili, sour cream, and salsa." + }, + { + "title": "Onion Rings", + "price": 7, + "description": "Crispy fried onion rings served with ranch dressing." + }, + { + "title": "French Fries", + "price": 5, + "description": "Crispy fried french fries." + }, + { + "title": "Mashed Potatoes", + "price": 6, + "description": "Creamy mashed potatoes." + }, + { + "title": "Coleslaw", + "price": 4, + "description": "Homemade coleslaw." + }, + { + "title": "Classic Cheeseburger", + "price": 12, + "description": "A juicy beef patty topped with melted American cheese, lettuce, tomato, and onion on a toasted bun." + }, + { + "title": "Bacon Cheeseburger", + "price": 14, + "description": "A classic cheeseburger with the addition of crispy bacon." + }, + { + "title": "Mushroom Swiss Burger", + "price": 15, + "description": "A beef patty topped with sautéed mushrooms, melted Swiss cheese, and a creamy horseradish sauce." + }, + { + "title": "Chicken Sandwich", + "price": 13, + "description": "A crispy chicken breast on a toasted bun with lettuce, tomato, and your choice of sauce." + }, + { + "title": "Pulled Pork Sandwich", + "price": 14, + "description": "Slow-cooked pulled pork on a toasted bun with coleslaw and barbecue sauce." + }, + { + "title": "Reuben Sandwich", + "price": 15, + "description": "Thinly sliced corned beef, Swiss cheese, sauerkraut, and Thousand Island dressing on rye bread." + }, + { + "title": "House Salad", + "price": 8, + "description": "Mixed greens with your choice of dressing." + }, + { + "title": "Caesar Salad", + "price": 9, + "description": "Romaine lettuce with croutons, Parmesan cheese, and Caesar dressing." + }, + { + "title": "Greek Salad", + "price": 10, + "description": "Mixed greens with feta cheese, olives, tomatoes, cucumbers, and red onions." + }, + { + "title": "Chocolate Lava Cake", + "price": 8, + "description": "A warm, gooey chocolate cake with a molten chocolate center." + }, + { + "title": "Apple Pie", + "price": 7, + "description": "A classic apple pie with a flaky crust and warm apple filling." + }, + { + "title": "Cheesecake", + "price": 8, + "description": "A creamy cheesecake with a graham cracker crust." + } +] diff --git a/samples/js-menu/package.json b/samples/js-menu/package.json new file mode 100644 index 000000000..1488af432 --- /dev/null +++ b/samples/js-menu/package.json @@ -0,0 +1,30 @@ +{ + "name": "menu", + "version": "1.0.0", + "description": "Genkit samples for a menu understanding app", + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "genkit:dev": "genkit start -- tsx --watch src/index.ts", + "compile": "tsc", + "build": "npm run build:clean && npm run compile", + "build:clean": "rimraf ./lib", + "build:watch": "tsc --watch", + "build-and-run": "npm run build && node lib/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.0-rc || ^0.9", + "@genkit-ai/dev-local-vectorstore": "^0.9.0-rc || ^0.9", + "@genkit-ai/firebase": "^0.9.0-rc || ^0.9", + "@genkit-ai/evaluator": "^0.9.0-rc || ^0.9", + "@genkit-ai/vertexai": "^0.9.0-rc || ^0.9" + }, + "devDependencies": { + "genkit-cli": "^0.9.0-rc || ^0.9", + "rimraf": "^6.0.1", + "typescript": "^5.3.3" + } +} diff --git a/samples/js-menu/src/01/example.json b/samples/js-menu/src/01/example.json new file mode 100644 index 000000000..10c76a2ea --- /dev/null +++ b/samples/js-menu/src/01/example.json @@ -0,0 +1,5 @@ +{ + "input": { + "question": "Which of your burgers would you recommend for someone like me who loves bacon?" + } +} diff --git a/samples/js-menu/src/01/prompts.ts b/samples/js-menu/src/01/prompts.ts new file mode 100644 index 000000000..c5157cf1e --- /dev/null +++ b/samples/js-menu/src/01/prompts.ts @@ -0,0 +1,87 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GenerateRequest } from '@genkit-ai/ai/model'; +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { ai } from '../genkit.js'; +import { MenuQuestionInput, MenuQuestionInputSchema } from '../types'; + +// Define a prompt to handle a customer question about the menu. +// This prompt uses definePrompt directly. + +export const s01_vanillaPrompt = ai.definePrompt( + { + name: 's01_vanillaPrompt', + input: { schema: MenuQuestionInputSchema }, + }, + async (input: MenuQuestionInput): Promise => { + const promptText = ` + You are acting as a helpful AI assistant named "Walt" that can answer + questions about the food available on the menu at Walt's Burgers. + Customer says: ${input.question} + `; + + return { + messages: [{ role: 'user', content: [{ text: promptText }] }], + config: { temperature: 0.3 }, + }; + } +); + +// Define another prompt which uses the Dotprompt library +// that also gives us a type-safe handlebars template system, +// and well-defined output schemas. + +export const s01_staticMenuDotPrompt = ai.definePrompt( + { + name: 's01_staticMenuDotPrompt', + model: gemini15Flash, + input: { schema: MenuQuestionInputSchema }, + output: { format: 'text' }, + }, + ` +You are acting as a helpful AI assistant named "Walt" that can answer +questions about the food available on the menu at Walt's Burgers. +Here is today's menu: + +- The Regular Burger $12 + The classic charbroiled to perfection with your choice of cheese + +- The Fancy Burger $13 + Classic burger topped with bacon & Blue Cheese + +- The Bacon Burger $13 + Bacon cheeseburger with your choice of cheese. + +- Everything Burger $14 + Heinz 57 sauce, American cheese, bacon, fried egg & crispy onion bits + +- Chicken Breast Sandwich $12 + Tender juicy chicken breast on a brioche roll. + Grilled, blackened, or fried + +Our fresh 1/2 lb. beef patties are made using choice cut +brisket, short rib & sirloin. Served on a toasted +brioche roll with chips. Served with lettuce, tomato & pickles. +Onions upon request. Substitute veggie patty $2 + +Answer this customer's question, in a concise and helpful manner, +as long as it is about food. + +Question: +{{question}} ? +` +); diff --git a/samples/js-menu/src/02/example.json b/samples/js-menu/src/02/example.json new file mode 100644 index 000000000..57a597769 --- /dev/null +++ b/samples/js-menu/src/02/example.json @@ -0,0 +1,3 @@ +{ + "question": "I'd like to try something spicy. What do you recommend?" +} diff --git a/samples/js-menu/src/02/flows.ts b/samples/js-menu/src/02/flows.ts new file mode 100644 index 000000000..4b3f66b8d --- /dev/null +++ b/samples/js-menu/src/02/flows.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ai } from '../genkit.js'; +import { AnswerOutputSchema, MenuQuestionInputSchema } from '../types'; +import { s02_dataMenuPrompt } from './prompts'; + +// Define a flow which generates a response from the prompt. + +export const s02_menuQuestionFlow = ai.defineFlow( + { + name: 's02_menuQuestion', + inputSchema: MenuQuestionInputSchema, + outputSchema: AnswerOutputSchema, + }, + async (input) => { + const { text } = await s02_dataMenuPrompt({ question: input.question }); + return { answer: text }; + } +); diff --git a/samples/js-menu/src/02/prompts.ts b/samples/js-menu/src/02/prompts.ts new file mode 100644 index 000000000..9ca0519ed --- /dev/null +++ b/samples/js-menu/src/02/prompts.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { ai } from '../genkit.js'; +import { MenuQuestionInputSchema } from '../types'; +import { menuTool } from './tools'; + +// The prompt uses a tool which will load the menu data, +// if the user asks a reasonable question about the menu. + +export const s02_dataMenuPrompt = ai.definePrompt( + { + name: 's02_dataMenu', + model: gemini15Flash, + input: { schema: MenuQuestionInputSchema }, + output: { format: 'text' }, + tools: [menuTool], + }, + ` +You are acting as a helpful AI assistant named Walt that can answer +questions about the food available on the menu at Walt's Burgers. + +Answer this customer's question, in a concise and helpful manner, +as long as it is about food on the menu or something harmless like sports. +Use the tools available to answer menu questions. +DO NOT INVENT ITEMS NOT ON THE MENU. + +Question: +{{question}} ? +` +); diff --git a/samples/js-menu/src/02/tools.ts b/samples/js-menu/src/02/tools.ts new file mode 100644 index 000000000..0c280f949 --- /dev/null +++ b/samples/js-menu/src/02/tools.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'genkit'; +import { ai } from '../genkit.js'; +import { MenuItem, MenuItemSchema } from '../types'; + +const menuData: Array = require('../../data/menu.json'); + +export const menuTool = ai.defineTool( + { + name: 'todaysMenu', + description: "Use this tool to retrieve all the items on today's menu", + inputSchema: z.object({}), + outputSchema: z.object({ + menuData: z + .array(MenuItemSchema) + .describe('A list of all the items on the menu'), + }), + }, + async () => Promise.resolve({ menuData: menuData }) +); diff --git a/samples/js-menu/src/03/chats.ts b/samples/js-menu/src/03/chats.ts new file mode 100644 index 000000000..4a3ab51be --- /dev/null +++ b/samples/js-menu/src/03/chats.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageData, MessageSchema } from '@genkit-ai/ai/model'; +import { z } from 'genkit'; + +// Our flow will take a sessionId along with each question to track the chat history. +// The host application should keep track of these ids somewhere. + +export const ChatSessionInputSchema = z.object({ + sessionId: z.string(), + question: z.string(), +}); + +// The flow will respond with an array of messages, +// which includes all history up until that point +// plus the last exchange with the model. + +export const ChatSessionOutputSchema = z.object({ + sessionId: z.string(), + history: z.array(MessageSchema), +}); + +export type ChatHistory = Array; + +// This is a very simple local storage for chat history. +// Each conversation is identified by a sessionId generated by the application. +// The constructor accepts a preamble of messages, which serve as a system prompt. + +export class ChatHistoryStore { + private preamble: ChatHistory; + private sessions: Map = new Map(); + + constructor(preamble: ChatHistory = []) { + this.preamble = preamble; + } + + write(sessionId: string, history: ChatHistory) { + this.sessions.set(sessionId, history); + } + + read(sessionId: string): ChatHistory { + return this.sessions.get(sessionId) || this.preamble; + } +} diff --git a/samples/js-menu/src/03/example.json b/samples/js-menu/src/03/example.json new file mode 100644 index 000000000..19ed21af5 --- /dev/null +++ b/samples/js-menu/src/03/example.json @@ -0,0 +1,4 @@ +{ + "sessionId": "session123", + "question": "Do you have anything healthy on this menu?" +} diff --git a/samples/js-menu/src/03/flows.ts b/samples/js-menu/src/03/flows.ts new file mode 100644 index 000000000..f71bc3a98 --- /dev/null +++ b/samples/js-menu/src/03/flows.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageData } from '@genkit-ai/ai/model'; +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { run } from 'genkit'; +import { ai } from '../genkit.js'; +import { MenuItem } from '../types'; +import { + ChatHistoryStore, + ChatSessionInputSchema, + ChatSessionOutputSchema, +} from './chats'; + +// Load the menu data from a JSON file. +const menuData = require('../../data/menu.json') as Array; + +// Render the preamble prompt that seeds our chat history. +const preamble: Array = [ + { + role: 'user', + content: [ + { + text: "Hi. What's on the menu today?", + }, + ], + }, + { + role: 'user', + content: [ + { + text: + 'I am Walt, a helpful AI assistant here at the restaurant.\n' + + 'I can answer questions about the food on the menu or any other questions\n' + + "you have about food in general. I probably can't help you with anything else.\n" + + "Here is today's menu: \n" + + menuData + .map((r) => `- ${r.title} ${r.price}\n${r.description}`) + .join('\n') + + 'Do you have any questions about the menu?\n', + }, + ], + }, +]; + +// A simple local storage for chat session history. +// You should probably actually use Firestore for this. +const chatHistoryStore = new ChatHistoryStore(preamble); + +// Define a flow which generates a response to each question. + +export const s03_multiTurnChatFlow = ai.defineFlow( + { + name: 's03_multiTurnChat', + inputSchema: ChatSessionInputSchema, + outputSchema: ChatSessionOutputSchema, + }, + async (input) => { + // First fetch the chat history. We'll wrap this in a run block. + // If we were going to a database for the history, + // we might want to have that db result captured in the trace. + let history = await run('fetchHistory', async () => + chatHistoryStore.read(input.sessionId) + ); + + // Generate the response + const llmResponse = await ai.generate({ + model: gemini15Flash, + messages: history, + prompt: { + text: input.question, + }, + }); + + // Add the exchange to the history store and return it + history = llmResponse.messages; + chatHistoryStore.write(input.sessionId, history); + return { + sessionId: input.sessionId, + history: history, + }; + } +); diff --git a/samples/js-menu/src/03/prompts.ts b/samples/js-menu/src/03/prompts.ts new file mode 100644 index 000000000..d332da7b8 --- /dev/null +++ b/samples/js-menu/src/03/prompts.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { ai } from '../genkit.js'; +import { DataMenuQuestionInputSchema } from '../types'; + +// This prompt will generate two messages when rendered. +// These two messages will be used to seed the exchange with the model. + +export const s03_chatPreamblePrompt = ai.definePrompt( + { + name: 's03_chatPreamble', + model: gemini15Flash, + input: { schema: DataMenuQuestionInputSchema }, + output: { format: 'text' }, + config: { temperature: 0.3 }, + }, + ` + {{ role "user" }} + Hi. What's on the menu today? + + {{ role "model" }} + I am Walt, a helpful AI assistant here at the restaurant. + I can answer questions about the food on the menu or any other questions + you have about food in general. I probably can't help you with anything else. + Here is today's menu: + {{#each menuData~}} + - {{this.title}} \${{this.price}} + {{this.description}} + {{~/each}} + Do you have any questions about the menu? +` +); diff --git a/samples/js-menu/src/04/example.indexMenuItems.json b/samples/js-menu/src/04/example.indexMenuItems.json new file mode 100644 index 000000000..d528d8ae9 --- /dev/null +++ b/samples/js-menu/src/04/example.indexMenuItems.json @@ -0,0 +1,57 @@ +[ + { + "title": "White Meat Crispy Chicken Wings", + "description": "All-white meat chicken wings tossed in your choice of wing sauce. Choose from classic buffalo, honey bbq, garlic parmesan, or sweet & sour", + "price": 12.0 + }, + { + "title": "Cheese Fries", + "description": "Fresh fries covered with melted cheddar cheese and bacon", + "price": 8.0 + }, + { + "title": "Reuben", + "description": "Classic Reuben sandwich with corned beef, sauerkraut, Swiss cheese, and Thousand Island dressing on grilled rye bread.", + "price": 12.0 + }, + { + "title": "Grilled Chicken Club Wrap", + "description": "Grilled chicken, bacon, lettuce, tomato, pickles, and cheddar cheese wrapped in a spinach tortilla, served with your choice of dressing", + "price": 12.0 + }, + { + "title": "Buffalo Chicken Sandwich", + "description": "Fried chicken breast coated in your choice of wing sauce, topped with lettuce, tomato, onion, and pickles on a toasted brioche roll.", + "price": 12.0 + }, + { + "title": "Half Cuban Sandwich", + "description": "Slow roasted pork butt, ham, Swiss, and yellow mustard on a toasted baguette", + "price": 12.0 + }, + { + "title": "The Albie Burger", + "description": "Classic burger topped with bacon, provolone, banana peppers, and chipotle mayo", + "price": 13.0 + }, + { + "title": "57 Chevy Burger", + "description": "Heaven burger with your choice of cheese", + "price": 14.0 + }, + { + "title": "Chicken Caesar Wrap", + "description": "Tender grilled chicken, romaine lettuce, croutons, and Parmesan cheese tossed in a creamy Caesar dressing and wrapped in a spinach tortilla", + "price": 10.0 + }, + { + "title": "Kids Hot Dog", + "description": "Kids under 12", + "price": 5.0 + }, + { + "title": "Chicken Fingers", + "description": "Tender chicken strips, grilled or fried", + "price": 8.0 + } +] diff --git a/samples/js-menu/src/04/example.menuQuestion.json b/samples/js-menu/src/04/example.menuQuestion.json new file mode 100644 index 000000000..21c7c4647 --- /dev/null +++ b/samples/js-menu/src/04/example.menuQuestion.json @@ -0,0 +1,3 @@ +{ + "question": "I'd like something cheesy!" +} diff --git a/samples/js-menu/src/04/flows.ts b/samples/js-menu/src/04/flows.ts new file mode 100644 index 000000000..c223dbe0e --- /dev/null +++ b/samples/js-menu/src/04/flows.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document } from '@genkit-ai/ai/retriever'; +import { + devLocalIndexerRef, + devLocalRetrieverRef, +} from '@genkit-ai/dev-local-vectorstore'; +import { z } from 'genkit'; +import { ai } from '../genkit.js'; +import { + AnswerOutputSchema, + MenuItem, + MenuItemSchema, + MenuQuestionInputSchema, +} from '../types'; +import { s04_ragDataMenuPrompt } from './prompts'; + +// Define a flow which indexes items on the menu. + +export const s04_indexMenuItemsFlow = ai.defineFlow( + { + name: 's04_indexMenuItems', + inputSchema: z.array(MenuItemSchema), + outputSchema: z.object({ rows: z.number() }), + }, + async (menuItems) => { + // Store each document with its text indexed, + // and its original JSON data as its metadata. + const documents = menuItems.map((menuItem) => { + const text = `${menuItem.title} ${menuItem.price} \n ${menuItem.description}`; + return Document.fromText(text, menuItem); + }); + await ai.index({ + indexer: devLocalIndexerRef('menu-items'), + documents, + }); + return { rows: menuItems.length }; + } +); + +// Define a flow which generates a response to the question, +// by retrieving relevant items from the menu. +// View this flow's trace to see the context that was retrieved, +// and how it was included in the prompt. + +export const s04_ragMenuQuestionFlow = ai.defineFlow( + { + name: 's04_ragMenuQuestion', + inputSchema: MenuQuestionInputSchema, + outputSchema: AnswerOutputSchema, + }, + async (input) => { + // Retrieve the 3 most relevant menu items for the question + const docs = await ai.retrieve({ + retriever: devLocalRetrieverRef('menu-items'), + query: input.question, + options: { k: 3 }, + }); + const menuData: Array = docs.map( + (doc) => (doc.metadata || {}) as MenuItem + ); + + // Generate the response + const response = await s04_ragDataMenuPrompt({ + menuData: menuData, + question: input.question, + }); + return { answer: response.text }; + } +); diff --git a/samples/js-menu/src/04/prompts.ts b/samples/js-menu/src/04/prompts.ts new file mode 100644 index 000000000..7ed500a5f --- /dev/null +++ b/samples/js-menu/src/04/prompts.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { ai } from '../genkit.js'; +import { DataMenuQuestionInputSchema } from '../types'; + +export const s04_ragDataMenuPrompt = ai.definePrompt( + { + name: 's04_ragDataMenu', + model: gemini15Flash, + input: { schema: DataMenuQuestionInputSchema }, + output: { format: 'text' }, + config: { temperature: 0.3 }, + }, + ` +You are acting as Walt, a helpful AI assistant here at the restaurant. +You can answer questions about the food on the menu or any other questions +customers have about food in general. + +Here are some items that are on today's menu that are relevant to +helping you answer the customer's question: +{{#each menuData~}} +- {{this.title}} \${{this.price}} + {{this.description}} +{{~/each}} + +Answer this customer's question: +{{question}}? +` +); diff --git a/samples/js-menu/src/05/example.visualMenuQuestion.json b/samples/js-menu/src/05/example.visualMenuQuestion.json new file mode 100644 index 000000000..3c49f36d0 --- /dev/null +++ b/samples/js-menu/src/05/example.visualMenuQuestion.json @@ -0,0 +1,3 @@ +{ + "question": "What kind of burger buns do you have?" +} diff --git a/samples/js-menu/src/05/flows.ts b/samples/js-menu/src/05/flows.ts new file mode 100644 index 000000000..f884c9181 --- /dev/null +++ b/samples/js-menu/src/05/flows.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import { z } from 'genkit'; +import path from 'path'; +import { ai } from '../genkit.js'; +import { + AnswerOutputSchema, + MenuQuestionInputSchema, + TextMenuQuestionInputSchema, +} from '../types'; +import { s05_readMenuPrompt, s05_textMenuPrompt } from './prompts'; + +// Define a flow that takes an image, passes it to Gemini Vision Pro, +// and extracts all of the text from the photo of the menu. +// Note that this example uses a hard-coded image file, as image input +// is not currently available in the Development UI runners. + +export const s05_readMenuFlow = ai.defineFlow( + { + name: 's05_readMenuFlow', + inputSchema: z.void(), // input is data/menu.jpeg + outputSchema: z.object({ menuText: z.string() }), + }, + async (unused) => { + const imageDataUrl = await inlineDataUrl('menu.jpeg', 'image/jpeg'); + const response = await s05_readMenuPrompt({ + imageUrl: imageDataUrl, + }); + return { menuText: response.text }; + } +); + +// Define a flow which generates a response to the question. +// Just returns the llm's text response to the question. + +export const s05_textMenuQuestionFlow = ai.defineFlow( + { + name: 's05_textMenuQuestion', + inputSchema: TextMenuQuestionInputSchema, + outputSchema: AnswerOutputSchema, + }, + async (input) => { + const response = await s05_textMenuPrompt({ + menuText: input.menuText, + question: input.question, + }); + return { answer: response.text }; + } +); + +// Define a third composite flow which chains the first two flows + +export const s05_visionMenuQuestionFlow = ai.defineFlow( + { + name: 's05_visionMenuQuestion', + inputSchema: MenuQuestionInputSchema, + outputSchema: AnswerOutputSchema, + }, + async (input) => { + // Run the first flow to read the menu image. + const menuResult = await s05_readMenuFlow(); + + // Pass the text of the menu and the question to the second flow + // and return the answer as this output. + return s05_textMenuQuestionFlow({ + question: input.question, + menuText: menuResult.menuText, + }); + } +); + +// Helper to read a local file and inline it as a data url + +async function inlineDataUrl( + imageFilename: string, + contentType: string +): Promise { + const filePath = path.join('./data', imageFilename); + const imageData = fs.readFileSync(filePath); + return `data:${contentType};base64,${imageData.toString('base64')}`; +} diff --git a/samples/js-menu/src/05/prompts.ts b/samples/js-menu/src/05/prompts.ts new file mode 100644 index 000000000..894edd931 --- /dev/null +++ b/samples/js-menu/src/05/prompts.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Flash } from '@genkit-ai/vertexai'; +import { z } from 'genkit'; +import { ai } from '../genkit.js'; +import { TextMenuQuestionInputSchema } from '../types'; + +export const s05_readMenuPrompt = ai.definePrompt( + { + name: 's05_readMenu', + model: gemini15Flash, + input: { + schema: z.object({ + imageUrl: z.string(), + }), + }, + output: { format: 'text' }, + config: { temperature: 0.1 }, + }, + ` +Extract _all_ of the text, in order, +from the following image of a restaurant menu. + +{{media url=imageUrl}} +` +); + +export const s05_textMenuPrompt = ai.definePrompt( + { + name: 's05_textMenu', + model: gemini15Flash, + input: { schema: TextMenuQuestionInputSchema }, + output: { format: 'text' }, + config: { temperature: 0.3 }, + }, + ` +You are acting as Walt, a helpful AI assistant here at the restaurant. +You can answer questions about the food on the menu or any other questions +customers have about food in general. + +Here is the text of today's menu to help you answer the customer's question: +{{menuText}} + +Answer this customer's question: +{{question}}? +` +); diff --git a/samples/js-menu/src/genkit.ts b/samples/js-menu/src/genkit.ts new file mode 100644 index 000000000..6109f4b2a --- /dev/null +++ b/samples/js-menu/src/genkit.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devLocalVectorstore } from '@genkit-ai/dev-local-vectorstore'; +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; +import { genkit } from 'genkit'; + +// Initialize Genkit + +export const ai = genkit({ + plugins: [ + vertexAI({ location: 'us-central1' }), + devLocalVectorstore([ + { + indexName: 'menu-items', + embedder: textEmbedding004, + embedderOptions: { taskType: 'RETRIEVAL_DOCUMENT' }, + }, + ]), + ], +}); diff --git a/samples/js-menu/src/index.ts b/samples/js-menu/src/index.ts new file mode 100644 index 000000000..5776c2268 --- /dev/null +++ b/samples/js-menu/src/index.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Export all of the example prompts and flows + +// 01 +export { s01_staticMenuDotPrompt, s01_vanillaPrompt } from './01/prompts'; +// 02 +export { s02_menuQuestionFlow } from './02/flows'; +export { s02_dataMenuPrompt } from './02/prompts'; +// 03 +export { s03_multiTurnChatFlow } from './03/flows'; +export { s03_chatPreamblePrompt } from './03/prompts'; +// 04 +export { s04_indexMenuItemsFlow, s04_ragMenuQuestionFlow } from './04/flows'; +export { s04_ragDataMenuPrompt } from './04/prompts'; +// 05 +export { + s05_readMenuFlow, + s05_textMenuQuestionFlow, + s05_visionMenuQuestionFlow, +} from './05/flows'; +export { s05_readMenuPrompt, s05_textMenuPrompt } from './05/prompts'; diff --git a/samples/js-menu/src/types.ts b/samples/js-menu/src/types.ts new file mode 100644 index 000000000..efe7bbc99 --- /dev/null +++ b/samples/js-menu/src/types.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as z from 'zod'; + +// The data model for a restaurant menu + +export const MenuItemSchema = z.object({ + title: z.string().describe('The name of the menu item'), + description: z + .string() + .describe('Details including ingredients and preparation'), + price: z.number().describe('Price in dollars'), +}); + +export type MenuItem = z.infer; + +// Input schema for a question about the menu + +export const MenuQuestionInputSchema = z.object({ + question: z.string(), +}); + +// Output schema containing an answer to a question + +export const AnswerOutputSchema = z.object({ + answer: z.string(), +}); + +// Input schema for a question about the menu +// where the menu is provided in JSON data. + +export const DataMenuQuestionInputSchema = z.object({ + menuData: z.array(MenuItemSchema), + question: z.string(), +}); + +// Input schema for a question about the menu +// where the menu is provided as unstructured text. + +export const TextMenuQuestionInputSchema = z.object({ + menuText: z.string(), + question: z.string(), +}); + +// Also export Typescript types for each of these Zod schemas +export type MenuQuestionInput = z.infer; +export type AnswerOutput = z.infer; +export type DataMenuPromptInput = z.infer; +export type TextMenuQuestionInput = z.infer; diff --git a/samples/js-menu/tsconfig.json b/samples/js-menu/tsconfig.json new file mode 100644 index 000000000..e51f33ae3 --- /dev/null +++ b/samples/js-menu/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "noImplicitReturns": true, + "noUnusedLocals": false, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + }, + "compileOnSave": true, + "include": ["src"] +} diff --git a/samples/js-schoolAgent/README.md b/samples/js-schoolAgent/README.md new file mode 100644 index 000000000..35d91f3ab --- /dev/null +++ b/samples/js-schoolAgent/README.md @@ -0,0 +1,107 @@ +# School Agent Sample + +A demonstration of a conversational, multi-agent assistant for a school system using GenKit and Google's Gemini Pro. This agent helps parents with attendance reporting and school information queries. + +In this example we have a RoutingAgent which is the main, general-purpose agent. +This agent comes equipped with additional specialized agents, that it can hand-off to as needed. + +These specialized agents are represented as prompts and embedded as tools to the original agent. + +## Agent Tools & Capabilities + +- **Agent Structure**: + - `RoutingAgent`: Main entry point and router, handling general queries and delegating to specialized agents + - `AttendanceAgent`: Specialized agent for absence/tardy reporting + - `GradesAgent`: Manages grade-related inquiries and academic performance + +Each specialized agent has its own set of tools that are only accessible to that specific agent: + +- **AttendanceAgent**: + - `reportAbsence`: Submit absence notifications + - `reportTardy`: Report late arrivals +- **GradesAgent**: + - `getRecentGrades`: Retrieve latest grade information + +The main RoutingAgent cannot directly access these specialized tools - it can only access its own tools and delegate to the specialized agents. This means the specialized agent descriptions need to clearly communicate their capabilities, since the main agent relies on these descriptions for appropriate routing. + +For example, when the RoutingAgent sees a grade-related query, it needs to know from the GradesAgent's description that it can handle grade lookups, even though it can't directly see the `getRecentGrades` tool. + +This architectural pattern: + +- Maintains clear separation of concerns +- Allows specialized agents to evolve independently +- Allows scaling up to a larger number of tools + +NOTE: The agent description is how the generalized agent knows what tools the specialized agent has available. An agent description that is too general may cause the routing agent to mess up by not knowing that a certain functionality was actually available. + +## Prerequisites + +- Node.js and genkit CLI installed +- Google AI API key + +## Getting Started + +1. Install dependencies: + +```bash +npm install +``` + +2. Set up your Google AI API key: + +```bash +export GOOGLE_GENAI_API_KEY=your_api_key_here +``` + +3. Start the development server: + +```bash +npm run genkit:dev +``` + +In your terminal, a commandline chat interface should show up: + +``` +Telemetry API running on http://localhost:4033 +Genkit Developer UI: http://localhost:4000 + +> school-agent@1.0.0 dev +> tsx --no-warnings --watch src/terminal.ts + +bell> Hi there, my name is Bell and I'm here to help! 👋🎉 I'm your friendly AI assistant for parents of Sparkyville High School. I can answer your questions about the school, events, grades, and more. Just ask me! 😊 + +prompt> [insert your chats here] +``` + +You can feel free to tweak the sample. The project builds in watch mode, so any changes will be picked up immediately and should restart the conversation. + +## Usage + +The agent uses a multi-agent architecture: + +- Routing Agent: Acts as the main entry point and router, handling general queries while delegating specialized requests to appropriate agents +- Attendance Agent: Specialized agent focused on absence and tardy reporting +- Grades Agent: Manages academic performance queries, grade reports, and transcript requests + +Example queries: + +- "Evelyn will be late today" +- "What are the upcoming holidays I should be aware of?" +- "Show me my child's current grades" + +## Development + +- `npm run dev` - Run in development mode with hot reloading +- `npm run build` - Build the project +- `npm start` - Run the built version + +## Project Structure + +- `src/` + - `agents/` + - `routingAgent.ts` - Main agent that uses other agents as tools + - `attendanceAgent.ts` - Specialized attendance agent + - `gradesAgent.ts` - Academic performance and grades agent + - `tools.ts` - Tool definitions + - `types.ts` - TypeScript types + - `data.ts` - Sample data diff --git a/samples/js-schoolAgent/package.json b/samples/js-schoolAgent/package.json new file mode 100644 index 000000000..3ebc1299b --- /dev/null +++ b/samples/js-schoolAgent/package.json @@ -0,0 +1,33 @@ +{ + "name": "school-agent", + "version": "1.0.0", + "description": "", + "main": "lib/index.js", + "scripts": { + "start": "node lib/terminal.js", + "dev": "tsx --no-warnings --watch src/terminal.ts", + "genkit:dev": "genkit start -- npm run dev", + "compile": "tsc", + "build": "pnpm build:clean && npm run compile", + "build:clean": "rimraf ./lib", + "build:watch": "tsc --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.3", + "@genkit-ai/googleai": "^0.9.3", + "google-auth-library": "^9.6.3", + "llm-chunk": "^0.0.1", + "pdf-parse": "^1.1.1" + }, + "devDependencies": { + "genkit-cli": "^0.9.3", + "@types/pdf-parse": "^1.1.4", + "cross-env": "^7.0.3", + "rimraf": "^6.0.1", + "tsx": "^4.19.1", + "typescript": "^5.3.3" + } +} diff --git a/samples/js-schoolAgent/prompts/myPrompt.prompt b/samples/js-schoolAgent/prompts/myPrompt.prompt new file mode 100644 index 000000000..c6ffa8db4 --- /dev/null +++ b/samples/js-schoolAgent/prompts/myPrompt.prompt @@ -0,0 +1 @@ +{{ role "system" }} your name is {{ @state.userName }}, always introduce yourself) \ No newline at end of file diff --git a/samples/js-schoolAgent/src/attendanceAgent.ts b/samples/js-schoolAgent/src/attendanceAgent.ts new file mode 100644 index 000000000..044aefd7c --- /dev/null +++ b/samples/js-schoolAgent/src/attendanceAgent.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ai } from './genkit.js'; +import { reportAbsence, reportTardy } from './tools.js'; +import { agentDescription } from './util.js'; + +const tools = [reportAbsence, reportTardy, 'routingAgent']; +const specialization = 'attendance'; + +const toolNames: string[] = tools.map((item) => { + if (typeof item === 'string') { + return item; + } else { + return item.name; + } +}); + +export const attendanceAgent = ai.definePrompt( + { + name: `${specialization}Agent`, + description: agentDescription(specialization, toolNames), + tools, + }, + ` {{ role "system"}} + +You are Bell, a helpful attendance assistance agent for Sparkyville High School. +A parent has been referred to you to handle a ${specialization}-related concern. +Use the tools available to you to assist the parent. + +- Parents may only report absences for their own students. +- If you are unclear about any of the fields required to report an absence or tardy, request clarification before using the tool. +- If the parent asks about anything other than attendance-related concerns that you can handle, transfer to the info agent. + + {{ userContext @state }} + ` +); diff --git a/samples/js-schoolAgent/src/data.ts b/samples/js-schoolAgent/src/data.ts new file mode 100644 index 000000000..b44268d76 --- /dev/null +++ b/samples/js-schoolAgent/src/data.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export async function getUpcomingHolidays() { + return [ + { date: '2024-11-28', holiday: 'Thanksgiving Break' }, + { date: '2024-11-29', holiday: 'Thanksgiving Break (Day 2)' }, + ]; +} + +export const EXAMPLE_EVENTS = [ + { + event: 'Freshman Fall Concert', + activity: 'Choir', + location: 'School Auditorium', + startTime: '2024-11-12T19:00', + endTime: '2023-11-12T20:30', + grades: [9], + }, + { + event: 'Fall Pep Rally', + location: 'Football Field', + startTime: '2024-10-27T14:00', + endTime: '2024-10-27T15:30', + grades: [9, 10, 11, 12], + }, + { + event: 'Junior Fall Concert', + activity: 'Choir', + location: 'School Auditorium', + startTime: '2024-11-15T19:00', + endTime: '2024-11-15T20:30', + grades: [11], + }, + { + event: 'Varsity Chess Club Tournament', + activity: 'Chess Club', + location: 'Library', + startTime: '2024-11-04T16:00', + endTime: '2024-11-04T18:00', + grades: [11, 12], + }, + { + event: 'Drama Club Auditions', + activity: 'Drama Club', + location: 'School Auditorium', + startTime: '2024-10-20T15:00', + endTime: '2024-10-20T17:00', + grades: [9, 10, 11, 12], + }, +]; + +export interface GradeEntry { + studentId: number; + subject: string; + grade: string; + date: string; + assignment: string; +} + +export const EXAMPLE_GRADES: GradeEntry[] = [ + { + studentId: 3734, + subject: 'Mathematics', + grade: 'A-', + date: '2024-03-01', + assignment: 'Quadratic Equations Quiz', + }, + { + studentId: 3734, + subject: 'English', + grade: 'B+', + date: '2024-03-05', + assignment: 'Essay: Shakespeare Analysis', + }, + { + studentId: 9433, + subject: 'Physics', + grade: 'A', + date: '2024-03-02', + assignment: 'Forces Lab Report', + }, +]; diff --git a/samples/js-schoolAgent/src/genkit.ts b/samples/js-schoolAgent/src/genkit.ts new file mode 100644 index 000000000..025c4b30d --- /dev/null +++ b/samples/js-schoolAgent/src/genkit.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gemini15Pro, googleAI } from '@genkit-ai/googleai'; +import { genkit } from 'genkit'; +import { AgentState } from './types'; + +export const ai = genkit({ + plugins: [googleAI()], + model: gemini15Pro, +}); + +ai.defineHelper( + 'userContext', + (state: AgentState) => `=== User Context + +- The current parent user is ${state?.parentName} +- The current date and time is: ${new Date().toString()} + +=== Registered students of the current user + +${state?.students.map((s) => ` - ${s.name}, student id: ${s.id} grade: ${s.grade}, activities: \n${s.activities.map((a) => ` - ${a}`).join('\n')}`).join('\n\n')}` +); + +export { z } from 'genkit'; diff --git a/samples/js-schoolAgent/src/gradesAgent.ts b/samples/js-schoolAgent/src/gradesAgent.ts new file mode 100644 index 000000000..58871fc52 --- /dev/null +++ b/samples/js-schoolAgent/src/gradesAgent.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ai } from './genkit.js'; +import { getRecentGrades } from './tools.js'; +import { agentDescription } from './util.js'; + +const tools = [getRecentGrades, 'routingAgent']; +const specialization = 'grades'; + +const toolNames: string[] = tools.map((item) => { + if (typeof item === 'string') { + return item; + } else { + return item.name; + } +}); + +export const gradesAgent = ai.definePrompt( + { + name: `${specialization}Agent`, + description: agentDescription(specialization, toolNames), + tools, + }, + ` {{ role "system"}} + +You are Bell, a helpful attendance assistance agent for Sparkyville High School. +A parent has been referred to you to handle a ${specialization}-related concern. +Use the tools available to you to assist the parent. + +Guidelines: +- Parents may only view grades for their own students +- Always verify the student belongs to the parent before sharing grade information +- Be encouraging and positive when discussing grades +- If asked about non-grade related topics, transfer back to the info agent +- Format grade information in a clear, easy-to-read manner + +{{ userContext @state }} + ` +); diff --git a/samples/js-schoolAgent/src/routingAgent.ts b/samples/js-schoolAgent/src/routingAgent.ts new file mode 100644 index 000000000..3986baa4f --- /dev/null +++ b/samples/js-schoolAgent/src/routingAgent.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { attendanceAgent } from './attendanceAgent'; +import { ai } from './genkit'; +import { gradesAgent } from './gradesAgent'; +import { searchEvents, upcomingHolidays } from './tools.js'; + +export const routingAgent = ai.definePrompt( + { + name: 'routingAgent', + description: `This agent helps with answering inquiries and requests.`, + tools: [searchEvents, attendanceAgent, gradesAgent, upcomingHolidays], + }, + `You are Bell, the friendly AI office receptionist at Sparkyville High School. + + Your job is to help answer inquiries from parents. Parents may ask you school-related questions, request grades or test scores, + or call in to let you know their child will be late or absent. + + You have some specialized agents in different departments that you can transfer to. + + 1. Grades Agent - This agent can provide informtion about previous scores for assignments and tests. + 2. Attendance Agent - This agent can help with attendance requests, such as marking a student as late/tardy or absent. + + Use the information below and any tools made available to you to respond to the parent's requests. + + If the parent has an inquiry that you do not know the answer to, do NOT make the answer up. Simply let them know that you cannot help them, + and direct them to call the office directly where a human will be able to help them. + + === Frequently Asked Questions + + - Classes begin at 8am, students are dismissed at 3:30pm + - Parking permits are issued on a first-come first-serve basis beginning Aug 1 + + {{userContext @state }} +` +); diff --git a/samples/js-schoolAgent/src/terminal.ts b/samples/js-schoolAgent/src/terminal.ts new file mode 100644 index 000000000..9b3717449 --- /dev/null +++ b/samples/js-schoolAgent/src/terminal.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Message, ToolRequestPart } from 'genkit'; +import { createInterface } from 'node:readline'; +import { ai } from './genkit.js'; +import { routingAgent } from './routingAgent.js'; + +const rl = createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const EXAMPLE_USER_CONTEXT = { + parentId: 4112, + parentName: 'Francis Smith', + students: [ + { + id: 3734, + name: 'Evelyn Smith', + grade: 9, + activities: ['Choir', 'Drama Club'], + }, + { id: 9433, name: 'Evan Smith', grade: 11, activities: ['Chess Club'] }, + ], +}; + +// ANSI color codes for terminal output +const COLORS = { + BELL: '\x1b[33m', + PROMPT: '\x1b[36m', + RESET: '\x1b[0m', +}; + +// Helper to print colored text +function printColored(prefix: string, text: string, color: string) { + console.log(`${color}${prefix}>${COLORS.RESET}`, text); +} + +// Get initial greeting from AI +async function getGreeting() { + const { text } = await ai.generate( + 'Come up with a short friendly greeting for yourself talking to a parent as Bell, the helpful AI assistant for parents of Sparkyville High School. Feel free to use emoji.' + ); + return text; +} + +// Process and display the chat response stream +async function handleChatResponse( + stream: AsyncIterable<{ text: string }>, + response: Promise, + startMessageCount: number +) { + console.log(); + process.stdout.write(`${COLORS.BELL}bell>${COLORS.RESET} `); + + for await (const chunk of stream) { + process.stdout.write(chunk.text); + } + + // Extract and display tools used + const toolsUsed = (await response).messages + .slice(startMessageCount) + .filter((m: Message) => m.role === 'model') + .map((m: Message) => + m.content + .filter((p) => !!p.toolRequest) + .map( + (p) => + `${p.toolRequest?.name}(${JSON.stringify(p.toolRequest?.input)})` + ) + ) + .flat() + .filter((t: ToolRequestPart) => !!t); + + console.log('\nTools Used:', toolsUsed); +} + +// Main chat loop +async function handleUserInput(chat: any): Promise { + return new Promise((resolve) => { + rl.question(`\n${COLORS.PROMPT}prompt>${COLORS.RESET} `, async (input) => { + try { + const startMessageCount = chat.messages.length; + const { stream, response } = await chat.sendStream(input); + await handleChatResponse(stream, response, startMessageCount); + resolve(); + } catch (e) { + console.log('Error:', e); + resolve(); + } + }); + }); +} + +async function main() { + const chat = ai + .createSession({ initialState: EXAMPLE_USER_CONTEXT }) + .chat(routingAgent); + + const greeting = await getGreeting(); + console.log(); + printColored('bell', greeting, COLORS.BELL); + + while (true) { + await handleUserInput(chat); + } +} + +setTimeout(main, 0); diff --git a/samples/js-schoolAgent/src/tools.ts b/samples/js-schoolAgent/src/tools.ts new file mode 100644 index 000000000..b0e3b848b --- /dev/null +++ b/samples/js-schoolAgent/src/tools.ts @@ -0,0 +1,147 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EXAMPLE_EVENTS, EXAMPLE_GRADES, getUpcomingHolidays } from './data.js'; +import { ai, z } from './genkit.js'; +import { AgentState } from './types.js'; + +export const searchEvents = ai.defineTool( + { + name: 'searchEvents', + description: + 'use this when asked about any time/location for school events including extra curriculars like clubs', + inputSchema: z.object({ + activity: z + .string() + .optional() + .describe( + 'if looking for a particular activity, provide it here. must be an exact match for an activity name' + ), + grade: z + .number() + .optional() + .describe('restrict searched events to a particular grade level'), + }), + }, + async ({ activity, grade }) => { + return EXAMPLE_EVENTS.filter( + (e) => !grade || e.grades.includes(grade) + ).filter( + (e) => !activity || e.activity?.toLowerCase() === activity?.toLowerCase() + ); + } +); + +function checkIsParent(studentId: number, state: AgentState) { + const student = state.students.find((s) => s.id === studentId); + if (!student) { + throw new Error( + `Unable to process request for student ID ${studentId}. Parents can only submit requests for their registered children.` + ); + } + return student; +} + +export const reportAbsence = ai.defineTool( + { + name: 'reportAbsence', + description: + 'use this tool to mark a specific student as absent on one or more days', + inputSchema: z.object({ + studentId: z.number().describe('the id of the student'), + date: z.string().describe('the date of the absence in YYYY-MM-DD format'), + reason: z.string().describe('the provided reason for the absence'), + excused: z + .boolean() + .describe('whether the absence is excused by the parent'), + }), + }, + async (input) => { + const student = checkIsParent( + input.studentId, + ai.currentSession().state! + ); + console.log( + `[TOOL] Absence reported for ${student.name} (ID: ${input.studentId}) on ${input.date}` + ); + return { ok: true, message: 'Absence successfully recorded' }; + } +); + +export const reportTardy = ai.defineTool( + { + name: 'reportTardy', + description: + 'use this tool to mark a specific student tardy for a given date', + inputSchema: z.object({ + studentId: z.number().describe('the id of the student'), + date: z.string().describe('the date of the tardy'), + reason: z.string().describe('the provided reason reason for the tardy'), + eta: z + .string() + .describe( + 'the time the student is expected to arrive at school in HH:MMam/pm format' + ), + excused: z + .boolean() + .describe('whether the absense is excused by the parent'), + }), + }, + async (input) => { + checkIsParent(input.studentId, ai.currentSession().state!); + console.log( + '[TOOL] Student', + input.studentId, + 'has been reported tardy for', + input.date + ); + return { ok: true }; + } +); + +export const upcomingHolidays = ai.defineTool( + { + name: 'upcomingHolidays', + description: 'can retrieve information about upcoming holidays', + outputSchema: z.string(), + }, + async () => JSON.stringify(await getUpcomingHolidays()) +); + +export const getRecentGrades = ai.defineTool( + { + name: 'getRecentGrades', + description: 'retrieves recent grades for a specific student', + inputSchema: z.object({ + studentId: z.number().describe('the id of the student'), + subject: z.string().optional().describe('optional subject filter'), + limit: z + .number() + .optional() + .describe('number of recent grades to return'), + }), + }, + async ({ studentId, subject, limit = 5 }) => { + checkIsParent(studentId, ai.currentSession().state!); + let grades = EXAMPLE_GRADES.filter((g) => g.studentId === studentId); + if (subject) { + grades = grades.filter( + (g) => g.subject.toLowerCase() === subject.toLowerCase() + ); + } + return grades.slice(0, limit); + } +); diff --git a/samples/js-schoolAgent/src/types.ts b/samples/js-schoolAgent/src/types.ts new file mode 100644 index 000000000..542b2351f --- /dev/null +++ b/samples/js-schoolAgent/src/types.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface AgentState { + parentId: number; + parentName: string; + students: { + id: number; + name: string; + grade: number; + activities: string[]; + }[]; +} diff --git a/samples/js-schoolAgent/src/util.ts b/samples/js-schoolAgent/src/util.ts new file mode 100644 index 000000000..e66f628fe --- /dev/null +++ b/samples/js-schoolAgent/src/util.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const agentDescription = (specialization: string, tools: string[]) => ` +Transfer to this agent when the user asks about ${specialization}. +This agent can perform the following functions: ${tools.map((t) => t).join(', ')}. +Do not mention that you are transferring, just do it.`; diff --git a/samples/js-schoolAgent/tsconfig.json b/samples/js-schoolAgent/tsconfig.json new file mode 100644 index 000000000..b73ccd04d --- /dev/null +++ b/samples/js-schoolAgent/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noImplicitReturns": true, + "noUnusedLocals": false, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + }, + "compileOnSave": true, + "include": ["src"] +} diff --git a/samples/prompts/README.md b/samples/prompts/README.md new file mode 100644 index 000000000..35f1a1735 --- /dev/null +++ b/samples/prompts/README.md @@ -0,0 +1,9 @@ +# Prompts Samples + +These sample shows off several of the prompting techniques show in [docs/prompts.md](/docs/prompts.md) + +These examples use Google Gemini, set your API key in the `GOOGLE_GENAI_API_KEY` environment variable. + +You can run these prompts in the Developer UI with `genkit start` + +or invoke them with e.g. `genkit flow:run simplePrompt` diff --git a/samples/prompts/package.json b/samples/prompts/package.json new file mode 100644 index 000000000..081931c36 --- /dev/null +++ b/samples/prompts/package.json @@ -0,0 +1,26 @@ +{ + "name": "prompts", + "version": "1.0.0", + "description": "", + "main": "lib/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node lib/index.js", + "genkit:dev": "genkit start -- tsx --watch src/index.ts", + "build": "tsc", + "build:watch": "tsc --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "genkit": "^0.9.0-rc || ^0.9", + "@genkit-ai/googleai": "^0.9.0-rc || ^0.9", + "express": "^4.21.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "genkit-cli": "^0.9.0-rc || ^0.9", + "typescript": "^5.5.4" + } +} diff --git a/samples/prompts/src/index.ts b/samples/prompts/src/index.ts new file mode 100644 index 000000000..9149c0358 --- /dev/null +++ b/samples/prompts/src/index.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Import the Genkit core libraries and plugins. +import { googleAI } from '@genkit-ai/googleai'; +import { genkit, z } from 'genkit'; + +const ai = genkit({ + plugins: [ + googleAI(), // Provide the key via the GOOGLE_GENAI_API_KEY environment variable or arg { apiKey: 'yourkey'} + ], +}); + +const simplePrompt = ai.defineFlow('simplePrompt', () => + ai.generate({ + model: 'googleai/gemini-1.5-flash', + prompt: 'You are a helpful AI assistant named Walt, say hello', + }) +); + +const simpleTemplate = ai.defineFlow('simpleTemplate', () => { + const name = 'Fred'; + return ai.generate({ + model: 'googleai/gemini-1.5-flash', + prompt: `You are a helpful AI assistant named Walt. Say hello to ${name}.`, + }); +}); + +const helloDotprompt = ai.definePrompt( + { + name: 'helloPrompt', + model: 'googleai/gemini-1.5-flash', + input: { + schema: z.object({ name: z.string() }), + }, + }, + `You are a helpful AI assistant named Walt. Say hello to {{name}}` +); + +const simpleDotprompt = ai.defineFlow('simpleDotprompt', () => { + return helloDotprompt({ name: 'Fred' }); +}); + +const outputSchema = z.object({ + short: z.string(), + friendly: z.string(), + likeAPirate: z.string(), +}); + +const threeGreetingsPrompt = ai.definePrompt( + { + name: 'threeGreetingsPrompt', + model: 'googleai/gemini-1.5-flash', + input: { + schema: z.object({ name: z.string() }), + }, + output: { + format: 'json', + schema: outputSchema, + }, + }, + `You are a helpful AI assistant named Walt. Say hello to {{name}}, write a response for each of the styles requested` +); + +const threeGreetings = ai.defineFlow('threeGreetingsPrompt', async () => { + const response = await threeGreetingsPrompt({ name: 'Fred' }); + return response.output?.likeAPirate; +}); + +// Start a flow server, which exposes your flows as HTTP endpoints. This call +// must come last, after all of your plug-in configuration and flow definitions. +// You can optionally specify a subset of flows to serve, and configure some +// HTTP server options, but by default, the flow server serves all defined flows. +ai.startFlowServer({ + flows: [threeGreetings, simpleTemplate, simpleDotprompt, simplePrompt], +}); diff --git a/samples/prompts/tsconfig.json b/samples/prompts/tsconfig.json new file mode 100644 index 000000000..efbb566bf --- /dev/null +++ b/samples/prompts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "include": ["src"], + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + } +}