diff --git a/js/testapps/flow-simple-ai/src/index.ts b/js/testapps/flow-simple-ai/src/index.ts
index 71f0bcb2f8..ac6ccff94d 100644
--- a/js/testapps/flow-simple-ai/src/index.ts
+++ b/js/testapps/flow-simple-ai/src/index.ts
@@ -22,7 +22,12 @@ import { defineFirestoreRetriever, firebase } from '@genkit-ai/firebase';
import { defineFlow, run } from '@genkit-ai/flow';
import { googleCloud } from '@genkit-ai/google-cloud';
import { googleAI, geminiPro as googleGeminiPro } from '@genkit-ai/googleai';
-import { geminiPro, textEmbeddingGecko, vertexAI } from '@genkit-ai/vertexai';
+import {
+ gemini15ProPreview,
+ geminiPro,
+ textEmbeddingGecko,
+ vertexAI,
+} from '@genkit-ai/vertexai';
import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
@@ -331,3 +336,40 @@ export const dotpromptContext = defineFlow(
return result.output() as any;
}
);
+
+const jokeSubjectGenerator = defineTool(
+ {
+ name: 'jokeSubjectGenerator',
+ description: 'can be called to generate a subject for a joke',
+ },
+ async () => {
+ return 'banana';
+ }
+);
+
+export const toolCaller = defineFlow(
+ {
+ name: 'toolCaller',
+ outputSchema: z.string(),
+ },
+ async (_, streamingCallback) => {
+ if (!streamingCallback) {
+ throw new Error('this flow only works in streaming mode');
+ }
+
+ const { response, stream } = await generateStream({
+ model: gemini15ProPreview,
+ config: {
+ temperature: 1,
+ },
+ tools: [jokeSubjectGenerator],
+ prompt: `tell me a joke`,
+ });
+
+ for await (const chunk of stream()) {
+ streamingCallback(chunk);
+ }
+
+ return (await response()).text();
+ }
+);
diff --git a/samples/js-angular/.gitignore b/samples/js-angular/.gitignore
new file mode 100644
index 0000000000..7951405f85
--- /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 0000000000..4e02b6f321
--- /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 0000000000..59d9a3a3e7
--- /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 0000000000..cc7b141350
--- /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 0000000000..0aeb2095bb
--- /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 0000000000..8762ae48e4
--- /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 0000000000..dc067a06f7
--- /dev/null
+++ b/samples/js-angular/genkit-app/package.json
@@ -0,0 +1,40 @@
+{
+ "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"
+ },
+ "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 0000000000..57614f9c96
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 0000000000..0165f5b0fd
--- /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 0000000000..0e0b805578
--- /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 0000000000..02dac7e71e
--- /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 0000000000..39d4f4ea0e
--- /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 0000000000..3d04dfa9c2
--- /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 0000000000..bd70f607fa
--- /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 0000000000..f57987dcc3
--- /dev/null
+++ b/samples/js-angular/genkit-app/src/app/home/home.component.html
@@ -0,0 +1,32 @@
+
+
+
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 0000000000..da80fa8b3e
--- /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 0000000000..19eda49ae5
--- /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 0000000000..f1e1997c12
--- /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 0000000000..00829ec6db
--- /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 }}
+
+
+
+
+
+ Choose a date
+
+ MM/DD/YYYY
+
+
+
+ Cancel
+
+ Apply
+
+
+
+
+
+
+ Oops... unknown tool {{ entry.toolRequest.name }}
+
+
+
+
+
+
+
+
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 0000000000..da62c1c123
--- /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 0000000000..c79a6e1f7b
--- /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 0000000000..02012c7f02
--- /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 '../../../utils/flow';
+
+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,
+ payload: {
+ 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 0000000000..e32ce2ae00
--- /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?
+
+
Generate
+
+
Loading...
+
+ {{ error }}
+
+
+
+
{{ character.name }}
+
+
+
+
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 0000000000..70fcce28eb
--- /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 0000000000..d853731059
--- /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 0000000000..fc1a0f8da8
--- /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 '../../../utils/flow';
+
+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,
+ payload: 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 0000000000..822a28173d
--- /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 0000000000..b1be530a21
--- /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 0000000000..0ff0b59e1b
--- /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/src/utils/flow.ts b/samples/js-angular/genkit-app/src/utils/flow.ts
new file mode 100644
index 0000000000..3aabbaa23e
--- /dev/null
+++ b/samples/js-angular/genkit-app/src/utils/flow.ts
@@ -0,0 +1,150 @@
+/**
+ * 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.
+ */
+
+const __flowStreamDelimiter = '\n';
+
+export async function runFlow({
+ url,
+ payload,
+ headers,
+}: {
+ url: string;
+ payload?: any;
+ headers?: Record;
+}) {
+ const response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ data: payload,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ ...headers,
+ },
+ });
+ const wrappedDesult = await response.json();
+ return wrappedDesult.result;
+}
+
+export function streamFlow({
+ url,
+ payload,
+ headers,
+}: {
+ url: string;
+ payload?: any;
+ headers?: Record;
+}) {
+ let chunkStreamController: ReadableStreamDefaultController | undefined =
+ undefined;
+ const chunkStream = new ReadableStream({
+ start(controller) {
+ chunkStreamController = controller;
+ },
+ pull() {},
+ cancel() {},
+ });
+
+ const operationPromise = __flowRunEnvelope({
+ url,
+ payload,
+ streamingCallback: (c) => {
+ chunkStreamController?.enqueue(c);
+ },
+ headers,
+ });
+ operationPromise.then((o) => {
+ chunkStreamController?.close();
+ return o;
+ });
+
+ return {
+ output() {
+ return operationPromise.then((op) => {
+ if (!op.done) {
+ throw new Error(`flow ${op.name} did not finish execution`);
+ }
+ if (op.result?.error) {
+ throw new Error(op.name, op.result?.error + op.result?.stacktrace);
+ }
+ return op.result?.response;
+ });
+ },
+ async *stream() {
+ const reader = chunkStream.getReader();
+ while (true) {
+ const chunk = await reader.read();
+ if (chunk.value) {
+ yield chunk.value;
+ }
+ if (chunk.done) {
+ break;
+ }
+ }
+ return await operationPromise;
+ },
+ };
+}
+
+async function __flowRunEnvelope({
+ url,
+ payload,
+ streamingCallback,
+ headers,
+}: {
+ url: string;
+ payload?: any;
+ streamingCallback: (chunk: any) => void;
+ headers?: Record;
+}) {
+ let response;
+ response = await fetch(url + '?stream=true', {
+ method: 'POST',
+ body: JSON.stringify({
+ data: payload,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ ...headers,
+ },
+ });
+ if (!response.body) {
+ throw new Error('Response body is empty');
+ }
+ var reader = response.body.getReader();
+ var decoder = new TextDecoder();
+
+ let buffer = '';
+ while (true) {
+ const result = await reader.read();
+ const decodedValue = decoder.decode(result.value);
+ if (decodedValue) {
+ buffer += decodedValue;
+ }
+ // If buffer includes the delimiter that means we are still recieving chunks.
+ while (buffer.includes(__flowStreamDelimiter)) {
+ streamingCallback(
+ JSON.parse(buffer.substring(0, buffer.indexOf(__flowStreamDelimiter)))
+ );
+ buffer = buffer.substring(
+ buffer.indexOf(__flowStreamDelimiter) + __flowStreamDelimiter.length
+ );
+ }
+ if (result.done) {
+ return JSON.parse(buffer);
+ }
+ }
+}
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 0000000000..84f1f992d2
--- /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 0000000000..437984834c
--- /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 0000000000..47e3dd7551
--- /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 0000000000..3a6fc3a807
--- /dev/null
+++ b/samples/js-angular/package.json
@@ -0,0 +1,22 @@
+{
+ "scripts": {
+ "start": "concurrently npm:start:server npm:start:ng",
+ "start:server": "cd server && genkit start",
+ "start:ng": "cd genkit-app && npm run start",
+ "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",
+ "devDependencies": {
+ "genkit": "^0.5.2",
+ "@angular/cli": "^18.0.2",
+ "concurrently": "^8.2.2",
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/samples/js-angular/server/package.json b/samples/js-angular/server/package.json
new file mode 100644
index 0000000000..855136c3d9
--- /dev/null
+++ b/samples/js-angular/server/package.json
@@ -0,0 +1,28 @@
+{
+ "main": "lib/index.js",
+ "scripts": {
+ "start": "genkit start",
+ "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-ai/ai": "^0.5.2",
+ "@genkit-ai/core": "^0.5.2",
+ "@genkit-ai/dotprompt": "^0.5.2",
+ "@genkit-ai/flow": "^0.5.2",
+ "@genkit-ai/vertexai": "^0.5.2",
+ "express": "^4.19.2",
+ "partial-json": "^0.1.7",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/samples/js-angular/server/src/agent.ts b/samples/js-angular/server/src/agent.ts
new file mode 100644
index 0000000000..4fa9b964f0
--- /dev/null
+++ b/samples/js-angular/server/src/agent.ts
@@ -0,0 +1,109 @@
+/**
+ * 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, generate } from '@genkit-ai/ai';
+import {
+ GenerateResponseSchema,
+ MessageData,
+ ModelArgument,
+ PartSchema,
+} from '@genkit-ai/ai/model';
+import { ToolArgument } from '@genkit-ai/ai/tool';
+import { defineFlow, run } from '@genkit-ai/flow';
+import { z } from 'zod';
+
+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(
+ {
+ name,
+ tools,
+ model,
+ historyStore,
+ systemPrompt,
+ returnToolRequests,
+ }: {
+ name: string;
+ systemPrompt?: string;
+ tools?: ToolArgument[];
+ model: ModelArgument;
+ historyStore?: HistoryStore;
+ returnToolRequests?: boolean;
+ },
+ customFn?: AgentFn
+) {
+ return 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 generate({
+ prompt: request.prompt,
+ history,
+ model,
+ tools,
+ returnToolRequests,
+ streamingCallback,
+ });
+ await run(
+ 'save-history',
+ { conversationId: request.conversationId, history: resp.toHistory() },
+ async () => {
+ request.conversationId
+ ? await historyStore?.save(request.conversationId, resp.toHistory())
+ : 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 0000000000..af4c04599c
--- /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 { defineTool } from '@genkit-ai/ai';
+import { MessageData } from '@genkit-ai/ai/model';
+import { gemini15FlashPreview } from '@genkit-ai/vertexai';
+import { z } from 'zod';
+import { HistoryStore, defineAgent } from './agent';
+
+const weatherTool = 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 = 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({
+ name: 'chatbotFlow',
+ model: gemini15FlashPreview,
+ 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/index.ts b/samples/js-angular/server/src/index.ts
new file mode 100644
index 0000000000..615a6185ec
--- /dev/null
+++ b/samples/js-angular/server/src/index.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 { configureGenkit } from '@genkit-ai/core';
+import { startFlowsServer } from '@genkit-ai/flow';
+import { vertexAI } from '@genkit-ai/vertexai';
+
+configureGenkit({
+ plugins: [vertexAI()],
+ logLevel: 'debug',
+ enableTracingAndMetrics: true,
+});
+
+export * from './chatbot.js';
+export * from './jsonStreaming.js';
+
+startFlowsServer();
diff --git a/samples/js-angular/server/src/jsonStreaming.ts b/samples/js-angular/server/src/jsonStreaming.ts
new file mode 100644
index 0000000000..e75c051db5
--- /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 { generateStream } from '@genkit-ai/ai';
+import { defineFlow } from '@genkit-ai/flow';
+import { gemini15ProPreview } from '@genkit-ai/vertexai';
+import { Allow, parse } from 'partial-json';
+import { z } from 'zod';
+
+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 = defineFlow(
+ {
+ 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 generateStream({
+ model: gemini15ProPreview,
+ output: {
+ 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 0000000000..efbb566bf7
--- /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
+ }
+}