|
| 1 | +# Copyright 2026 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +import os |
| 18 | + |
| 19 | +from google.adk.agents import Agent |
| 20 | +from google.adk.apps import App |
| 21 | +from google.adk.auth.auth_credential import AuthCredential |
| 22 | +from google.adk.auth.auth_tool import AuthConfig |
| 23 | +from google.adk.auth.credential_manager import CredentialManager |
| 24 | +from google.adk.integrations.agent_identity import GcpAuthProvider |
| 25 | +from google.adk.integrations.agent_identity import GcpAuthProviderScheme |
| 26 | +from google.adk.tools.authenticated_function_tool import AuthenticatedFunctionTool |
| 27 | +from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams |
| 28 | +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset |
| 29 | +import httpx |
| 30 | + |
| 31 | +PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT") |
| 32 | +LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION") |
| 33 | +MAPS_API_AUTH_PROVIDER_ID = os.environ.get("MAPS_API_AUTH_PROVIDER_ID") |
| 34 | +SPOTIFY_2LO_AUTH_PROVIDER_ID = os.environ.get("SPOTIFY_2LO_AUTH_PROVIDER_ID") |
| 35 | +SPOTIFY_3LO_AUTH_PROVIDER_ID = os.environ.get("SPOTIFY_3LO_AUTH_PROVIDER_ID") |
| 36 | + |
| 37 | +MAPS_API_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{MAPS_API_AUTH_PROVIDER_ID}" |
| 38 | +SPOTIFY_2LO_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{SPOTIFY_2LO_AUTH_PROVIDER_ID}" |
| 39 | +SPOTIFY_3LO_AUTH_PROVIDER = f"projects/{PROJECT_ID}/locations/{LOCATION}/connectors/{SPOTIFY_3LO_AUTH_PROVIDER_ID}" |
| 40 | + |
| 41 | +MAPS_MCP_ENDPOINT = "https://mapstools.googleapis.com/mcp" |
| 42 | +CONTINUE_URI = "http://localhost:8080/commit" |
| 43 | +MODEL = "gemini-2.5-flash" |
| 44 | + |
| 45 | + |
| 46 | +async def spotify_search_track( |
| 47 | + credential: AuthCredential, query: str |
| 48 | +) -> str | list: |
| 49 | + """Searches for a track on Spotify and returns its details.""" |
| 50 | + headers = {} |
| 51 | + if http := credential.http: |
| 52 | + if http.scheme and http.credentials and (token := http.credentials.token): |
| 53 | + headers["Authorization"] = f"{http.scheme.title()} {token}" |
| 54 | + if http.additional_headers: |
| 55 | + headers.update(http.additional_headers) |
| 56 | + |
| 57 | + if not headers: |
| 58 | + return "Error: No authentication token available." |
| 59 | + |
| 60 | + async with httpx.AsyncClient() as client: |
| 61 | + response = await client.get( |
| 62 | + "https://api.spotify.com/v1/search", |
| 63 | + headers=headers, |
| 64 | + params={"q": query, "type": "track", "limit": 1}, |
| 65 | + ) |
| 66 | + |
| 67 | + if response.status_code != 200: |
| 68 | + return f"Error from Spotify API: {response.status_code} - {response.text}" |
| 69 | + |
| 70 | + data = response.json() |
| 71 | + items = data.get("tracks", {}).get("items", []) |
| 72 | + |
| 73 | + if not items: |
| 74 | + return f"No track found for query '{query}'." |
| 75 | + |
| 76 | + return items |
| 77 | + |
| 78 | + |
| 79 | +async def spotify_get_playlists(credential: AuthCredential) -> str | list: |
| 80 | + """Fetches the current user's private playlists on Spotify.""" |
| 81 | + headers = {} |
| 82 | + if http := credential.http: |
| 83 | + if http.scheme and http.credentials and (token := http.credentials.token): |
| 84 | + headers["Authorization"] = f"{http.scheme.title()} {token}" |
| 85 | + if http.additional_headers: |
| 86 | + headers.update(http.additional_headers) |
| 87 | + |
| 88 | + if not headers: |
| 89 | + return "Error: No authentication token available." |
| 90 | + |
| 91 | + async with httpx.AsyncClient() as client: |
| 92 | + response = await client.get( |
| 93 | + "https://api.spotify.com/v1/me/playlists", |
| 94 | + headers=headers, |
| 95 | + params={"limit": 10}, |
| 96 | + ) |
| 97 | + |
| 98 | + if response.status_code != 200: |
| 99 | + return f"Error from Spotify API: {response.status_code} - {response.text}" |
| 100 | + |
| 101 | + data = response.json() |
| 102 | + items = data.get("items", []) |
| 103 | + |
| 104 | + if not items: |
| 105 | + return "No playlists found for the current user." |
| 106 | + |
| 107 | + # Extract useful information |
| 108 | + return [ |
| 109 | + { |
| 110 | + "name": item.get("name"), |
| 111 | + "public": item.get("public"), |
| 112 | + "total_tracks": item.get("tracks", {}).get("total"), |
| 113 | + } |
| 114 | + for item in items |
| 115 | + if item |
| 116 | + ] |
| 117 | + |
| 118 | + |
| 119 | +spotify_auth_config_2lo = AuthConfig( |
| 120 | + auth_scheme=GcpAuthProviderScheme(name=SPOTIFY_2LO_AUTH_PROVIDER) |
| 121 | +) |
| 122 | +spotify_search_track_tool = AuthenticatedFunctionTool( |
| 123 | + func=spotify_search_track, |
| 124 | + auth_config=spotify_auth_config_2lo, |
| 125 | +) |
| 126 | + |
| 127 | +spotify_auth_config_3lo = AuthConfig( |
| 128 | + auth_scheme=GcpAuthProviderScheme( |
| 129 | + name=SPOTIFY_3LO_AUTH_PROVIDER, |
| 130 | + scopes=["playlist-read-private"], |
| 131 | + continue_uri=CONTINUE_URI, |
| 132 | + ) |
| 133 | +) |
| 134 | +spotify_get_playlist_tool = AuthenticatedFunctionTool( |
| 135 | + func=spotify_get_playlists, |
| 136 | + auth_config=spotify_auth_config_3lo, |
| 137 | +) |
| 138 | + |
| 139 | +maps_tools = McpToolset( |
| 140 | + connection_params=StreamableHTTPConnectionParams(url=MAPS_MCP_ENDPOINT), |
| 141 | + auth_scheme=GcpAuthProviderScheme(name=MAPS_API_AUTH_PROVIDER), |
| 142 | + errlog=None, # Required for agent freezing (pickling) |
| 143 | +) |
| 144 | + |
| 145 | +CredentialManager.register_auth_provider(GcpAuthProvider()) |
| 146 | + |
| 147 | +root_agent = Agent( |
| 148 | + name="gcp_auth_agent", |
| 149 | + model=MODEL, |
| 150 | + instruction=( |
| 151 | + "You are a Spotify and Google Maps assistant. Use your tools to " |
| 152 | + "search for track details, fetch the user's private playlists, " |
| 153 | + "and look up locations. Keep responses concise, friendly, and " |
| 154 | + "emoji-filled!" |
| 155 | + ), |
| 156 | + tools=[ |
| 157 | + spotify_search_track_tool, |
| 158 | + spotify_get_playlist_tool, |
| 159 | + maps_tools, |
| 160 | + ], |
| 161 | +) |
| 162 | + |
| 163 | +app = App( |
| 164 | + name="gcp_auth", |
| 165 | + root_agent=root_agent, |
| 166 | +) |
0 commit comments