feat: Add sample agent demonstrating 2LO, 3LO, and API Key auth via GcpAuthProvider

PiperOrigin-RevId: 904823303
This commit is contained in:
Google Team Member
2026-04-23 23:14:34 -07:00
committed by Copybara-Service
parent 2565cc6872
commit 909a8c2ad4
5 changed files with 971 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
# GCP Auth Sample
Demonstrates the use of Agent Identity auth manager with an agent that queries
Spotify and Google Maps using auth providers.
Use `adk web` to run API key and 2-legged oauth flows, while use the included
custom agent web client to run 3-legged oauth flows.
## Setup
### 1. Activate environment
```bash
cd adk-python
python3 -m venv .venv
source .venv/bin/activate
```
### 2. Install dependencies
```bash
pip install "google-adk[agent-identity]"
```
### 3. Authenticate your environment
```bash
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="YOUR_GOOGLE_CLOUD_PROJECT"
gcloud auth application-default set-quota-project $GOOGLE_CLOUD_PROJECT
```
### 4. Create auth providers
Refer to the [public documentation](https://cloud.google.com/iam/docs/manage-auth-providers) to create the following Agent Identity auth providers.
> **Note:**
> The identity running the agent (via Application Default Credentials) must have
> the necessary [permissions](https://docs.cloud.google.com/iam/docs/roles-permissions/iamconnectors#iamconnectors.user)
> to retrieve credentials from these connectors. Ensure your account has the
> necessary role to access these resources.
```bash
export GOOGLE_CLOUD_LOCATION="YOUR_GOOGLE_CLOUD_LOCATION"
export MAPS_API_AUTH_PROVIDER_ID="YOUR_MAPS_API_AUTH_PROVIDER_ID"
export SPOTIFY_2LO_AUTH_PROVIDER_ID="YOUR_SPOTIFY_2LO_AUTH_PROVIDER_ID"
export SPOTIFY_3LO_AUTH_PROVIDER_ID="YOUR_SPOTIFY_3LO_AUTH_PROVIDER_ID"
gcloud alpha agent-identity connectors create $MAPS_API_AUTH_PROVIDER_ID \
--project=$GOOGLE_CLOUD_PROJECT \
--location=$GOOGLE_CLOUD_LOCATION \
--api-key=YOUR_API_KEY
gcloud alpha agent-identity connectors create $SPOTIFY_2LO_AUTH_PROVIDER_ID \
--project=$GOOGLE_CLOUD_PROJECT \
--location=$GOOGLE_CLOUD_LOCATION \
--two-legged-oauth-client-id=OAUTH_CLIENT_ID \
--two-legged-oauth-client-secret=OAUTH_CLIENT_SECRET \
--two-legged-oauth-token-endpoint=OAUTH_TOKEN_ENDPOINT
gcloud alpha agent-identity connectors create $SPOTIFY_3LO_AUTH_PROVIDER_ID \
--project=$GOOGLE_CLOUD_PROJECT \
--location=$GOOGLE_CLOUD_LOCATION \
--three-legged-oauth-client-id=OAUTH_CLIENT_ID \
--three-legged-oauth-client-secret=OAUTH_CLIENT_SECRET \
--three-legged-oauth-authorization-url=AUTHORIZATION_URL \
--three-legged-oauth-token-url=TOKEN_URL \
--allowed-scopes=ALLOWED_SCOPES
```
### 5. Test API key and 2LO auth provider using ADK web client
```bash
adk web contributing/samples
```
- On the ADK web UI, select the agent named `gcp_auth` from the dropdown.
- Sample queries to try:
- API key (Google Maps tool): "What is the current weather in New York?"
- 2LO key (Spotify tool): "Tell me about the song: Waving Flag"
### 6. Test 3LO auth provider using custom web client
> **Note:** If the agent backend is running on a different port or host other
> than `localhost:8000`, please set the `AGENT_BACKEND_URL` environment variable
> before starting the client (e.g.,
> `export AGENT_BACKEND_URL="http://localhost:9000"`).
- In a separate shell, activate environment
```bash
cd adk-python
python3 -m venv .venv
source .venv/bin/activate
```
- Navigate to the client directory and install dependencies
```bash
cd contributing/samples/gcp_auth/client
pip install -r requirements.txt
```
- Start the client application
```bash
uvicorn main:app --port 8080 --reload
```
- Open `http://localhost:8080`. (**Note:** You must use `localhost` and not `127.0.0.1`, as the OAuth redirect URL specifically requires it.)
- On the login screen, enter an arbitrary user ID (e.g. test_user123).
- Sample queries to try:
- 3LO key (Spotify tool): "What are my private Spotify playlists?"
+166
View File
@@ -0,0 +1,166 @@
# Copyright 2026 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.
from __future__ import annotations
import os
from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.tools.authenticated_function_tool import AuthenticatedFunctionTool
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
import httpx
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT")
LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION")
MAPS_API_AUTH_PROVIDER_ID = os.environ.get("MAPS_API_AUTH_PROVIDER_ID")
SPOTIFY_2LO_AUTH_PROVIDER_ID = os.environ.get("SPOTIFY_2LO_AUTH_PROVIDER_ID")
SPOTIFY_3LO_AUTH_PROVIDER_ID = os.environ.get("SPOTIFY_3LO_AUTH_PROVIDER_ID")
MAPS_API_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{MAPS_API_AUTH_PROVIDER_ID}"
SPOTIFY_2LO_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{SPOTIFY_2LO_AUTH_PROVIDER_ID}"
SPOTIFY_3LO_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{SPOTIFY_3LO_AUTH_PROVIDER_ID}"
MAPS_MCP_ENDPOINT = "https://mapstools.googleapis.com/mcp"
CONTINUE_URI = "http://localhost:8080/commit"
MODEL = "gemini-2.5-flash"
async def spotify_search_track(
credential: AuthCredential, query: str
) -> str | list:
"""Searches for a track on Spotify and returns its details."""
headers = {}
if http := credential.http:
if http.scheme and http.credentials and (token := http.credentials.token):
headers["Authorization"] = f"{http.scheme.title()} {token}"
if http.additional_headers:
headers.update(http.additional_headers)
if not headers:
return "Error: No authentication token available."
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.spotify.com/v1/search",
headers=headers,
params={"q": query, "type": "track", "limit": 1},
)
if response.status_code != 200:
return f"Error from Spotify API: {response.status_code} - {response.text}"
data = response.json()
items = data.get("tracks", {}).get("items", [])
if not items:
return f"No track found for query '{query}'."
return items
async def spotify_get_playlists(credential: AuthCredential) -> str | list:
"""Fetches the current user's private playlists on Spotify."""
headers = {}
if http := credential.http:
if http.scheme and http.credentials and (token := http.credentials.token):
headers["Authorization"] = f"{http.scheme.title()} {token}"
if http.additional_headers:
headers.update(http.additional_headers)
if not headers:
return "Error: No authentication token available."
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.spotify.com/v1/me/playlists",
headers=headers,
params={"limit": 10},
)
if response.status_code != 200:
return f"Error from Spotify API: {response.status_code} - {response.text}"
data = response.json()
items = data.get("items", [])
if not items:
return "No playlists found for the current user."
# Extract useful information
return [
{
"name": item.get("name"),
"public": item.get("public"),
"total_tracks": item.get("tracks", {}).get("total"),
}
for item in items
if item
]
spotify_auth_config_2lo = AuthConfig(
auth_scheme=GcpAuthProviderScheme(name=SPOTIFY_2LO_AUTH_PROVIDER)
)
spotify_search_track_tool = AuthenticatedFunctionTool(
func=spotify_search_track,
auth_config=spotify_auth_config_2lo,
)
spotify_auth_config_3lo = AuthConfig(
auth_scheme=GcpAuthProviderScheme(
name=SPOTIFY_3LO_AUTH_PROVIDER,
scopes=["playlist-read-private"],
continue_uri=CONTINUE_URI,
)
)
spotify_get_playlist_tool = AuthenticatedFunctionTool(
func=spotify_get_playlists,
auth_config=spotify_auth_config_3lo,
)
maps_tools = McpToolset(
connection_params=StreamableHTTPConnectionParams(url=MAPS_MCP_ENDPOINT),
auth_scheme=GcpAuthProviderScheme(name=MAPS_API_AUTH_PROVIDER),
errlog=None, # Required for agent freezing (pickling)
)
CredentialManager.register_auth_provider(GcpAuthProvider())
root_agent = Agent(
name="gcp_auth_agent",
model=MODEL,
instruction=(
"You are a Spotify and Google Maps assistant. Use your tools to "
"search for track details, fetch the user's private playlists, "
"and look up locations. Keep responses concise, friendly, and "
"emoji-filled!"
),
tools=[
spotify_search_track_tool,
spotify_get_playlist_tool,
maps_tools,
],
)
app = App(
name="gcp_auth",
root_agent=root_agent,
)
@@ -0,0 +1,566 @@
<!doctype html>
<html>
<head>
<title>Gcp Auth demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg: #f8fafc;
--surface: #ffffff;
--primary: #1db954;
--text-main: #191414;
--text-muted: #64748b;
--bubble-agent: #ffffff;
}
body {
font-family:
"Inter",
-apple-system,
sans-serif;
margin: 0;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
color: var(--text-main);
display: flex;
flex-direction: column;
height: 100vh;
}
#header {
padding: 24px;
background: var(--surface);
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
letter-spacing: -0.02em;
text-align: center;
font-size: 1.1em;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
#chat-wrapper {
flex: 1;
overflow-y: auto;
padding: 24px 16px;
scroll-behavior: smooth;
}
#chat {
max-width: 680px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.msg {
max-width: 85%;
line-height: 1.55;
font-size: 15px;
padding: 12px 18px;
border-radius: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user {
align-self: flex-end;
background: var(--primary);
color: white;
border-bottom-right-radius: 4px;
}
.agent {
align-self: flex-start;
background: var(--bubble-agent);
border-bottom-left-radius: 4px;
white-space: pre-wrap;
}
.thought {
align-self: flex-start;
color: var(--text-muted);
font-size: 13px;
font-style: italic;
margin-top: -12px;
}
/* Basic ASCII Thinking Animation */
.thinking-bubble {
align-self: flex-start;
background: var(--bubble-agent);
border-bottom-left-radius: 4px;
color: #aaa;
font-weight: bold;
font-family: monospace;
}
#input-container {
background: var(--surface);
border-top: 1px solid #e5e7eb;
padding: 24px;
}
#controls {
max-width: 680px;
margin: 0 auto;
display: flex;
gap: 12px;
}
input {
flex: 1;
padding: 14px 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
font-size: 15px;
outline: none;
transition: all 0.2s;
background: var(--bg);
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
background: #fff;
}
button {
padding: 0 24px;
background: var(--primary);
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<!-- ========================================== -->
<!-- HTML BODY STARTS -->
<!-- ========================================== -->
<body>
<div id="header" style="position: relative">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="vertical-align: middle; margin-right: 8px; color: var(--primary)"
>
<path d="M9 18V5l12-2v13"></path>
<circle cx="6" cy="18" r="3"></circle>
<circle cx="18" cy="16" r="3"></circle>
</svg>
Spotify and Maps Assistant
<div
id="user-info-container"
style="
position: absolute;
right: 24px;
top: 50%;
transform: translateY(-50%);
display: none;
flex-direction: column;
align-items: flex-end;
gap: 4px;
"
>
<div
id="user-badge"
style="
font-size: 14px;
font-weight: 500;
color: #1db954;
background: #f1f5f9;
padding: 4px 12px;
border-radius: 20px;
"
></div>
<div
id="session-badge"
style="font-size: 11px; font-weight: normal; color: #94a3b8; font-family: monospace"
></div>
</div>
</div>
<div
id="login-container"
style="
flex: 1;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
"
>
<div
style="
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
text-align: center;
width: 100%;
max-width: 320px;
box-sizing: border-box;
"
>
<h2 style="margin-top: 0; color: #191414">Welcome</h2>
<p style="color: #64748b; font-size: 14px; margin-bottom: 24px">
Please enter your User ID to continue.
</p>
<input
type="text"
id="loginUserId"
placeholder="User ID"
style="width: 100%; box-sizing: border-box; margin-bottom: 16px"
onkeypress="if(event.key==='Enter') login()"
/>
<button
onclick="login()"
style="
width: 100%;
box-sizing: border-box;
padding: 14px;
border-radius: 12px;
cursor: pointer;
"
>
Login
</button>
</div>
</div>
<div id="chat-wrapper" style="display: none">
<div id="chat">
<div class="msg agent">
Asks questions about Spotify: song details, your private playlists (needs user consent)
etc. Additionally can also ask questions to Google maps.
</div>
</div>
</div>
<div id="input-container" style="display: none">
<div id="controls">
<input
type="text"
id="userInput"
placeholder="Ask a question..."
autofocus
autocomplete="off"
/>
<button id="sendBtn" onclick="send()">Send</button>
</div>
</div>
<!-- ========================================== -->
<!-- JAVASCRIPT STARTS -->
<!-- ========================================== -->
<script>
const chat = document.getElementById("chat");
const wrapper = document.getElementById("chat-wrapper");
const input = document.getElementById("userInput");
const btn = document.getElementById("sendBtn");
let thinkingDiv = null;
let thinkingInterval = null;
let pendingAuthCall = null;
let currentUserId = "";
let currentSessionId = "";
function login() {
const idInput = document.getElementById("loginUserId").value.trim();
if (!idInput) return;
currentUserId = idInput;
document.cookie = "user_id=" + currentUserId + "; path=/";
// Generate a random session ID for this specific chat session
currentSessionId = "session_" + Math.random().toString(36).substring(2, 15);
document.getElementById("login-container").style.display = "none";
document.getElementById("chat-wrapper").style.display = "block";
document.getElementById("input-container").style.display = "block";
document.getElementById("user-badge").innerText = "👤 " + currentUserId;
document.getElementById("session-badge").innerText = "ID: " + currentSessionId;
document.getElementById("user-info-container").style.display = "flex";
input.focus();
scroll();
}
const scroll = () => (wrapper.scrollTop = wrapper.scrollHeight);
async function resumeSession() {
if (!pendingAuthCall) {
return;
}
const payload = {
user_id: currentUserId,
session_id: currentSessionId,
function_response: {
name: "adk_request_credential",
id: pendingAuthCall.id,
response: pendingAuthCall.args.authConfig,
},
};
const cardId = "auth-card-" + pendingAuthCall.id;
const authCard = document.getElementById(cardId);
if (authCard) {
authCard.remove();
} else {
// Fallback cleanup just in case
const cards = document.querySelectorAll('div[id^="auth-card-"]');
cards.forEach((card) => card.remove());
}
pendingAuthCall = null;
const tempDiv = add("Authenticating and syncing with Spotify...", "thought");
setThinking(true);
try {
const response = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
tempDiv.remove(); // Remove the sync message once the response starts
await processStream(response);
} catch (err) {
tempDiv.remove();
add("Error resuming session.", "agent");
} finally {
setThinking(false);
input.disabled = false;
btn.disabled = false;
input.focus();
}
}
function add(text, type, isHTML = false) {
const div = document.createElement("div");
div.className = type === "user" ? "msg user" : type === "thought" ? "thought" : "msg agent";
if (isHTML) div.innerHTML = text;
else div.innerText = text;
chat.appendChild(div);
scroll();
return div;
}
function setThinking(active) {
if (active) {
if (thinkingDiv) return;
thinkingDiv = document.createElement("div");
thinkingDiv.className = "msg thinking-bubble";
chat.appendChild(thinkingDiv);
let dots = 0;
thinkingInterval = setInterval(() => {
dots = (dots + 1) % 4;
thinkingDiv.innerText = ".".repeat(dots).padEnd(3, " ");
}, 400);
} else if (thinkingDiv) {
clearInterval(thinkingInterval);
thinkingDiv.remove();
thinkingDiv = null;
}
scroll();
}
async function processStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let currentAgentDiv = null;
let fullText = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const json = JSON.parse(line.slice(6).trim());
if (json.content?.parts) {
for (const p of json.content.parts) {
const fc = p.functionCall || p.function_call;
if (fc) {
if (fc.name === "adk_request_credential") {
setThinking(false);
pendingAuthCall = fc;
const args = fc.args || {};
const authConfig = args.authConfig || args.auth_config;
if (!authConfig) {
console.error("Missing auth config in args:", args);
add(
"Error: Received credential request but missing auth configuration.",
"agent",
);
continue;
}
const authUri = authConfig.exchangedAuthCredential?.oauth2?.authUri;
const nonce = authConfig.exchangedAuthCredential?.oauth2?.nonce;
if (nonce) {
document.cookie = "consent_nonce=" + nonce + "; path=/";
}
// Create a standalone authorization card outside the standard 'msg' styling
const authDiv = document.createElement("div");
authDiv.id = "auth-card-" + pendingAuthCall.id;
authDiv.style.cssText =
"background-color: #fffbeb; border: 1px solid #fde68a; padding: 20px; border-radius: 12px; margin: 12px auto; max-width: 100%; box-shadow: 0 2px 4px rgba(0,0,0,0.05); align-self: stretch;";
// Disable input and button
input.disabled = true;
btn.disabled = true;
// Define the cancel function globally or attach it to the window so the inline onclick can reach it
window.cancelAuth = function () {
if (pendingAuthCall) {
resumeSession();
}
};
// Define the function to open the popup and poll for closure
window.openAuthPopup = function (url, buttonElement) {
if (buttonElement) {
buttonElement.innerText = "Waiting for authorization...";
buttonElement.style.opacity = "0.7";
buttonElement.style.cursor = "wait";
const cancelBtn = buttonElement.nextElementSibling;
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.style.opacity = "0.5";
}
}
const popup = window.open(url, "_blank");
if (!popup) {
alert("Popup blocked! Please allow popups for this site.");
if (buttonElement) {
buttonElement.innerText = "Authorize to continue";
buttonElement.style.opacity = "1";
buttonElement.style.cursor = "pointer";
}
} else {
// The polling loop watches for the window to close
const timer = setInterval(() => {
if (popup.closed) {
clearInterval(timer);
// The popup closed, either via success script or user canceling.
// We resume the session, and ADK backend will handle whether a token exists.
if (pendingAuthCall) {
resumeSession();
}
}
}, 500);
}
};
authDiv.innerHTML = `
<div style="text-align: left;">
<h3 style="color: #78350f; font-weight: 600; margin: 0 0 8px 0; font-size: 15px;">
⚠️ Secure Authorization Required
</h3>
<p style="color: #92400e; font-size: 14px; margin: 0 0 16px 0; line-height: 1.5;">
The agent is blocked because it needs access to your Spotify account. You must authenticate to continue.
</p>
<div style="display: flex; gap: 12px; align-items: center;">
<button onclick="window.openAuthPopup('${authUri}', this)" style="display: inline-block; background-color: #2563eb; color: white; font-weight: 500; padding: 8px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: background-color 0.2s;">
Authorize to continue
</button>
<button onclick="window.cancelAuth()" style="display: inline-block; background-color: transparent; color: #64748b; font-weight: 500; padding: 8px 16px; border: 1px solid #cbd5e1; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.2s;">
Cancel
</button>
</div>
</div>
`;
chat.appendChild(authDiv);
scroll();
currentAgentDiv = null;
} else {
// The ADK backend sends the tool call payload multiple times.
// We only log it once per tool call.
if (json.partial === false) {
continue;
}
add("⚙️ Tool call: " + fc.name, "thought");
currentAgentDiv = null;
}
}
if (p.text) {
// The ADK backend sends the full accumulated text in the final "partial: false" event.
// Since we already append the deltas ("partial: true"), we should ignore the final one to avoid doubling the text.
if (json.partial === false && fullText.length > 0) {
continue;
}
setThinking(false);
if (!currentAgentDiv) {
currentAgentDiv = add("", "agent");
fullText = "";
}
fullText += p.text;
currentAgentDiv.innerText = fullText;
}
}
}
} catch (e) {
console.error("JSON Parse Error on line:", line, e);
}
}
}
scroll();
}
}
async function send() {
const val = input.value.trim();
if (!val) return;
add(val, "user");
input.value = "";
input.disabled = true;
btn.disabled = true;
setThinking(true);
try {
const response = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: val,
user_id: currentUserId,
session_id: currentSessionId,
}),
});
await processStream(response);
} catch (err) {
setThinking(false);
add("Error connecting to service.", "agent");
} finally {
setThinking(false);
input.disabled = false;
btn.disabled = false;
input.focus();
}
}
input.onkeypress = (e) => {
if (e.key === "Enter") send();
};
</script>
</body>
</html>
@@ -0,0 +1,122 @@
# Copyright 2026 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.
from __future__ import annotations
import json
import logging
import os
import sys
from fastapi import FastAPI
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.responses import StreamingResponse
import httpx
logging.basicConfig(
level=logging.INFO, stream=sys.stdout, format="%(levelname)s: %(message)s"
)
logger = logging.getLogger("google_adk." + __name__)
app = FastAPI()
AGENT_URL = os.environ.get("AGENT_BACKEND_URL", "http://localhost:8000")
@app.get("/")
def ui():
with open("index.html", "r") as f:
return HTMLResponse(content=f.read())
@app.post("/chat")
async def chat(request: Request):
data = await request.json()
message = data.get("message")
function_response = data.get("function_response")
app_name = "gcp_auth"
user_id = data.get("user_id", "test_user")
session_id = data.get("session_id", "default_session_id")
payload = {
"appName": app_name,
"userId": user_id,
"sessionId": session_id,
"streaming": True,
}
if message:
payload["newMessage"] = {
"role": "user",
"parts": [{"text": message}],
}
elif function_response:
payload["newMessage"] = {
"role": "user",
"parts": [{"functionResponse": function_response}],
}
# Ensure the session exists before we try to continue it via /run_sse
async def proxy_stream():
async with httpx.AsyncClient(timeout=120.0) as client:
# Attempt to create the session (ignoring if it already exists or fails quietly)
await client.post(
f"{AGENT_URL}/apps/{app_name}/users/{user_id}/sessions/{session_id}"
)
async with client.stream(
"POST", f"{AGENT_URL}/run_sse", json=payload
) as r:
if r.status_code != 200:
err = await r.aread()
yield f"data: {json.dumps({'error': err.decode()})}\n\n"
return
async for line in r.aiter_lines():
if line:
yield f"data: {line}\n\n" if line.startswith("{") else f"{line}\n\n"
return StreamingResponse(proxy_stream(), media_type="text/event-stream")
@app.api_route("/commit", methods=["GET"])
async def commit(request: Request):
connector = request.query_params.get("connector_name")
payload = {
"userId": request.cookies.get("user_id"),
"userIdValidationState": request.query_params.get(
"user_id_validation_state"
),
"consentNonce": request.cookies.get("consent_nonce"),
}
url = f"https://iamconnectorcredentials.googleapis.com/v1alpha/{connector}/credentials:finalize"
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(url, json=payload)
resp.raise_for_status()
except httpx.HTTPError as e:
err_text = e.response.text if hasattr(e, "response") else str(e)
status = e.response.status_code if hasattr(e, "response") else 500
logger.error(f"Commit failed: {err_text}")
return HTMLResponse(err_text, status_code=status)
return HTMLResponse("""
<script>
window.close();
</script>
<p>Success. You can close this window.</p>
""")
@@ -0,0 +1,4 @@
fastapi
uvicorn
httpx
google-auth