From e23522459976bbdcc83db0838ae642912a5888b1 Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Sat, 28 Feb 2026 00:37:19 -0500 Subject: [PATCH] [Draft] Initial pass at adding docs for arcade-java TODO: - [ ] Add Spring AI guide --- app/_components/agent-framework-tabs.tsx | 15 +- app/en/get-started/agent-frameworks/_meta.tsx | 3 + .../agent-frameworks/springai/page.mdx | 7 + .../quickstarts/call-tool-agent/page.mdx | 158 +++++++++++++++++- .../call-third-party-apis/page.mdx | 144 ++++++++++++++-- .../tool-calling/error-handling/page.mdx | 91 +++++++++- app/en/references/page.mdx | 34 ++++ public/images/icons/spring-ai.svg | 29 ++++ 8 files changed, 460 insertions(+), 21 deletions(-) create mode 100644 app/en/get-started/agent-frameworks/springai/page.mdx create mode 100644 public/images/icons/spring-ai.svg diff --git a/app/_components/agent-framework-tabs.tsx b/app/_components/agent-framework-tabs.tsx index ef7b703b0..7163bd4bb 100644 --- a/app/_components/agent-framework-tabs.tsx +++ b/app/_components/agent-framework-tabs.tsx @@ -3,7 +3,10 @@ import { PlatformCard } from "@/app/_components/platform-card"; export function AgentFrameworkTabs() { return ( - +
+ +
+ +
+
); } diff --git a/app/en/get-started/agent-frameworks/_meta.tsx b/app/en/get-started/agent-frameworks/_meta.tsx index 7c18310dc..3eae42bb6 100644 --- a/app/en/get-started/agent-frameworks/_meta.tsx +++ b/app/en/get-started/agent-frameworks/_meta.tsx @@ -28,6 +28,9 @@ export const meta: MetaRecord = { vercelai: { title: "Vercel AI SDK", }, + springai: { + title: "Spring AI SDK", + }, }; export default meta; diff --git a/app/en/get-started/agent-frameworks/springai/page.mdx b/app/en/get-started/agent-frameworks/springai/page.mdx new file mode 100644 index 000000000..09531d7f9 --- /dev/null +++ b/app/en/get-started/agent-frameworks/springai/page.mdx @@ -0,0 +1,7 @@ +--- +title: "Build an AI Agent with Arcade and Spring AI" +description: "Learn how to call Arcade tools from Spring AI." +--- + +import { Steps, Tabs, Callout } from "nextra/components"; + diff --git a/app/en/get-started/quickstarts/call-tool-agent/page.mdx b/app/en/get-started/quickstarts/call-tool-agent/page.mdx index f665dbb02..7cb42d0c0 100644 --- a/app/en/get-started/quickstarts/call-tool-agent/page.mdx +++ b/app/en/get-started/quickstarts/call-tool-agent/page.mdx @@ -24,6 +24,7 @@ Install and use the Arcade client to call Arcade Hosted Tools. - An [Arcade API key](/get-started/setup/api-keys) - The [`uv` package manager](https://docs.astral.sh/uv/getting-started/installation/) if you are using Python - The [`bun` runtime](https://bun.com/) if you are using TypeScript +- [Apache Maven](https://maven.apache.org/) or [Gradle](https://gradle.org/) if you are using Java @@ -44,7 +45,7 @@ Install and use the Arcade client to call Arcade Hosted Tools. ### Install the Arcade client - + In your terminal, run the following command to create a new `uv` project: @@ -95,12 +96,34 @@ bun install @arcadeai/arcadejs ``` + +Create a new Maven or Gradle project with the following dependency: + + +#### Gradle: + +```groovy filename="build.gradle.kts" +implementation("dev.arcade:arcade-java:${arcade-java-version}") +``` + +#### Maven: +```xml filename="pom.xml" + + dev.arcade + arcade-java + ${arcade-java-version} + +``` + +See the [Arcade Java SDK docs](https://github.com/ArcadeAI/arcade-java?tab=readme-ov-file#installation) for the latest version. + + ### Setup the client - + @@ -137,14 +160,24 @@ let userId = "{arcade_user_id}"; ``` + +```java +// You can also set the `ARCADE_API_KEY` environment variable instead of passing it as a +// parameter by using ArcadeOkHttpClient.fromEnv(). +ArcadeClient client = ArcadeOkHttpClient.builder().apiKey("{arcade_api_key}").build(); +// Arcade needs a unique identifier for your application user (this could be an email address, a UUID, etc). +// In this example, use the email you used to sign up for Arcade.dev: +String userId = "{arcade_user_id}"; +``` + ### Write a helper function to authorize and run tools This helper function will check if a tool requires authorization and if so, it will print the authorization URL and wait for the user to authorize the tool call. If the tool does not require authorization, it will run the tool directly without interrupting the flow. - + @@ -207,6 +240,46 @@ async function authorize_and_run_tool({ } ``` + + +```java filename="App.java" + public static Map authorizeAndRunTool(ArcadeClient client, + String toolName, + Map input, + String userId) { + // Start the authorization process + AuthorizationResponse authResponse = client.tools().authorize( + AuthorizeToolRequest.builder() + .toolName(toolName) + .userId(userId) + .build()); + + // If the authorization is not completed, print the authorization URL and wait for the user to authorize the app. + // Tools that do not require authorization will have the status "completed" already. + authResponse + .status() + .filter(status -> status != AuthorizationResponse.Status.COMPLETED) + .flatMap(status -> authResponse.url()) + .ifPresent(url -> logger.info( + """ + Click this link to authorize {}: + {}. + The process will continue once you have authorized the app. + """, toolName, url)); + client.auth().waitForCompletion(authResponse); + + // Execute the tool and extract the output as a Map + return client.tools().execute(ExecuteToolRequest.builder() + .toolName(toolName) + .input(input) + .userId(userId) + .includeErrorStacktrace(true) + .build()) + .output() + .flatMap(Output::valueAsObject) + .orElse(Map.of()); +} +``` @@ -218,7 +291,7 @@ In this example workflow, we: - Create a Google Doc with the news - Send a link to the Google Doc to the user - + @@ -335,12 +408,68 @@ console.log(respose_send_email.output?.value); ``` +```java filename="App.java" +Map searchResult = authorizeAndRunTool( + client, + "GoogleNews.SearchNewsStories", + Map.of("keywords", "MCP URL mode elicitation"), + userId); + +// Extract the list of news results from the tool output +List news = searchResult + .getOrDefault("news_results", JsonValue.from(List.of())) + .toListOrEmpty(); + +String output = "latest news about MCP URL mode elicitation:\n" + + news.stream() + .map(item -> { + Map newsItem = item.toMapOrEmpty(); + return newsItem.get("source").asStringOrThrow() + " - " + + newsItem.get("title").asStringOrThrow() + "\n" + + newsItem.get("link").asStringOrThrow() + "\n\n"; + }) + .collect(Collectors.joining("\n")); + +// Create a Google Doc with the news results +// If the user has not previously authorized the Google Docs tool, they will be prompted to authorize the tool +// call. +Map createDocResult = authorizeAndRunTool( + client, + "GoogleDocs.CreateDocumentFromText", + Map.of("title", "News about MCP URL mode elicitation", "text_content", output), + userId); + +String googleDocUrl = createDocResult.get("documentUrl").asStringOrThrow(); + +String emailBody = + "You can find the news about MCP URL mode elicitation in the following Google Doc: " + googleDocUrl; +Map sendEmailResult = authorizeAndRunTool( + client, + "Gmail.SendEmail", + Map.of("recipient", userId, "subject", "News about MCP URL mode elicitation", "body", emailBody), + userId); + +// Print the response from the tool call +logger.info( + """ + Success! Check your email at {} + + You just chained 3 tools together: + 1. Searched Google News for stories about MCP URL mode elicitation + 2. Created a Google Doc with the results + 3. Sent yourself an email with the document link + + Email metadata: {} + """, + userId, + sendEmailResult); +``` ### Run the code - + @@ -385,6 +514,23 @@ console.log(respose_send_email.output?.value); ``` + + + Run your Java application, you should see output similar to: + + ```text + Success! Check your email at brian.demers@gmail.com + + You just chained 3 tools together: + 1. Searched Google News for stories about MCP URL mode elicitation + 2. Created a Google Doc with the results + 3. Sent yourself an email with the document link + + Email metadata: {id=19ba..., label_ids=[UNREAD, SENT, INBOX], thread_id=19ba..., url=https://mail.google.com/mail/u/0/#sent/19ba...} + ``` + + For a full example, see the [Arcade Java SDK project](https://github.com/ArcadeAI/arcade-java/tree/main/arcade-java-example); + @@ -397,7 +543,7 @@ In this example, we call the tool methods directly. In your real applications an ## Full Example Code - + diff --git a/app/en/guides/tool-calling/call-third-party-apis/page.mdx b/app/en/guides/tool-calling/call-third-party-apis/page.mdx index d8c6a56b5..cbf5e92b5 100644 --- a/app/en/guides/tool-calling/call-third-party-apis/page.mdx +++ b/app/en/guides/tool-calling/call-third-party-apis/page.mdx @@ -25,14 +25,52 @@ This can be useful when you need to manage authorization flows in your applicati ### Install required libraries - + ```bash pip install arcadepy google-api-python-client google-auth-httplib2 google-auth-oauthlib ``` ```bash npm install @arcadeai/arcadejs googleapis ``` + + +#### Gradle: + +```groovy +implementation("dev.arcade:arcade-java:${arcade-java-version}") +implementation("com.google.api-client:google-api-client:2.9.0") +implementation("com.google.oauth-client:google-oauth-client-jetty:1.39.0") +implementation("com.google.apis:google-api-services-gmail:v1-rev20260112-2.0.0") +``` + +#### Maven: + +```xml + + dev.arcade + arcade-java + ${arcade-java-version} + + + com.google.api-client + google-api-client + 2.9.0 + + + com.google.oauth-client + google-oauth-client-jetty + 1.39.0 + + + com.google.apis + google-api-services-gmail + v1-rev20260112-2.0.0 + +``` + +See the [Arcade Java SDK docs](https://github.com/ArcadeAI/arcade-java?tab=readme-ov-file#installation) for the latest version. + ### Start coding - + Create a new file `direct_api_call.py` and import all libraries we're going to use: @@ -51,6 +89,27 @@ import { Arcade } from "@arcadeai/arcadejs"; import { google } from "googleapis"; ``` + + + +Create a new file `GmailExample.java` and import all packages we're going to use: + +```java +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.services.gmail.Gmail; +import com.google.api.services.gmail.model.Message; +import dev.arcade.client.ArcadeClient; +import dev.arcade.client.okhttp.ArcadeOkHttpClient; +import dev.arcade.models.AuthorizationContext; +import dev.arcade.models.AuthorizationResponse; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +``` @@ -58,7 +117,7 @@ import { google } from "googleapis"; Create an instance of the Arcade client: - + ```python client = Arcade() # Automatically finds the `ARCADE_API_KEY` env variable @@ -69,15 +128,21 @@ client = Arcade() # Automatically finds the `ARCADE_API_KEY` env variable const client = new Arcade(); // Automatically finds the `ARCADE_API_KEY` env variable ``` + + +```java +// Automatically finds the `ARCADE_API_KEY` env variable +ArcadeClient client = ArcadeOkHttpClient.fromEnv(); +``` ### Initiate an authorization request + + Use `client.auth.start()` to initiate the authorization process: - - ```python # This would be your app's internal ID for the user (an email, UUID, etc.) user_id = "{arcade_user_id}" @@ -90,9 +155,11 @@ provider="google", scopes=["https://www.googleapis.com/auth/gmail.readonly"], ) -```` +``` +Use `client.auth.start()` to initiate the authorization process: + ```javascript // Your app's internal ID for the user (an email, UUID, etc). // It's used internally to identify your user in Arcade, not to identify with the Gmail service. @@ -103,8 +170,21 @@ const user_id = "{arcade_user_id}"; let auth_response = await client.auth.start(user_id, "google", { scopes: ["https://www.googleapis.com/auth/gmail.readonly"], }); -```` +``` + + +Use `client.auth().start()` to initiate the authorization process: + +```java +// Your app's internal ID for the user (an email, UUID, etc). +// It's used internally to identify your user in Arcade, not to identify with the Gmail service. +// Use your Arcade account email for testing: +String userId = "{arcade_user_id}"; + +AuthorizationResponse authResponse = client.auth().start( userId, "google", "oauth2", + List.of("https://www.googleapis.com/auth/gmail.readonly")); +``` @@ -112,7 +192,7 @@ let auth_response = await client.auth.start(user_id, "google", { If authorization is not completed, prompt the user to visit the authorization URL: - + ```python if auth_response.status != "completed": @@ -128,12 +208,22 @@ if (auth_response.status !== "completed") { } ``` + + +```java +// check the response status +authResponse + .status() + .filter(status -> status != AuthorizationResponse.Status.COMPLETED) + .flatMap(status -> authResponse.url()) + .ifPresent(url -> logger.info("Click this link to authorize: {}", url)); +``` ### Wait for the user to authorize the request - + ```python # Wait for the authorization to complete @@ -146,6 +236,11 @@ auth_response = client.auth.wait_for_completion(auth_response) auth_response = await client.auth.waitForCompletion(auth_response); ``` + + +```java +AuthorizationResponse completedAuthResponse = client.auth().waitForCompletion(authResponse); +``` @@ -153,7 +248,7 @@ auth_response = await client.auth.waitForCompletion(auth_response); Once authorization is complete, you can use the obtained token to access the third-party service: - + ```python credentials = Credentials(auth_response.context.token) @@ -165,7 +260,7 @@ gmail.users().messages().list(userId="me").execute().get("messages", []) print(email_messages) -```` +``` ```javascript @@ -180,16 +275,39 @@ const response = await gmail.users.messages.list({ const email_messages = response.data.messages || []; console.log(email_messages); -```` +``` + + + +```java +// Use the credential +String token = completedAuthResponse.context().flatMap(AuthorizationContext::token).get(); +JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); +HttpTransport httpTransport = new NetHttpTransport(); +GoogleCredential credential = new GoogleCredential().setAccessToken(token); + +Gmail gmail = new Gmail.Builder(httpTransport, jsonFactory, credential) + .setApplicationName("Your Application Name") + .build(); + +// List email messages +List messages = gmail.users().messages().list("me").execute().getMessages(); +logger.info("Messages: {}", messages); +``` ### Execute the code - + ```python python3 direct_api_call.py ``` ```javascript node direct_api_call.js ``` + + Run the `GmailExample.java` class. + + See the [Arcade Java SDK Examples](https://github.com/ArcadeAI/arcade-java/tree/next/arcade-java-example/src/main/java/dev/arcade/example) for the full class. + You should see an output similar to this:, which is a list of the email messages returned by the Gmail API: diff --git a/app/en/guides/tool-calling/error-handling/page.mdx b/app/en/guides/tool-calling/error-handling/page.mdx index 097f906a8..ce5a2f65b 100644 --- a/app/en/guides/tool-calling/error-handling/page.mdx +++ b/app/en/guides/tool-calling/error-handling/page.mdx @@ -20,7 +20,7 @@ Arcade's error handling is designed to provide you with as much information as p Here's how to handle different types of output errors when executing tools with Arcade's client libraries: - + ```python """ @@ -155,6 +155,95 @@ if (response.output.error) { } ``` + +```java +package dev.arcade.example.docs; + +import dev.arcade.client.ArcadeClient; +import dev.arcade.client.okhttp.ArcadeOkHttpClient; +import dev.arcade.models.AuthorizationResponse; +import dev.arcade.models.tools.AuthorizeToolRequest; +import dev.arcade.models.tools.ExecuteToolRequest; +import dev.arcade.models.tools.ExecuteToolResponse; +import dev.arcade.models.tools.ExecuteToolResponse.Output.Error.Kind; +import java.time.Duration; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + /** + * This example demonstrates how to handle different kinds of output errors when executing a tool. + */ + public class HandlingErrors { + + private static final Logger logger = LoggerFactory.getLogger(HandlingErrors.class); + + static void handleToolError(ExecuteToolResponse.Output.Error error) { + Kind errorKind = error.kind(); + if (errorKind == Kind.TOOL_RUNTIME_BAD_INPUT_VALUE) { + // You provided the executed tool with an invalid input value + logger.error(error.message()); + } else if (errorKind == Kind.TOOL_RUNTIME_RETRY) { + // The tool returned a retryable error. Provide the additional + // prompt content to the LLM and retry the tool call + error.additionalPromptContent().ifPresent(logger::error); + } else if (errorKind == Kind.TOOL_RUNTIME_CONTEXT_REQUIRED) { + // The tool requires extra context from the user or orchestrator. + // Provide the additional prompt content to them and then retry the + // tool call with the new context + error.additionalPromptContent().ifPresent(logger::error); + } else if (errorKind == Kind.TOOL_RUNTIME_FATAL) { + // The tool encountered a fatal error during execution + logger.error(error.message()); + } else if (errorKind == Kind.UPSTREAM_RUNTIME_RATE_LIMIT) { + // The tool encountered a rate limit error from an upstream service + // Wait for the specified amount of time and then retry the tool call + Duration timeToWait = error.retryAfterMs().map(Duration::ofMillis).orElse(Duration.ofMillis(500)); + logger.error("Wait for {} before retrying the tool call", timeToWait); + } else if (errorKind.asString().startsWith("UPSTREAM_")) { + // The tool encountered an error from an upstream service + logger.error(error.message()); + } +} + + public static void main(String[] args) { + + ArcadeClient client = ArcadeOkHttpClient.fromEnv(); // Automatically finds the `ARCADE_API_KEY` env variable + String userId = "{arcade_user_id}"; + String toolName = "Reddit.GetPostsInSubreddit"; + + // Go through the OAuth flow for the tool + AuthorizationResponse authResponse = client.tools() + .authorize(AuthorizeToolRequest.builder() + .toolName(toolName) + .userId(userId) + .build()); + + authResponse + .status() + .filter(status -> status != AuthorizationResponse.Status.COMPLETED) + .flatMap(status -> authResponse.url()) + .ifPresent(url -> logger.info("Click this link to authorize: {}", url)); + + logger.debug("Authorization response: {}", authResponse.status()); + client.auth().waitForCompletion(authResponse); + + // Execute the tool + ExecuteToolResponse toolResponse = client.tools() + .execute(ExecuteToolRequest.builder() + .toolName(toolName) + .input(Map.of("subreddit", "programming", "limit", 1)) + .userId(userId) + .includeErrorStacktrace(true) + .build()); + + toolResponse.output() + .flatMap(ExecuteToolResponse.Output::error) + .ifPresent(HandlingErrors::handleToolError); + } +} +``` + ## Error types in Arcade client libraries diff --git a/app/en/references/page.mdx b/app/en/references/page.mdx index ac75df1fc..09b15b026 100644 --- a/app/en/references/page.mdx +++ b/app/en/references/page.mdx @@ -130,6 +130,40 @@ Complete reference documentation for Arcade's APIs, MCP servers, and available a Learn more about the Go Client + {/* Go Client */} +
+

+ Java Client +

+

+ With Gradle: +

+ + ```kotlin filename="build.gradle.kts" + implementation("dev.arcade:arcade-java:${arcade-java-version}") + ``` + +

+ With Maven: +

+ + ```xml filename="pom.xml" + + dev.arcade + arcade-java + ${arcade-java-version} + + ``` + + + Learn more and find the latest version for the Java Client + +

diff --git a/public/images/icons/spring-ai.svg b/public/images/icons/spring-ai.svg new file mode 100644 index 000000000..0c8c3fd08 --- /dev/null +++ b/public/images/icons/spring-ai.svg @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file