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 @@ + + +
+ + Firebase Genkit Samples + + + +
+ + description + +
+ +
+ +
+
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 @@ + + +
+

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 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 }} +
+
+
+

+ 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 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? + + + +
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 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 + } +}