Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Node/adk-wrapper/.gitignore
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
119 changes: 119 additions & 0 deletions Node/adk-wrapper/README.md
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`.
20 changes: 20 additions & 0 deletions Node/adk-wrapper/firebase.json
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"
]
}
]
}
33 changes: 33 additions & 0 deletions Node/adk-wrapper/functions/.eslintrc.js
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],
},
};
10 changes: 10 additions & 0 deletions Node/adk-wrapper/functions/.gitignore
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
32 changes: 32 additions & 0 deletions Node/adk-wrapper/functions/package.json
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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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".

Suggested change
"node": "24"
"node": "22"

},
"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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import HttpsError to provide structured error responses to the client. This should be applied to all endpoint files.

Suggested change
import { onCall } from "firebase-functions/v2/https";
import { onCall, HttpsError } from "firebase-functions/v2/https";

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use HttpsError with the unauthenticated code instead of a generic Error. Generic errors are returned to the client as INTERNAL (500), which hides the actual cause and is poor practice for API design. This pattern should be followed in all other endpoint files as well.

Suggested change
if (!uid) {
throw new Error("Unauthorized");
}
if (!uid) {
throw new HttpsError("unauthenticated", "The function must be called while authenticated.");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 });
});
19 changes: 19 additions & 0 deletions Node/adk-wrapper/functions/src/adk-endpoints/async_get_session.ts
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 || [];
});
Loading
Loading