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..bd92e5f4e --- /dev/null +++ b/app/en/get-started/agent-frameworks/springai/page.mdx @@ -0,0 +1,444 @@ +--- +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"; + +# Build an AI agent with Arcade and Spring AI + +[Spring AI](https://docs.spring.io/spring-ai/reference/) is a framework that brings AI capabilities to Spring Boot applications. It provides a unified API for interacting with large language models, including support for tool calling. This guide uses **Spring AI 1.1.x** with **Spring Boot 3.5**. + +In this guide, you'll build a Spring Boot web application that uses Arcade's Gmail and Slack tools through Spring AI's `ChatClient`. Your application exposes a REST endpoint that accepts natural language prompts, lets the LLM decide which tools to call, and handles Arcade's authorization flow when users need to connect their accounts. + + + + +Build a Spring Boot web application that integrates Arcade tools with Spring AI for Gmail and Slack access + + + + + +- +- [Java 17+](https://adoptium.net/) +- An [OpenAI API key](https://platform.openai.com/api-keys) +- [Apache Maven](https://maven.apache.org/) or [Gradle](https://gradle.org/) + + + + + +- How to configure the Arcade Spring Boot starter +- How to expose Arcade tools to Spring AI using `@Tool` annotations +- How to build a chat endpoint with tool calling +- How to handle Arcade's authorization flow + + + + +## Spring AI concepts + +Before diving into the code, here are the key Spring AI concepts used in this guide: + +- [ChatClient](https://docs.spring.io/spring-ai/reference/api/chatclient.html) -- A fluent API for interacting with AI models. It handles sending prompts, receiving responses, and coordinating tool calls. +- [@Tool annotation](https://docs.spring.io/spring-ai/reference/api/tools.html) -- Marks a method as a tool that the AI model can call. Spring AI automatically generates the tool schema from the method signature and annotations. +- [OpenAI integration](https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html) -- The `spring-ai-starter-model-openai` starter configures the OpenAI chat model with properties from `application.properties`. + +## Build the agent + + + +### Create a new Spring Boot project + +Go to [start.spring.io](https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.5.11&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=arcade-agent&name=arcade-agent&packageName=com.example.arcadeagent&dependencies=web,spring-ai-openai) and configure the project with the following settings: + +| Setting | Value | +|---------|-------| +| **Project** | Maven (or Gradle) | +| **Language** | Java | +| **Spring Boot** | 3.5.x | +| **Java** | 17 | +| **Dependencies** | Spring Web, OpenAI | + +Click **Generate**, extract the archive, and open the project in your IDE. + + +The Spring Initializr may default to Spring Boot 4.x. Make sure to select a **3.5.x** version from the dropdown. + + +### Add the Arcade dependency + +Add the `arcade-spring-boot-starter` to your project. This starter auto-configures an `ArcadeClient` bean that your application can inject. + + + + + +Add the dependency to your `pom.xml`: + +```xml filename="pom.xml" + + dev.arcade + arcade-spring-boot-starter + ${arcade-java-version} + +``` + + + + + +Add the dependency to your `build.gradle`: + +```groovy filename="build.gradle" +implementation 'dev.arcade:arcade-spring-boot-starter:${arcade-java-version}' +``` + + + + + +See the [Arcade Java SDK docs](https://github.com/ArcadeAI/arcade-java#installation) for the latest version. + +### Configure application properties + +Replace the contents of `src/main/resources/application.properties` with the following: + +```properties filename="application.properties" +spring.application.name=arcade-agent + +# OpenAI +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.options.model=gpt-4o + +# Arcade +arcade.api-key=${ARCADE_API_KEY} +arcade.user-id=${ARCADE_USER_ID} +``` + +Set these environment variables before running the application: + +```bash +export OPENAI_API_KEY=your-openai-api-key +export ARCADE_API_KEY=your-arcade-api-key +export ARCADE_USER_ID=your-email@example.com +``` + +The `ARCADE_USER_ID` is your app's identifier for the current user (often the email you signed up with). Arcade uses this to track authorizations per user. + + +Do not commit API keys to version control. Use environment variables or a secrets manager. + + +### Create the Arcade tool provider + +Create `src/main/java/com/example/arcadeagent/ArcadeToolProvider.java`. This service wraps Arcade tool calls and exposes them to Spring AI using `@Tool` annotations: + +```java filename="src/main/java/com/example/arcadeagent/ArcadeToolProvider.java" +package com.example.arcadeagent; + +import dev.arcade.client.ArcadeClient; +import dev.arcade.models.AuthorizationResponse; +import dev.arcade.models.tools.AuthorizeToolRequest; +import dev.arcade.models.tools.ExecuteToolRequest; +import dev.arcade.models.tools.ExecuteToolResponse; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +// Mark as a Spring-managed bean so it can be injected +// into the ChatController +@Service +public class ArcadeToolProvider { + + private static final Logger log = + LoggerFactory.getLogger(ArcadeToolProvider.class); + + // ArcadeClient is auto-configured by arcade-spring-boot-starter + private final ArcadeClient client; + // Identifies the current user for Arcade authorization tracking + private final String userId; + + ArcadeToolProvider(ArcadeClient client, @Value("${arcade.user-id}") String userId) { + this.client = client; + this.userId = userId; + } + + // --- Tool methods ------------------------------------------------ + // Each @Tool method is exposed to the LLM by Spring AI. + // The LLM reads the name and description to decide + // when to call it. + + @Tool(name = "list_emails", + description = "List recent emails from the user's Gmail inbox") + public String listEmails( + @ToolParam(description = "Search query, e.g. 'in:inbox'") String query) { + return executeTool( + "Gmail.ListEmails", + Map.of("query", query) + ); + } + + @Tool(name = "send_email", description = "Send an email using Gmail") + public String sendEmail( + @ToolParam(description = "Recipient email address") String recipient, + @ToolParam(description = "Email subject line") String subject, + @ToolParam(description = "Email body text") String body) { + return executeTool("Gmail.SendEmail", Map.of( + "recipient", recipient, + "subject", subject, + "body", body + )); + } + + @Tool(name = "send_slack_message", description = "Send a message to a Slack channel") + public String sendSlackMessage( + @ToolParam(description = "The Slack channel name") String channelName, + @ToolParam(description = "The message text to send") String message) { + return executeTool( + "Slack.SendMessage", + Map.of("channel_name", channelName, + "message", message) + ); + } + + // --- Arcade API helpers ------------------------------------------ + + /** + * Calls an Arcade tool and returns the result as a String. + * If the tool requires OAuth authorization, returns an + * authorization URL instead. + */ + private String executeTool(String toolName, Map input) { + // First, ensure the user has authorized this tool + String authResult = handleAuthorization(toolName); + if (authResult != null) { + return authResult; + } + + log.debug("Executing tool {}, input: {}", toolName, input); + try { + // Build and send the execute request to Arcade + ExecuteToolResponse response = client.tools() + .execute(ExecuteToolRequest.builder() + .toolName(toolName) + .userId(userId) + .input(input) + .build()); + + // Happy path: tool executed successfully + if (response.success().orElse(false)) { + String result = response.output() + .map(o -> o._value().toString()) + .orElse("{}"); + log.debug("Tool {} returned: {}", toolName, result); + return result; + } + + // Check for an error in the response + Optional error = + response.output() + .flatMap(ExecuteToolResponse.Output::error); + + if (error.isPresent()) { + String msg = error.get().message(); + log.warn("Tool {} error: {}", toolName, msg); + return "Error: " + msg; + } + + return "Error: tool execution failed"; + } catch (Exception e) { + log.error("Tool {} failed: {}", + toolName, e.getMessage()); + return "Error: " + e.getMessage(); + } + } + + /** + * Checks whether the user has authorized the tool. + * If authorization is already COMPLETED, returns null + * (the caller can proceed). Otherwise, returns a message + * with the authorization URL for the user to visit. + */ + private String handleAuthorization(String toolName) { + try { + // Start the authorization process + AuthorizationResponse authResponse = client.tools() + .authorize(AuthorizeToolRequest.builder() + .toolName(toolName) + .userId(userId) + .build()); + + // Tools that do not require authorization (or that + // the user has already authorized) return COMPLETED. + Optional status = + authResponse.status(); + if (status.isPresent() && AuthorizationResponse + .Status.COMPLETED.equals(status.get())) { + return null; // authorized -- proceed with execution + } + + // Not yet authorized -- return the URL for the user + Optional url = authResponse.url(); + if (url.isPresent()) { + log.debug("Tool {} requires authorization: {}", + toolName, url.get()); + return String.format( + "Authorization required for '%s'. " + + "Open this URL to authorize: %s", + toolName, url.get() + ); + } + + return "Authorization not completed for: " + + toolName; + } catch (Exception e) { + log.error( + "Authorization request failed for {}: {}", + toolName, e.getMessage() + ); + return "Error requesting authorization: " + + e.getMessage(); + } + } +} +``` + +**How this works:** + +- The `ArcadeClient` bean is auto-configured by the `arcade-spring-boot-starter` when `arcade.api-key` is set. +- Each `@Tool` method wraps an Arcade tool call. Spring AI reads these annotations and makes the tools available to the LLM. +- The `executeTool` method first checks authorization, then calls the Arcade API. If authorization status is `COMPLETED`, it proceeds with execution. Otherwise, it returns an authorization URL for the user to visit. + +### Create the chat controller + +Create `src/main/java/com/example/arcadeagent/ChatController.java`. This controller exposes a REST endpoint that accepts user prompts and returns AI responses: + +```java filename="src/main/java/com/example/arcadeagent/ChatController.java" +package com.example.arcadeagent; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ChatController { + + // System prompt that tells the LLM what tools it has + // and how to handle authorization URLs + private static final String SYSTEM_PROMPT = """ + You are a helpful assistant with access to Gmail and + Slack tools. Use the available tools to fulfill user + requests. When a tool returns an authorization URL, + include it in your response so the user can authorize + access. After completing any action, confirm what you + did with specific details."""; + + private final ChatClient chatClient; + + ChatController( + ChatClient.Builder chatClientBuilder, + ArcadeToolProvider toolProvider + ) { + this.chatClient = chatClientBuilder + // Set the system prompt for every conversation + .defaultSystem(SYSTEM_PROMPT) + // Register all @Tool methods from ArcadeToolProvider + .defaultTools(toolProvider) + .build(); + } + + // POST /api/chat -- accepts a user message, returns + // the LLM response (which may include tool results) + @PostMapping("/api/chat") + public ChatResponse chat(@RequestBody ChatRequest request) { + String response = chatClient + .prompt() + .user(request.message()) // the user's message + .call() // send to the LLM + .content(); // extract the text reply + + return new ChatResponse(response); + } + + // Request and response records for the REST endpoint + public record ChatRequest(String message) {} + public record ChatResponse(String response) {} +} +``` + +**How this works:** + +- The `ChatClient` is created with the system prompt and the `ArcadeToolProvider` registered as the default tools. +- When a user sends a message to `POST /api/chat`, the LLM processes it and decides whether to call any tools. +- If the LLM calls a tool (for example, `list_emails`), Spring AI invokes the corresponding `@Tool` method on `ArcadeToolProvider`, which delegates to the Arcade API. +- The response is returned as JSON. + +### Run the application + +Make sure your environment variables are set, then start the application: + + + + + +```bash +./mvnw spring-boot:run +``` + + + + + +```bash +./gradlew bootRun +``` + + + + + +Test the endpoint with `curl`: + +```bash +curl -X POST http://localhost:8080/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "List my recent emails"}' +``` + +On first use, the response will include an authorization URL because the user hasn't connected their Gmail account yet. Open the URL in a browser to authorize, then retry the request: + +```json +{ + "response": "It looks like I need access to your Gmail account. Please open this URL to authorize: https://accounts.google.com/o/oauth2/..." +} +``` + +After authorization, subsequent requests will return results: + +```bash +curl -X POST http://localhost:8080/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "Send a Slack message to #general saying hello from my AI agent"}' +``` + + + +## Key takeaways + +- **The Arcade Spring Boot starter auto-configures the client**: Add `arcade-spring-boot-starter` to your dependencies and set `arcade.api-key` in your configuration. The starter creates an `ArcadeClient` bean you can inject anywhere. +- **`@Tool` annotations bridge Arcade and Spring AI**: Annotate methods on a `@Service` class and pass it to `ChatClient.Builder.defaultTools()`. Spring AI handles the tool schema generation and invocation lifecycle. +- **Authorization is handled in the tool response**: When Arcade returns an "authorization required" error, the tool provider requests an OAuth URL and returns it as part of the response. The LLM includes this URL in its reply to the user. + +## Next steps + +1. **Add more tools**: Browse the [MCP server catalog](/resources/integrations) and add `@Tool` methods for GitHub, Google Docs, Notion, and more. +2. **Add user authentication**: In production, resolve `userId` from your authentication system instead of a static configuration property. See [Security](/guides/security) for best practices. +3. **Stream responses**: Replace `.call()` with `.stream()` on the `ChatClient` to stream responses using [Server-Sent Events](https://docs.spring.io/spring-ai/reference/api/chatclient.html#_streaming_responses). 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