-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat: implement ADK wrapper functions for Agent Engine sessions #1252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Logs | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| firebase-debug.log* | ||
| firebase-debug.*.log* | ||
|
|
||
| # Firebase cache | ||
| .firebase/ | ||
|
|
||
| # Firebase config | ||
|
|
||
| # Uncomment this if you'd like others to create their own Firebase project. | ||
| # For a team working on the same Firebase project(s), it is recommended to leave | ||
| # it commented so all members can deploy to the same project(s) in .firebaserc. | ||
| # .firebaserc | ||
|
|
||
| # Runtime data | ||
| pids | ||
| *.pid | ||
| *.seed | ||
| *.pid.lock | ||
|
|
||
| # Directory for instrumented libs generated by jscoverage/JSCover | ||
| lib-cov | ||
|
|
||
| # Coverage directory used by tools like istanbul | ||
| coverage | ||
|
|
||
| # nyc test coverage | ||
| .nyc_output | ||
|
|
||
| # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||
| .grunt | ||
|
|
||
| # Bower dependency directory (https://bower.io/) | ||
| bower_components | ||
|
|
||
| # node-waf configuration | ||
| .lock-wscript | ||
|
|
||
| # Compiled binary addons (http://nodejs.org/api/addons.html) | ||
| build/Release | ||
|
|
||
| # Dependency directories | ||
| node_modules/ | ||
|
|
||
| # Optional npm cache directory | ||
| .npm | ||
|
|
||
| # Optional eslint cache | ||
| .eslintcache | ||
|
|
||
| # Optional REPL history | ||
| .node_repl_history | ||
|
|
||
| # Output of 'npm pack' | ||
| *.tgz | ||
|
|
||
| # Yarn Integrity file | ||
| .yarn-integrity | ||
|
|
||
| # dotenv environment variables file | ||
| .env | ||
|
|
||
| # dataconnect generated files | ||
| .dataconnect |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # Agent Engine (ADK) Wrapper Proxy | ||
|
|
||
| This sample demonstrates how to create a proxy between a client application and Google Cloud's Agent Engine using Firebase Cloud Functions. It allows secure access to agents built with the Agent Development Kit (ADK) from customer-facing applications. | ||
|
|
||
| ## Why use a proxy? | ||
|
|
||
| Accessing Agent Engine directly from a client application requires service account credentials or user credentials with broad access, which is not secure for public-facing apps. This wrapper provides: | ||
|
|
||
| 1. **Authentication**: Automatically integrates with Firebase Authentication. | ||
| 2. **Security**: Enforces App Check to prevent abuse. | ||
| 3. **Encapsulation**: Hides specific Agent Engine IDs and project details from the client. | ||
|
|
||
| ## Functions Code | ||
|
|
||
| See the [functions/src/adk-endpoints](functions/src/adk-endpoints) directory for the implementation of each endpoint. | ||
|
|
||
| All functions are implemented as **Callable Functions** (`onCall`). They automatically decode client data and verify user authentication tokens. | ||
|
|
||
| ### How to call from your app (Client Example) | ||
|
|
||
| To call these functions from a client app (e.g., Web, iOS, Android), use the Firebase Functions SDK. Here is a generic example using the JS SDK: | ||
|
|
||
| ```javascript | ||
| import { getFunctions, httpsCallable } from 'firebase/functions'; | ||
|
|
||
| const functions = getFunctions(); | ||
|
|
||
| // Example: Calling async_create_session | ||
| const asyncCreateSession = httpsCallable(functions, 'async_create_session'); | ||
| try { | ||
| const result = await asyncCreateSession(); | ||
| console.log('Session created:', result.data); | ||
| } catch (error) { | ||
| console.error('Error creating session:', error); | ||
| } | ||
| ``` | ||
|
|
||
| ### Callable Function Reference | ||
|
|
||
| Here are the available callable functions and how to call them with data. | ||
|
|
||
| #### `async_create_session` | ||
| Creates a new session for the authenticated user. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: None (Uses the authenticated user's UID as `user_id`). | ||
| * **Returns**: The created session object. | ||
|
|
||
| #### `async_delete_session` | ||
| Deletes a session for the authenticated user. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ session_id: string }` | ||
| * **Returns**: The result from Agent Engine. | ||
|
|
||
| #### `async_get_session` | ||
| Retrieves details for a specific session. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ session_id: string }` | ||
| * **Returns**: The session details. | ||
|
|
||
| #### `async_list_sessions` | ||
| Lists all sessions for the authenticated user. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: None (Uses the authenticated user's UID to filter sessions). | ||
| * **Returns**: An array of sessions. | ||
|
|
||
| #### `async_add_session_to_memory` | ||
| Adds a session to memory (generates memories). | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ session: any }` | ||
| * **Returns**: The result from Agent Engine. | ||
|
|
||
| #### `async_search_memory` | ||
| Searches memories for the given user. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ query: string }` | ||
| * **Returns**: The search results. | ||
|
|
||
| #### `async_stream_query` | ||
| Streams responses asynchronously from the ADK application. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ message: string, session_id?: string, run_config?: any }` | ||
| * **Returns**: An object containing the full response and chunks. | ||
|
|
||
| #### `streaming_agent_run_with_events` | ||
| Streams responses asynchronously from the ADK application, typically used by tools like AgentSpace. | ||
| * **Requires Auth**: Yes | ||
| * **Input**: `{ request_json: any }` | ||
| * **Returns**: An object containing the full response and chunks. | ||
|
|
||
| ## The `common` Folder & Configuration | ||
|
|
||
| The `functions/src/common` folder contains shared logic and configuration for all endpoints. | ||
|
|
||
| * `adk.ts`: Contains helper functions `callReasoningEngine` and `callReasoningEngineStream` that use the `@google-cloud/aiplatform` SDK to communicate with Agent Engine. | ||
| * `config.ts`: Defines the configuration options for the project. | ||
|
|
||
| ### Configuration Options in `config.ts` | ||
|
|
||
| To use this wrapper, you need to configure it with your Google Cloud and Agent Engine details. You can do this by setting environment variables or editing the values directly in `config.ts`: | ||
|
|
||
| * **`PROJECT_ID`**: The Google Cloud project ID containing your agent. Defaults to `process.env.GCLOUD_PROJECT`. | ||
| * **`LOCATION`**: The region where your Agent Engine agent is deployed (e.g., `us-central1`). Defaults to `process.env.LOCATION`. | ||
| * **`REASONING_ENGINE_ID`**: The unique ID of your reasoning engine instance. Defaults to `process.env.REASONING_ENGINE_ID`. | ||
| * **`ENFORCE_APP_CHECK`**: Set to `true` to require Firebase App Check tokens for all requests. Hardcoded to `true` in this sample. | ||
| * **`REPLAY_PROTECTED`**: Set to `true` to consume App Check tokens for replay protection. Hardcoded to `true` in this sample. | ||
|
|
||
| ## Deploy and test | ||
|
|
||
| To set up the sample: | ||
|
|
||
| 1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com). | ||
| 2. Enable Cloud Functions and Firebase Authentication. | ||
| 3. Deploy your ADK agent to Agent Engine and obtain the `REASONING_ENGINE_ID`. | ||
| 4. Clone this repository. | ||
| 5. Navigate to this sample directory: `cd Node/adk-wrapper`. | ||
| 6. Set up your project: `firebase use --add` and follow the instructions. | ||
| 7. Install dependencies: `cd functions; npm install; cd -`. | ||
| 8. Set environment variables or edit `functions/src/common/config.ts` with your values. | ||
| 9. Deploy the functions: `firebase deploy`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "functions": [ | ||
| { | ||
| "source": "functions", | ||
| "codebase": "default", | ||
| "disallowLegacyRuntimeConfig": true, | ||
| "ignore": [ | ||
| "node_modules", | ||
| ".git", | ||
| "firebase-debug.log", | ||
| "firebase-debug.*.log", | ||
| "*.local" | ||
| ], | ||
| "predeploy": [ | ||
| "npm --prefix \"$RESOURCE_DIR\" run lint", | ||
| "npm --prefix \"$RESOURCE_DIR\" run build" | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| module.exports = { | ||
| root: true, | ||
| env: { | ||
| es6: true, | ||
| node: true, | ||
| }, | ||
| extends: [ | ||
| "eslint:recommended", | ||
| "plugin:import/errors", | ||
| "plugin:import/warnings", | ||
| "plugin:import/typescript", | ||
| "google", | ||
| "plugin:@typescript-eslint/recommended", | ||
| ], | ||
| parser: "@typescript-eslint/parser", | ||
| parserOptions: { | ||
| project: ["tsconfig.json", "tsconfig.dev.json"], | ||
| sourceType: "module", | ||
| }, | ||
| ignorePatterns: [ | ||
| "/lib/**/*", // Ignore built files. | ||
| "/generated/**/*", // Ignore generated files. | ||
| ], | ||
| plugins: [ | ||
| "@typescript-eslint", | ||
| "import", | ||
| ], | ||
| rules: { | ||
| "quotes": ["error", "double"], | ||
| "import/no-unresolved": 0, | ||
| "indent": ["error", 2], | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Compiled JavaScript files | ||
| lib/**/*.js | ||
| lib/**/*.js.map | ||
|
|
||
| # TypeScript v1 declaration files | ||
| typings/ | ||
|
|
||
| # Node.js dependency directory | ||
| node_modules/ | ||
| *.local |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "name": "functions", | ||
| "scripts": { | ||
| "lint": "eslint --ext .js,.ts .", | ||
| "build": "tsc", | ||
| "build:watch": "tsc --watch", | ||
| "serve": "npm run build && firebase emulators:start --only functions", | ||
| "shell": "npm run build && firebase functions:shell", | ||
| "start": "npm run shell", | ||
| "deploy": "firebase deploy --only functions", | ||
| "logs": "firebase functions:log" | ||
| }, | ||
| "engines": { | ||
| "node": "24" | ||
| }, | ||
| "main": "lib/index.js", | ||
| "dependencies": { | ||
| "@google-cloud/aiplatform": "^6.5.0", | ||
| "firebase-admin": "^13.6.0", | ||
| "firebase-functions": "^7.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@typescript-eslint/eslint-plugin": "^5.12.0", | ||
| "@typescript-eslint/parser": "^5.12.0", | ||
| "eslint": "^8.9.0", | ||
| "eslint-config-google": "^0.14.0", | ||
| "eslint-plugin-import": "^2.25.4", | ||
| "firebase-functions-test": "^3.4.1", | ||
| "typescript": "^5.7.3" | ||
| }, | ||
| "private": true | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||||||||||
| import { onCall } from "firebase-functions/v2/https"; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| import { callReasoningEngine } from "../common/adk"; | ||||||||||||||
| import { ENFORCE_APP_CHECK, REPLAY_PROTECTED } from "../common/config"; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Generates memories. | ||||||||||||||
| */ | ||||||||||||||
| export const async_add_session_to_memory = onCall({ | ||||||||||||||
| timeoutSeconds: 3600, | ||||||||||||||
| enforceAppCheck: ENFORCE_APP_CHECK, | ||||||||||||||
| consumeAppCheckToken: REPLAY_PROTECTED, | ||||||||||||||
| }, async (request) => { | ||||||||||||||
| const uid = request.auth?.uid; | ||||||||||||||
| if (!uid) { | ||||||||||||||
| throw new Error("Unauthorized"); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+14
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||||||||||||||
| const { session } = request.data; | ||||||||||||||
| return await callReasoningEngine("async_add_session_to_memory", { user_id: uid, session }); | ||||||||||||||
| }); | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { onCall } from "firebase-functions/v2/https"; | ||
| import { callReasoningEngine } from "../common/adk"; | ||
| import { ENFORCE_APP_CHECK, REPLAY_PROTECTED } from "../common/config"; | ||
|
|
||
| /** | ||
| * Creates a new session. | ||
| */ | ||
| export const async_create_session = onCall({ | ||
| timeoutSeconds: 3600, | ||
| enforceAppCheck: ENFORCE_APP_CHECK, | ||
| consumeAppCheckToken: REPLAY_PROTECTED, | ||
| }, async (request) => { | ||
| const uid = request.auth?.uid; | ||
| if (!uid) { | ||
| throw new Error("Unauthorized"); | ||
| } | ||
| return await callReasoningEngine("async_create_session", { user_id: uid }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { onCall } from "firebase-functions/v2/https"; | ||
| import { callReasoningEngine } from "../common/adk"; | ||
| import { ENFORCE_APP_CHECK, REPLAY_PROTECTED } from "../common/config"; | ||
|
|
||
| /** | ||
| * Deletes a session for the given user. | ||
| */ | ||
| export const async_delete_session = onCall({ | ||
| timeoutSeconds: 3600, | ||
| enforceAppCheck: ENFORCE_APP_CHECK, | ||
| consumeAppCheckToken: REPLAY_PROTECTED, | ||
| }, async (request) => { | ||
| const uid = request.auth?.uid; | ||
| if (!uid) { | ||
| throw new Error("Unauthorized"); | ||
| } | ||
| const { session_id } = request.data; | ||
| return await callReasoningEngine("async_delete_session", { user_id: uid, session_id }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { onCall } from "firebase-functions/v2/https"; | ||
| import { callReasoningEngine } from "../common/adk"; | ||
| import { ENFORCE_APP_CHECK, REPLAY_PROTECTED } from "../common/config"; | ||
|
|
||
| /** | ||
| * Get a session for the given user. | ||
| */ | ||
| export const async_get_session = onCall({ | ||
| timeoutSeconds: 3600, | ||
| enforceAppCheck: ENFORCE_APP_CHECK, | ||
| consumeAppCheckToken: REPLAY_PROTECTED, | ||
| }, async (request) => { | ||
| const uid = request.auth?.uid; | ||
| if (!uid) { | ||
| throw new Error("Unauthorized"); | ||
| } | ||
| const { session_id } = request.data; | ||
| return await callReasoningEngine("async_get_session", { user_id: uid, session_id }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { onCall } from "firebase-functions/v2/https"; | ||
| import { callReasoningEngine } from "../common/adk"; | ||
| import { ENFORCE_APP_CHECK, REPLAY_PROTECTED } from "../common/config"; | ||
|
|
||
| /** | ||
| * List sessions for the given user. | ||
| */ | ||
| export const async_list_sessions = onCall({ | ||
| timeoutSeconds: 3600, | ||
| enforceAppCheck: ENFORCE_APP_CHECK, | ||
| consumeAppCheckToken: REPLAY_PROTECTED, | ||
| }, async (request) => { | ||
| const uid = request.auth?.uid; | ||
| if (!uid) { | ||
| throw new Error("Unauthorized"); | ||
| } | ||
| console.log("Calling async_list_sessions for uid:", uid); | ||
| const result = await callReasoningEngine("async_list_sessions", { user_id: uid }) as any; | ||
| console.log("Reasoning Engine result:", JSON.stringify(result, null, 2)); | ||
| return result?.sessions || []; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Node.js version "24" is likely a typo or refers to a version not yet supported by Firebase Functions. It is recommended to use a stable LTS version like "22" or "20".