diff --git a/.vscode/settings.json b/.vscode/settings.json index c8087be3..791aba76 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,11 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, "editor.defaultFormatter": "charliermarsh.ruff" }, "[jsonc]": { diff --git a/examples/bareclient/get_gmail_emails.py b/examples/bareclient/get_gmail_emails.py deleted file mode 100644 index 6ddb16d0..00000000 --- a/examples/bareclient/get_gmail_emails.py +++ /dev/null @@ -1,26 +0,0 @@ -from arcadepy import Arcade - -client = Arcade( - base_url="http://localhost:9099", -) - -user_id = "you@example.com" - -# Start the authorization process -auth_response = client.tools.authorize( - tool_name="Google.ListEmails", - user_id=user_id, -) - -if auth_response.status != "completed": - print(f"Click this link to authorize: {auth_response.authorization_url}") - input("After you have authorized, press Enter to continue...") - -inputs = {"n_emails": 5} - -response = client.tools.execute( - tool_name="Google.ListEmails", - inputs=inputs, - user_id=user_id, -) -print(response) diff --git a/examples/bareclient/get_gmail_token.py b/examples/bareclient/get_gmail_token.py deleted file mode 100644 index 5df9ebdc..00000000 --- a/examples/bareclient/get_gmail_token.py +++ /dev/null @@ -1,34 +0,0 @@ -from arcadepy import Arcade -from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2 -from google.oauth2.credentials import Credentials -from googleapiclient.discovery import build - -client = Arcade( - base_url="http://localhost:9099", -) - -user_id = "you@example.com" - -# Start the authorization process -auth_response = client.auth.authorize( - auth_requirement=AuthRequirement( - provider_id="google", - oauth2=AuthRequirementOauth2( - scopes=["https://www.googleapis.com/auth/gmail.readonly"], - ), - ), - user_id=user_id, -) - -if auth_response.status != "completed": - print(f"Click this link to authorize: {auth_response.authorization_url}") - input("After you have authorized, press Enter to continue...") - -# Use the token from the authorization response -creds = Credentials(auth_response.context.token) -service = build("gmail", "v1", credentials=creds) - -# Now you can use the Google API -results = service.users().labels().list(userId="me").execute() -labels = results.get("labels", []) -print("Labels:", labels) diff --git a/examples/call_a_tool_directly.py b/examples/call_a_tool_directly.py new file mode 100644 index 00000000..b1a2db21 --- /dev/null +++ b/examples/call_a_tool_directly.py @@ -0,0 +1,40 @@ +""" +This example demonstrates how to directly call a tool that does not require authorization. +""" + +from arcadepy import Arcade # pip install arcade-py + + +def call_non_auth_tool(client: Arcade, user_id: str) -> None: + """Directly call a prebuilt tool that does not require authorization. + + In this example, we are + 1. Preparing the inputs to the Math.Add tool + 2. Executing the tool + 3. Printing the output of the tool's execution, i.e., the result of adding 9001 and 42 + + This is a simple example of calling a non-auth tool. Next, try writing your own non-auth tool for your own use case. + """ + # Prepare the inputs to the tool as a dictionary where keys are the names of the parameters expected by the tool and the values are the actual values to pass to the tool + inputs = {"a": 9001, "b": 42} + + # Execute the tool + response = client.tools.execute( + tool_name="Math.Add", + inputs=inputs, + user_id=user_id, + ) + + # Print the output of the tool execution + print(response.output.value) + + +if __name__ == "__main__": + cloud_host = "https://api.arcade-ai.com" + + client = Arcade( + base_url=cloud_host, # Alternatively, use http://localhost:9099 if you are running Arcade Engine locally, or any base_url if you're hosting elsewhere + ) + + user_id = "you@example.com" + call_non_auth_tool(client, user_id) diff --git a/examples/call_a_tool_directly_with_auth.py b/examples/call_a_tool_directly_with_auth.py new file mode 100644 index 00000000..0b4b7602 --- /dev/null +++ b/examples/call_a_tool_directly_with_auth.py @@ -0,0 +1,51 @@ +""" +This example demonstrates how to directly call a tool that requires authorization. +""" + +from arcadepy import Arcade # pip install arcade-py + + +def call_auth_tool(client: Arcade, user_id: str) -> None: + """Directly call a prebuilt tool that requires authorization. + + In this example, we are + 1. Authorizing Arcade to read emails from the user's Gmail account with the user's permission to do so + 2. Reading 5 emails from the user's Gmail account + 3. Printing the emails + + Try altering this example to call a tool that requires a different authorization. + """ + # Start the authorization process + auth_response = client.tools.authorize( + tool_name="Google.ListEmails", + user_id=user_id, + ) + + # If not already authorized, then wait for the user to authorize the permissions required by the tool + if auth_response.status != "completed": + print(f"Click this link to authorize: {auth_response.authorization_url}") + + # Wait for the user to complete the auth flow, if necessary + client.auth.wait_for_completion(auth_response) + + # Prepare the inputs to the tool as a dictionary where keys are the names of the parameters expected by the tool and the values are the actual values to pass to the tool + inputs = {"n_emails": 5} + + # Execute the tool + response = client.tools.execute( + tool_name="Google.ListEmails", + inputs=inputs, + user_id=user_id, + ) + + # Print the output of the tool execution. + print(response) + + +if __name__ == "__main__": + client = Arcade( + base_url="https://api.arcade-ai.com", # Alternatively, use http://localhost:9099 if you are running Arcade Engine locally, or any base_url if you're hosting elsewhere + ) + + user_id = "you@example.com" + call_auth_tool(client, user_id) diff --git a/examples/call_a_tool_with_llm.py b/examples/call_a_tool_with_llm.py new file mode 100644 index 00000000..42e75dae --- /dev/null +++ b/examples/call_a_tool_with_llm.py @@ -0,0 +1,45 @@ +""" +This example shows how to call a tool that requires authorization with an LLM using the OpenAI Python client. +""" + +import os + +from openai import OpenAI + + +def call_tool_with_openai(client: OpenAI) -> dict: + response = client.chat.completions.create( + messages=[ + {"role": "user", "content": "Star the ArcadeAI/arcade-ai repository."}, + ], + model="gpt-4o-mini", # TODO: Try "claude-3-5-sonnet-20240620" or other models from our supported model providers. Checkout out our docs for a full list: https://docs.arcade-ai.com/integrations + user="you@example.com", + tools=["Github.SetStarred"], + tool_choice="generate", # TODO: Try "execute" and note any differences + ) + + return response + + +if __name__ == "__main__": + arcade_api_key = os.environ.get( + "ARCADE_API_KEY" + ) # If you forget your Arcade API key, it is stored at ~/.arcade/credentials.yaml on `arcade login` + cloud_host = "https://api.arcade-ai.com" + "/v1" + + openai_client = OpenAI( + api_key=arcade_api_key, + base_url=cloud_host, # Alternatively, use http://localhost:9099/v1 if you are running Arcade Engine locally + ) + + chat_result = call_tool_with_openai(openai_client) + # If the tool call requires authorization, then wait for the user to authorize and then call the tool again + if ( + chat_result.choices[0].tool_authorizations + and chat_result.choices[0].tool_authorizations[0].get("status") == "pending" + ): + print(chat_result.choices[0].message.content) + input("After you have authorized, press Enter to continue...") + chat_result = call_tool_with_openai(openai_client) + + print(chat_result.choices[0].message.content) diff --git a/examples/call_multiple_tools_directly_with_auth.py b/examples/call_multiple_tools_directly_with_auth.py new file mode 100644 index 00000000..48949cbb --- /dev/null +++ b/examples/call_multiple_tools_directly_with_auth.py @@ -0,0 +1,178 @@ +"""Example script demonstrating how to call multiple tools directly with authentication. + +For this example, we are using the prebuilt Spotify toolkit to start playing similar songs to the currently playing song. + +Steps: +1. Get the currently playing song +2. Get audio features for the currently playing song +3. Get song recommendations that are similar to the currently playing song +4. Start playing the recommended songs +5. Inform the user which recommended song is now playing +""" + +from typing import Any, Optional + +from arcadepy import Arcade # pip install arcade-py + + +# Need to click on a link for every provider +def get_permissions(client: Arcade, provider_to_scopes: dict, user_id: str) -> None: + """Prompt the user to authorize necessary permissions for each provider.""" + for provider, scopes in provider_to_scopes.items(): + auth_response = client.auth.start( + user_id=user_id, + provider=provider, + scopes=scopes, + ) + + if auth_response.status != "completed": + print(f"Click this link to authorize: {auth_response.authorization_url}") + input("After you have authorized, press Enter to continue...") + + +def call_tool(client: Arcade, tool_name: str, user_id: str, inputs: Optional[dict] = None) -> Any: + """Call a single tool.""" + if inputs is None: + inputs = {} + + response = client.tools.execute( + tool_name=tool_name, + inputs=inputs, + user_id=user_id, + ) + return response.output.value + + +def recommend_similar_songs( + client: Arcade, provider_to_scopes: dict, tools: list[str], user_id: str, user_country_code: str +) -> None: + """Execute the sequence of tools to get recommendations and start playback.""" + get_permissions(client, provider_to_scopes, user_id) + + recommendation_params = {"seed_genres": []} + + ( + get_currently_playing_tool, + get_tracks_audio_features_tool, + get_recommendations_tool, + start_tracks_playback_tool, + ) = tools + + # Step 1: Get the currently playing song + while True: + response = call_tool(client, get_currently_playing_tool, user_id) + + if response["is_playing"]: + break + + print("Nothing is playing right now. Press Enter once you start playing a song...") + input() + + # Step 1.5: Use the previous tool output to construct the inputs for the following tool calls + current_track_name = response["track_name"] + current_track_artists = response["track_artists"] + current_track_id = response["track_id"] + current_track_spotify_url = response["track_spotify_url"] + current_track_artists_ids = response["track_artists_ids"] + print( + f"\nYou are currently listening to '{current_track_name}' by {', '.join(current_track_artists)}" + ) + recommendation_params.update({ + "seed_tracks": [current_track_id], + "seed_artists": current_track_artists_ids, + }) + + # Step 2:Get audio features for the currently playing song + response = call_tool( + client, + get_tracks_audio_features_tool, + user_id, + inputs={"track_ids": [current_track_id]}, + ) + + # Step 2.5: Use the previous tool output to construct the inputs for the following tool calls + audio_features = response["audio_features"][0] + recommendation_params.update({ + "target_acousticness": float(audio_features["acousticness"]), + "target_danceability": float(audio_features["danceability"]), + "target_energy": float(audio_features["energy"]), + "target_instrumentalness": float(audio_features["instrumentalness"]), + "target_key": int(audio_features["key"]), + "target_liveness": float(audio_features["liveness"]), + "target_loudness": float(audio_features["loudness"]), + "target_mode": int(audio_features["mode"]), + "target_speechiness": float(audio_features["speechiness"]), + "target_tempo": float(audio_features["tempo"]), + "target_time_signature": int(audio_features["time_signature"]), + "target_valence": float(audio_features["valence"]), + }) + + # Step 3: Get song recommendations that are similar to the currently playingsong + print( + f"Getting recommendations similar to '{current_track_name}' by {', '.join(current_track_artists)} - {current_track_spotify_url}" + ) + response = call_tool(client, get_recommendations_tool, user_id, inputs=recommendation_params) + + # Step 3.5: Use the previous tool output to construct the inputs for the following tool calls + # Filter out remixes and the same song from the recommendations + tracks = [ + track for track in response["tracks"] if not track["name"].startswith(current_track_name) + ] + track_ids = [ + track["id"] for track in tracks if (user_country_code in track["available_markets"]) + ] + track_names = [track["name"] for track in tracks] + tracks_artists = [[artist["name"] for artist in track["artists"]] for track in tracks] + track_spotify_urls = [track["external_urls"]["spotify"] for track in tracks] + + if not track_ids: + print("I couldn't find any similar songs that are available in your country.") + return + + print("\nHere are some recommendations:") + for track_name, track_artists, track_spotify_url in zip( + track_names, tracks_artists, track_spotify_urls + ): + print(f"\t{track_name} by {', '.join(track_artists)} - {track_spotify_url}") + + # Step 4: Start playing the recommended songs + response = call_tool( + client, + start_tracks_playback_tool, + user_id, + inputs={"track_ids": track_ids}, + ) + + # Step 5: Inform the user which recommended song is now playing + response = call_tool(client, get_currently_playing_tool, user_id) + print( + f"\nNow playing: {response['track_name']} by {', '.join(response['track_artists'])} - {response['track_spotify_url']}" + ) + + +if __name__ == "__main__": + client = Arcade(base_url="https://api.arcade-ai.com") + + # Necessary scopes for the tools we are calling: + provider_to_scopes = { + "spotify": [ + "user-read-currently-playing", + "user-read-playback-state", + "user-modify-playback-state", + ], + } + + tools = [ + "Spotify.GetCurrentlyPlaying", # Get info about the current song + "Spotify.GetTracksAudioFeatures", # Get audio features for the current song + "Spotify.GetRecommendations", # Get recommendations similar to the current song + "Spotify.StartTracksPlaybackById", # Start playing the recommended songs + ] + + user_id = "you@example.com" + user_country_code = "US" + + while True: + recommend_similar_songs(client, provider_to_scopes, tools, user_id, user_country_code) + print("\nPress Enter to get more recommendations...") + input() diff --git a/examples/call_multiple_tools_with_llm.py b/examples/call_multiple_tools_with_llm.py new file mode 100644 index 00000000..d67093b9 --- /dev/null +++ b/examples/call_multiple_tools_with_llm.py @@ -0,0 +1,91 @@ +""" +Example script demonstrating how to call multiple tools (sequentially) using an LLM with authentication. + +For this example, we are using the prebuilt Spotify toolkit to start playing similar songs to the currently playing song. + +Steps: +1. Get the currently playing song +2. Get audio features for the currently playing song +3. Get song recommendations that are similar to the currently playing song +4. Start playing the recommended songs +5. Inform the user which recommended song is now playing +""" + +import os + +from openai import OpenAI + + +def call_tool(client: OpenAI, user_id: str, tool: str, message: dict, history: list[dict]) -> str: + """Make a tool call with a specific tool and message.""" + response = client.chat.completions.create( + messages=[ + *history, + message, + ], + model="gpt-4o", + user=user_id, + tools=[tool], + tool_choice="generate", + ) + return response + + +def call_tools_with_llm(client: OpenAI, user_id: str, user_country_code: str) -> list[dict]: + """Use an LLM to execute the sequence of tools to get recommendations and start playback.""" + tools = [ + "Spotify.GetCurrentlyPlaying", + "Spotify.GetTracksAudioFeatures", + "Spotify.GetRecommendations", + "Spotify.StartTracksPlaybackById", + "Spotify.GetCurrentlyPlaying", + ] + + messages = [ + {"role": "user", "content": "Get the currently playing song."}, + {"role": "user", "content": "Retrieve its audio features."}, + { + "role": "user", + "content": "Get song recommendations similar to it. " + "Do not include the currently playing song in the recommendations " + "or any remixed versions of the song. " + "Also only include tracks that are available in the user's country. " + f"The current user resides in {user_country_code}.", + }, + {"role": "user", "content": "Start playing the recommended songs. Just one tool call."}, + {"role": "user", "content": "Get the currently playing song."}, + ] + + history = [] + for i in range(len(messages)): + response = call_tool(client, user_id, tools[i], messages[i], history) + print("\n\n", response.choices[0].message.content) + if ( + response.choices[0].tool_authorizations + and response.choices[0].tool_authorizations[0].get("status") == "pending" + ): + input("\nPress Enter once you have authorized...") + response = call_tool(client, user_id, tools[i], messages[i], history) + history.append(messages[i]) + history.append({"role": "assistant", "content": response.choices[0].message.content}) + + return history + + +if __name__ == "__main__": + arcade_api_key = os.environ.get("ARCADE_API_KEY") + cloud_host = "https://api.arcade-ai.com/v1" + + openai_client = OpenAI( + api_key=arcade_api_key, + base_url=cloud_host, + ) + + user_id = "you@example.com" + user_country_code = "US" + + while True: + history = call_tools_with_llm(openai_client, user_id, user_country_code) + + print("\nPress Enter to get more recommendations...") + input() diff --git a/examples/get_auth_token.py b/examples/get_auth_token.py new file mode 100644 index 00000000..364a9a5b --- /dev/null +++ b/examples/get_auth_token.py @@ -0,0 +1,58 @@ +""" +This example demonstrates how to get an authorization token for a user and then use it to make a request to the Google API on behalf of the user. +""" + +from arcadepy import Arcade +from google.oauth2.credentials import Credentials # pip install google-auth +from googleapiclient.discovery import build # pip install google-api-python-client + + +def get_auth_token(client: Arcade, user_id: str) -> str: + """Get an authorization token for a user. + + In this example, we are + 1. Starting the authorization process for the Gmail Readonly scope + 2. Waiting for the user to authorize the scope + 3. Getting the authorization token + 4. Using the authorization token to make a request to the Google API on behalf of the user + """ + # Start the authorization process + auth_response = client.auth.start( + user_id, "google", scopes=["https://www.googleapis.com/auth/gmail.readonly"] + ) + + if auth_response.status != "completed": + print(f"Click this link to authorize: {auth_response.authorization_url}") + auth_response = client.auth.wait_for_completion(auth_response) + + return auth_response.context.token + + +def use_auth_token(token: str) -> None: + """Use an authorization token to make a request to the Google API on behalf of a user. + + In this example, we are + 1. Using the authorization token that we got from the authorization process to make a request to the Google API + client.auth.wait_for_completion(auth_response) + """ + # Use the token from the authorization response + creds = Credentials(token) + service = build("gmail", "v1", credentials=creds) + + # Now you can use the Google API + results = service.users().labels().list(userId="me").execute() + labels = results.get("labels", []) + print("Labels:", labels) + + +if __name__ == "__main__": + cloud_host = "https://api.arcade-ai.com" + + client = Arcade( + base_url=cloud_host, # Alternatively, use http://localhost:9099 if you are running Arcade locally, or any base_url if you're hosting elsewhere + ) + + user_id = "you@example.com" + + token = get_auth_token(client, user_id) + use_auth_token(token) diff --git a/examples/simple_chatbot.py b/examples/simple_chatbot.py new file mode 100644 index 00000000..776d02a0 --- /dev/null +++ b/examples/simple_chatbot.py @@ -0,0 +1,74 @@ +""" +Example script demonstrating how to build a simple chatbot with Arcade AI. + +For this example, we are using the prebuilt Google Docs toolkit to create and edit documents. + +Try asking questions like: +- "Create a document with the title 'My New Document' and content 'Hello, World!'" +- "List my 2 most recently modified documents and tell me the title, document id, and document URL of each one and summarize them." +- "Edit the second document from the list you just returned and add the text 'Hello, World!' to the end of it." +""" + +import os + +from openai import OpenAI + + +def chat(openai_client: OpenAI, tool_names: list[str], user_id: str) -> None: + history = [] + + print("Hello! How can I help you today?") + while True: + message = {"role": "user", "content": input(">")} + history.append(message) + chat_result = call_tool_with_openai(openai_client, tool_names, user_id, history) + + # If the tool call requires authorization, then wait for the user to authorize and then call the tool again + if ( + chat_result.choices[0].tool_authorizations + and chat_result.choices[0].tool_authorizations[0].get("status") == "pending" + ): + print("\n" + chat_result.choices[0].message.content) + input("\nAfter you have authorized, press Enter to continue...") + chat_result = call_tool_with_openai(openai_client, tool_names, user_id, history) + + history.append({"role": "assistant", "content": chat_result.choices[0].message.content}) + + print(chat_result.choices[0].message.content) + + +def call_tool_with_openai( + client: OpenAI, tool_names: list[str], user_id: str, messages: list[dict] +) -> dict: + response = client.chat.completions.create( + messages=messages, + model="gpt-4o-mini", + user=user_id, + tools=tool_names, + tool_choice="generate", + ) + + return response + + +if __name__ == "__main__": + arcade_api_key = os.environ.get( + "ARCADE_API_KEY" + ) # If you forget your Arcade API key, it is stored at ~/.arcade/credentials.yaml on `arcade login` + local_host = "http://localhost:9099/v1" + user_id = "user@example.com" + + openai_client = OpenAI( + api_key=arcade_api_key, + base_url=local_host, + ) + + tool_names = [ + "Google.GetDocumentById", + "Google.InsertTextAtEndOfDocument", + "Google.CreateBlankDocument", + "Google.CreateDocumentFromText", + "Google.ListDocuments", + ] + + chat(openai_client, tool_names, user_id) diff --git a/toolkits/spotify/arcade_spotify/tools/models.py b/toolkits/spotify/arcade_spotify/tools/models.py index fe0f6382..50f93832 100644 --- a/toolkits/spotify/arcade_spotify/tools/models.py +++ b/toolkits/spotify/arcade_spotify/tools/models.py @@ -18,7 +18,9 @@ class PlaybackState: album_spotify_url: Optional[str] = None track_id: Optional[str] = None track_name: Optional[str] = None + track_spotify_url: Optional[str] = None track_artists: list[str] = field(default_factory=list) + track_artists_ids: list[str] = field(default_factory=list) show_id: Optional[str] = None show_spotify_url: Optional[str] = None episode_name: Optional[str] = None diff --git a/toolkits/spotify/arcade_spotify/tools/utils.py b/toolkits/spotify/arcade_spotify/tools/utils.py index cc9d31ed..8e4e7910 100644 --- a/toolkits/spotify/arcade_spotify/tools/utils.py +++ b/toolkits/spotify/arcade_spotify/tools/utils.py @@ -75,7 +75,9 @@ def convert_to_playback_state(data: dict) -> PlaybackState: playback_state.album_spotify_url = album.get("external_urls", {}).get("spotify") playback_state.track_name = item.get("name") playback_state.track_id = item.get("id") + playback_state.track_spotify_url = item.get("external_urls").get("spotify") playback_state.track_artists = [artist.get("name") for artist in item.get("artists", [])] + playback_state.track_artists_ids = [artist.get("id") for artist in item.get("artists", [])] elif data.get("currently_playing_type") == "episode": item = data.get("item", {}) show = item.get("show", {})