From 7f25c136ff1211e7da4acddc857841a94e54db14 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Sat, 6 Dec 2025 17:12:35 -0800 Subject: [PATCH 01/10] initial stab --- app/en/home/_meta.tsx | 3 + .../home/connect-arcade-to-your-llm/page.mdx | 204 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 app/en/home/connect-arcade-to-your-llm/page.mdx diff --git a/app/en/home/_meta.tsx b/app/en/home/_meta.tsx index ff7c4d069..fdfa7192d 100644 --- a/app/en/home/_meta.tsx +++ b/app/en/home/_meta.tsx @@ -119,6 +119,9 @@ export const meta: MetaRecord = { type: "separator", title: "Guides", }, + "connect-arcade-to-your-llm": { + title: "Integrate Arcade tools into your LLM application", + }, glossary: { title: "Glossary", }, diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx new file mode 100644 index 000000000..68996d9c0 --- /dev/null +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -0,0 +1,204 @@ +--- +title: "Connect Arcade to your LLM" +description: "Learn how to connect Arcade to your LLM" +--- + +import { Steps, Tabs, Callout } from "nextra/components"; +import { SignupLink } from "@/app/_components/analytics"; + +# Connect Arcade to your LLM + + + + +Integrate Arcade tool-calling capabilities into an application that uses an LLM, with or without an orchestration framework. + + + + + +- An Arcade account +- An [Arcade API key](/home/api-keys) +- The [`uv` package manager](https://docs.astral.sh/uv/getting-started/installation/) + + + + + +- Install the Arcade client +- Execute your first tool using the Arcade client +- Authorize a tool to star a GitHub repository on your behalf + + + + + + +### Install the Arcade client + + + + + In your terminal, run the following command to install the Python client package `arcadepy`: + +```bash +uv pip install arcadepy +``` + + + + + In your terminal, run the following command to install the JavaScript client package `@arcadeai/arcadejs`: + +```bash +npm install @arcadeai/arcadejs +``` + + + + + +### Instantiate and use the client + + + + + +Create a new script called `example.py`: + +```python filename="example.py" +from arcadepy import Arcade + +# You can also set the `ARCADE_API_KEY` environment variable instead of passing it as a parameter. + +client = Arcade(api_key="{arcade_api_key}") + +# 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: + +user_id = "{arcade_user_id}" + +# Let's use the `Math.Sqrt` tool from the Arcade Math MCP Server to get the square root of a number. + +response = client.tools.execute( + tool_name="Math.Sqrt", + input={"a": '625'}, + user_id=user_id, +) + +print(f"The square root of 625 is {response.output.value}") + +# Now, let's use a tool that requires authentication to star a GitHub repository + +auth_response = client.tools.authorize( +tool_name="GitHub.SetStarred", +user_id=user_id, +) + +if auth_response.status != "completed": + print(f"Click this link to authorize: `{auth_response.url}`. The process will continue once you have authorized the app." ) # Wait for the user to authorize the app + client.auth.wait_for_completion(auth_response.id); + +response = client.tools.execute( + tool_name="GitHub.SetStarred", + input={ + "owner": "ArcadeAI", + "name": "arcade-mcp", + "starred": True, + }, + user_id=user_id, +) + +print(response.output.value) + +``` + + + + + +Create a new script called `example.mjs`: + +```javascript filename="example.mjs" +import Arcade from "@arcadeai/arcadejs"; + +// You can also set the `ARCADE_API_KEY` environment variable instead of passing it as a parameter. +const client = new Arcade({ + apiKey: "{arcade_api_key}", +}); + +// 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: +let userId = "{arcade_user_id}"; + +// Let's use the `Math.Sqrt` tool from the Arcade Math MCP Server to get the square root of a number. +const response_sqrt = await client.tools.execute({ + tool_name: "Math.Sqrt", + input: { a: "625" }, + user_id: userId, +}); + +console.log(response_sqrt.output.value); + +// Now, let's use a tool that requires authentication + +const authResponse = await client.tools.authorize({ + tool_name: "GitHub.SetStarred", + user_id: userId, +}); + +if (authResponse.status !== "completed") { + console.log( + `Click this link to authorize: \`${authResponse.url}\`. The process will continue once you have authorized the app.` + ); + // Wait for the user to authorize the app + await client.auth.waitForCompletion(authResponse.id); +} + +const response_github = await client.tools.execute({ + tool_name: "GitHub.SetStarred", + input: { + owner: "ArcadeAI", + name: "arcade-mcp", + starred: true, + }, + user_id: userId, +}); + +console.log(response_github.output.value); +``` + + + + + +### Run the code + + + + + +```bash +uv run example.py +> The square root of 625 is 25 +> Successfully starred the repository ArcadeAI/arcade-mcp +``` + + + + + ```bash + node example.mjs + > The square root of 625 is 25 + > Successfully starred the repository ArcadeAI/arcade-mcp + ``` + + + + + + +## Next Steps + +In this simple example, we call the tool methods directly. In your real applications and agents, you'll likely be letting the LLM decide which tools to call - learn more about using Arcade with Frameworks in the [Frameworks](/home/langchain/use-arcade-tools) section, or [how to build your own tools](/home/build-tools/create-a-mcp-server). From c7a1a3131ff08cea2640fbcaacc5c80bc138754b Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Mon, 8 Dec 2025 09:34:56 -0800 Subject: [PATCH 02/10] prereqs --- app/en/home/connect-arcade-to-your-llm/page.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 68996d9c0..a0aa04f78 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -25,9 +25,9 @@ Integrate Arcade tool-calling capabilities into an application that uses an LLM, -- Install the Arcade client -- Execute your first tool using the Arcade client -- Authorize a tool to star a GitHub repository on your behalf +- Setup a simple agentic loop +- Add Arcade tools to your agentic loop +- Implement a multi-turn conversation loop From e61487f299537d90b437e123a41ae7733e763ee0 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Wed, 10 Dec 2025 13:23:10 -0800 Subject: [PATCH 03/10] added OpenRouter tutorial for wiring Arcade with any LLM --- .../home/connect-arcade-to-your-llm/page.mdx | 324 +++++++++++------- 1 file changed, 209 insertions(+), 115 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index a0aa04f78..6340dd418 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -11,7 +11,7 @@ import { SignupLink } from "@/app/_components/analytics"; -Integrate Arcade tool-calling capabilities into an application that uses an LLM, with or without an orchestration framework. +Integrate Arcade tool-calling capabilities into an application that uses an LLM in Python. @@ -19,6 +19,7 @@ Integrate Arcade tool-calling capabilities into an application that uses an LLM, - An Arcade account - An [Arcade API key](/home/api-keys) +- An [OpenRouter API key](https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys) - The [`uv` package manager](https://docs.astral.sh/uv/getting-started/installation/) @@ -34,171 +35,264 @@ Integrate Arcade tool-calling capabilities into an application that uses an LLM, -### Install the Arcade client +### Create a new project and install the dependencies - - - - In your terminal, run the following command to install the Python client package `arcadepy`: +In your terminal, run the following command to create a new `uv` project ```bash -uv pip install arcadepy +mkdir arcade-llm-example +cd arcade-llm-example +uv init ``` - - - - In your terminal, run the following command to install the JavaScript client package `@arcadeai/arcadejs`: +Create a new virtual environment and activate it: ```bash -npm install @arcadeai/arcadejs +uv venv +source .venv/bin/activate ``` - - - - -### Instantiate and use the client - - - - +Install the dependencies: -Create a new script called `example.py`: +```bash +uv add arcadepy openai python-dotenv +``` -```python filename="example.py" -from arcadepy import Arcade +Your directory should now look like this: -# You can also set the `ARCADE_API_KEY` environment variable instead of passing it as a parameter. +```bash +arcade-llm-example/ +├── .git/ +├── .gitignore +├── python-version +├── .venv/ +├── main.py +├── pyproject.toml +├── main.py +├── README.md +└── uv.lock +``` -client = Arcade(api_key="{arcade_api_key}") +### Instantiate and use the clients -# Arcade needs a unique identifier for your application user (this could be an email address, a UUID, etc). +Create a new file called `.env` and add your Arcade API key, as well as your OpenAI API key: -# In this example, use the email you used to sign up for Arcade.dev: +```bash +ARCADE_API_KEY=YOUR_ARCADE_API_KEY +ARCADE_USER_ID=YOUR_ARCADE_USER_ID +OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY +OPENROUTER_MODEL=YOUR_OPENROUTER_MODEL +``` -user_id = "{arcade_user_id}" + + In this example, we're using OpenRouter to access the model, as it makes it + very easy to use any model from multiple providers with a single API. + OpenRouter is compliant with the OpenAI API specification, so you can use it + with any OpenAI-compatible library. + -# Let's use the `Math.Sqrt` tool from the Arcade Math MCP Server to get the square root of a number. +Open the `main.py` file in your editor of choice, and replace replace the contents with the following: -response = client.tools.execute( - tool_name="Math.Sqrt", - input={"a": '625'}, - user_id=user_id, +```python filename="main.py" +from arcadepy import Arcade +from openai import OpenAI +from dotenv import load_dotenv +import json +import os + +load_dotenv() + +client = Arcade() +arcade_user_id = os.getenv("ARCADE_USER_ID") +llm_client = OpenAI( + api_key=os.getenv("OPENROUTER_API_KEY"), + base_url="https://openrouter.ai/api/v1" ) -print(f"The square root of 625 is {response.output.value}") +``` -# Now, let's use a tool that requires authentication to star a GitHub repository +### Select and retrieve the tools from Arcade -auth_response = client.tools.authorize( -tool_name="GitHub.SetStarred", -user_id=user_id, -) +In this example, we're implementing a simple agent that can retrieve and send emails, as well as send messages to Slack. In a harness like this one, where the entire catalog of tools is exposed to the LLM, you will want to choose only the tools that are relevant to the task at hand to avoid overwhelming the LLM with too many options, and to make your agent more token efficient. -if auth_response.status != "completed": - print(f"Click this link to authorize: `{auth_response.url}`. The process will continue once you have authorized the app." ) # Wait for the user to authorize the app - client.auth.wait_for_completion(auth_response.id); - -response = client.tools.execute( - tool_name="GitHub.SetStarred", - input={ - "owner": "ArcadeAI", - "name": "arcade-mcp", - "starred": True, - }, - user_id=user_id, -) +```python filename="main.py" +# We define here the tools that we want to use in the agent +tool_catalog = [ + "Gmail.ListEmails", + "Gmail.SendEmail", + "Slack.SendMessage", + "Slack.WhoAmI" +] -print(response.output.value) +# We get the tool definitions from the Arcade API, so that we can expose them to the LLM +tool_definitions = [] +for tool in tool_catalog: + tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) +``` +### Write a helper function that handles tool authorization and execution + +The LLM may choose to call any of the tools in our catalog, and we need to handle different scenarios, such as when the tool requires authorization. There are many approaches to this, but a good pattern is to handle interruptions like this outside of the agentic context if we can do so. As a rule of thumb, you should evaluate whether it is relevant for the LLM to be aware of the authorization process, or if it's better to handle it in the harness, and keep the context as if the tool was already authorized. The latter option optimizes for token efficiency, and we will implement it in this example. + +```python filename="main.py" +# Helper function to authorize and run any tool +def authorize_and_run_tool(tool_name: str, input: str): + # Start the authorization process + auth_response = arcade.tools.authorize( + tool_name=tool_name, + user_id=arcade_user_id, + ) + + # 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. + if auth_response.status != "completed": + print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") + arcade.auth.wait_for_completion(auth_response.id) + + # Parse the input + input_json = json.loads(input) + + # Run the tool + result = arcade.tools.execute( + tool_name=tool_name, + input=input_json, + user_id=arcade_user_id, + ) + + # Return the tool output to the caller as a JSON string + return json.dumps(result.output.value) ``` - +That helper function adapts to any tool in the catalog, and will make sure that the authorization requirements are met before executing the tool. For more complex agentic patterns, this is generally the best place to handle interruptions that may require user interaction, such as when the tool requires a user to approve a request, or to provide additional context. + +### Write a helper function that handles the LLM's invocation + +There are many orchestration patterns that can be used to handle the LLM invocation. A common pattern is a ReAct architecture, where the user prompt will result in a loop of messages between the LLM and the tools, until the LLM provides a final response (no tool calls). This is the pattern we will implement in this example. + +To avoid the risk of infinite loops, we will limit the number of turns to a maximum of 5. This is a parameter that you can tune to your needs, and it's a good idea to set it to a value that is high enough to allow the LLM to complete the task it's designed to do, but low enough to prevent infinite loops. + +```python filename="main.py" +def invoke_llm( + history: list[dict], + model: str = "google/gemini-2.5-flash", + max_turns: int = 5, + tools: list[dict] = None, + tool_choice: str = "auto", +) -> list[dict]: + """ + Multi-turn LLM invocation that processes the conversation until + the assistant provides a final response (no tool calls). + + Returns the updated conversation history. + """ + turns = 0 + + while turns < max_turns: + turns += 1 + + response = llm_client.chat.completions.create( + model=model, + messages=history, + tools=tools, + tool_choice=tool_choice, + ) + + assistant_message = response.choices[0].message + + if assistant_message.tool_calls: + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = tool_call.function.arguments + print(f"🛠️ Harness: Calling {tool_name} with input {tool_args}") + tool_result = authorize_and_run_tool(tool_name, tool_args) + print(f"🛠️ Harness: Tool call {tool_name} completed") + history.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": tool_result, + }) + + continue + + else: + history.append({ + "role": "assistant", + "content": assistant_message.content, + }) + + break + + return history +``` - +In combination, these two helper functions will form the core of our agentic loop. You will notice that the authorization is handled outside of the agentic context, and the tool execution is passed back to the LLM in every case. Depending on your needs, you may want to handle tool orchestration within the harness, and pass only the final result of multiple tool calls to the LLM. -Create a new script called `example.mjs`: +### Write the main agentic loop -```javascript filename="example.mjs" -import Arcade from "@arcadeai/arcadejs"; +Now that we've written the helper functions, we can write a very simple agentic loop that interacts with the user. The core pieces of this loop are: -// You can also set the `ARCADE_API_KEY` environment variable instead of passing it as a parameter. -const client = new Arcade({ - apiKey: "{arcade_api_key}", -}); +1. Initialize the conversation history with the system prompt +2. Get the user input, and add it to the conversation history +3. Invoke the LLM with the conversation history, tools, and tool choice +4. Repeat from step 2 until the user decides to stop the conversation -// 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: -let userId = "{arcade_user_id}"; +```python filename="main.py" +def chat(): + """Interactive multi-turn chat session.""" + print("Chat started. Type 'quit' or 'exit' to end the session.\n") -// Let's use the `Math.Sqrt` tool from the Arcade Math MCP Server to get the square root of a number. -const response_sqrt = await client.tools.execute({ - tool_name: "Math.Sqrt", - input: { a: "625" }, - user_id: userId, -}); + # Initialize the conversation history with the system prompt + history: list[dict] = [ + {"role": "system", "content": "You are a helpful assistant."} + ] -console.log(response_sqrt.output.value); + while True: + try: + user_input = input("😎 You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + break -// Now, let's use a tool that requires authentication + if not user_input: + continue -const authResponse = await client.tools.authorize({ - tool_name: "GitHub.SetStarred", - user_id: userId, -}); + if user_input.lower() in ("quit", "exit"): + print("Goodbye!") + break -if (authResponse.status !== "completed") { - console.log( - `Click this link to authorize: \`${authResponse.url}\`. The process will continue once you have authorized the app.` - ); - // Wait for the user to authorize the app - await client.auth.waitForCompletion(authResponse.id); -} + # Add user message to history + history.append({"role": "user", "content": user_input}) -const response_github = await client.tools.execute({ - tool_name: "GitHub.SetStarred", - input: { - owner: "ArcadeAI", - name: "arcade-mcp", - starred: true, - }, - user_id: userId, -}); + # Get LLM response + history = invoke_llm( + history, tools=tool_definitions) -console.log(response_github.output.value); -``` + # Print the latest assistant response + assistant_response = history[-1]["content"] + print(f"\n🤖 Assistant: {assistant_response}\n") - - +if __name__ == "__main__": + chat() +``` ### Run the code - - - +It's time to run the code and see it in action! Run the following command to start the chat: ```bash -uv run example.py -> The square root of 625 is 25 -> Successfully starred the repository ArcadeAI/arcade-mcp +uv run main.py ``` - - - - ```bash - node example.mjs - > The square root of 625 is 25 - > Successfully starred the repository ArcadeAI/arcade-mcp - ``` +With the selection of tools above, you should be able to get the agent to effectively complete the following prompts: - - +- "Please send a message to the #general channel on Slack greeting everyone with a haiku about agents." +- "Please write a poem about multi-tool orchestration and send it to the #general channel on Slack, also send it to me in an email." +- "Please summarize my latest 5 emails, then send me a DM on Slack with the summary." ## Next Steps -In this simple example, we call the tool methods directly. In your real applications and agents, you'll likely be letting the LLM decide which tools to call - learn more about using Arcade with Frameworks in the [Frameworks](/home/langchain/use-arcade-tools) section, or [how to build your own tools](/home/build-tools/create-a-mcp-server). +- Learn more about using Arcade with frameworks like [LangChain](/home/langchain/use-arcade-tools) or [Mastra](/home/mastra/overview). +- Learn more about how to [build your own MCP Servers](/home/build-tools/create-a-mcp-server). From c645390bd0bd954c88d43f472bce78abc218cb1d Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Wed, 10 Dec 2025 13:25:48 -0800 Subject: [PATCH 04/10] added example code at the bottom of the guide --- .../home/connect-arcade-to-your-llm/page.mdx | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 6340dd418..858fcec8c 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -296,3 +296,154 @@ With the selection of tools above, you should be able to get the agent to effect - Learn more about using Arcade with frameworks like [LangChain](/home/langchain/use-arcade-tools) or [Mastra](/home/mastra/overview). - Learn more about how to [build your own MCP Servers](/home/build-tools/create-a-mcp-server). + +## Example code + +```python filename="main.py" +from arcadepy import Arcade +from dotenv import load_dotenv +from openai import OpenAI +import json +import os + +load_dotenv() + +arcade = Arcade() +arcade_user_id = os.getenv("ARCADE_USER_ID") +llm_client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY"), +) + +# We define here the tools that we want to use in the agent +tool_catalog = [ + "Gmail.ListEmails", + "Gmail.SendEmail", + "Slack.SendMessage", + "Slack.WhoAmI" +] + +# We get the tool definitions from the Arcade API, so that we can expose them to the LLM +tool_definitions = [] +for tool in tool_catalog: + tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) + + +# Helper function to authorize and run any tool +def authorize_and_run_tool(tool_name: str, input: str): + # Start the authorization process + auth_response = arcade.tools.authorize( + tool_name=tool_name, + user_id=arcade_user_id, + ) + + # 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. + if auth_response.status != "completed": + print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") + arcade.auth.wait_for_completion(auth_response.id) + + # Parse the input + input_json = json.loads(input) + + # Run the tool + result = arcade.tools.execute( + tool_name=tool_name, + input=input_json, + user_id=arcade_user_id, + ) + + # Return the tool output to the caller as a JSON string + return json.dumps(result.output.value) + + +def invoke_llm( + history: list[dict], + model: str = "google/gemini-2.5-flash", + max_turns: int = 5, + tools: list[dict] = None, + tool_choice: str = "auto", +) -> list[dict]: + """ + Multi-turn LLM invocation that processes the conversation until + the assistant provides a final response (no tool calls). + + Returns the updated conversation history. + """ + turns = 0 + + while turns < max_turns: + turns += 1 + + response = llm_client.chat.completions.create( + model=model, + messages=history, + tools=tools, + tool_choice=tool_choice, + ) + + assistant_message = response.choices[0].message + + if assistant_message.tool_calls: + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = tool_call.function.arguments + print(f"🛠️ Harness: Calling {tool_name} with input {tool_args}") + tool_result = authorize_and_run_tool(tool_name, tool_args) + print(f"🛠️ Harness: Tool call {tool_name} completed") + history.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": tool_result, + }) + + continue + + else: + history.append({ + "role": "assistant", + "content": assistant_message.content, + }) + + break + + return history + + +def chat(): + """Interactive multi-turn chat session.""" + print("Chat started. Type 'quit' or 'exit' to end the session.\n") + + history: list[dict] = [ + {"role": "system", "content": "You are a helpful assistant."} + ] + + while True: + try: + user_input = input("😎 You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + break + + if not user_input: + continue + + if user_input.lower() in ("quit", "exit"): + print("Goodbye!") + break + + # Add user message to history + history.append({"role": "user", "content": user_input}) + + # Get LLM response + history = invoke_llm( + history, tools=tool_definitions) + + # Print the latest assistant response + assistant_response = history[-1]["content"] + print(f"\n🤖 Assistant: {assistant_response}\n") + + +if __name__ == "__main__": + chat() +``` From 57ef311e4932247649a8477d9d5dbf7422b30b95 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Tue, 16 Dec 2025 14:58:55 -0300 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: RL "Nearest" Nabors <236306+nearestnabors@users.noreply.github.com> --- .../home/connect-arcade-to-your-llm/page.mdx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 858fcec8c..2080d9974 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -11,7 +11,7 @@ import { SignupLink } from "@/app/_components/analytics"; -Integrate Arcade tool-calling capabilities into an application that uses an LLM in Python. +Integrate Arcade's tool-calling capabilities into an application that uses an LLM in Python. @@ -77,7 +77,7 @@ arcade-llm-example/ Create a new file called `.env` and add your Arcade API key, as well as your OpenAI API key: -```bash +```text filename=".env" ARCADE_API_KEY=YOUR_ARCADE_API_KEY ARCADE_USER_ID=YOUR_ARCADE_USER_ID OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY @@ -113,7 +113,7 @@ llm_client = OpenAI( ### Select and retrieve the tools from Arcade -In this example, we're implementing a simple agent that can retrieve and send emails, as well as send messages to Slack. In a harness like this one, where the entire catalog of tools is exposed to the LLM, you will want to choose only the tools that are relevant to the task at hand to avoid overwhelming the LLM with too many options, and to make your agent more token efficient. +In this example, we're implementing a multi-tool agent that can retrieve and send emails, as well as send messages to Slack. In a harness like this one the entire catalog of tools is exposed to the LLM. To make your agent more efficient, choose only the tools that are relevant to the task at hand to avoid overwhelming the LLM and to make your agent more token efficient. ```python filename="main.py" # We define here the tools that we want to use in the agent @@ -124,7 +124,7 @@ tool_catalog = [ "Slack.WhoAmI" ] -# We get the tool definitions from the Arcade API, so that we can expose them to the LLM +# Get the tool definitions from the Arcade API tool_definitions = [] for tool in tool_catalog: tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) @@ -132,7 +132,9 @@ for tool in tool_catalog: ### Write a helper function that handles tool authorization and execution -The LLM may choose to call any of the tools in our catalog, and we need to handle different scenarios, such as when the tool requires authorization. There are many approaches to this, but a good pattern is to handle interruptions like this outside of the agentic context if we can do so. As a rule of thumb, you should evaluate whether it is relevant for the LLM to be aware of the authorization process, or if it's better to handle it in the harness, and keep the context as if the tool was already authorized. The latter option optimizes for token efficiency, and we will implement it in this example. +The LLM may choose to call any of the tools in its catalog, some of which may require authorization, interrupting the process. + +One way to handle interruptions like this is to evaluate whether the LLM needs to be aware of the authorization process or if it's better to handle it in the harness, keeping the context as if the tool were already authorized. The latter option optimizes for token efficiency, so is used in the following example: ```python filename="main.py" # Helper function to authorize and run any tool @@ -163,13 +165,13 @@ def authorize_and_run_tool(tool_name: str, input: str): return json.dumps(result.output.value) ``` -That helper function adapts to any tool in the catalog, and will make sure that the authorization requirements are met before executing the tool. For more complex agentic patterns, this is generally the best place to handle interruptions that may require user interaction, such as when the tool requires a user to approve a request, or to provide additional context. +This helper function adapts to any tool in the catalog and will make sure that the authorization requirements are met before executing the tool. For more complex agentic patterns, this is generally the best place to handle interruptions that may require user interaction, such as when the tool requires a user to approve a request, or to provide additional context. ### Write a helper function that handles the LLM's invocation There are many orchestration patterns that can be used to handle the LLM invocation. A common pattern is a ReAct architecture, where the user prompt will result in a loop of messages between the LLM and the tools, until the LLM provides a final response (no tool calls). This is the pattern we will implement in this example. -To avoid the risk of infinite loops, we will limit the number of turns to a maximum of 5. This is a parameter that you can tune to your needs, and it's a good idea to set it to a value that is high enough to allow the LLM to complete the task it's designed to do, but low enough to prevent infinite loops. +To avoid the risk of infinite loops, limit the number of turns (in this case, a maximum of 5). This is a parameter that you can tune to your needs. Set it to a value that is high enough to allow the LLM to complete its task but low enough to prevent infinite loops. ```python filename="main.py" def invoke_llm( @@ -225,14 +227,14 @@ def invoke_llm( return history ``` -In combination, these two helper functions will form the core of our agentic loop. You will notice that the authorization is handled outside of the agentic context, and the tool execution is passed back to the LLM in every case. Depending on your needs, you may want to handle tool orchestration within the harness, and pass only the final result of multiple tool calls to the LLM. +These two helper functions form the core of your agentic loop. Notice that authorization is handled outside the agentic context, and the tool execution is passed back to the LLM in every case. Depending on your needs, you may want to handle tool orchestration within the harness and pass only the final result of multiple tool calls to the LLM. ### Write the main agentic loop -Now that we've written the helper functions, we can write a very simple agentic loop that interacts with the user. The core pieces of this loop are: +Now that you've written the helper functions, write a very simple agentic loop that interacts with the user. The core pieces of this loop are: 1. Initialize the conversation history with the system prompt -2. Get the user input, and add it to the conversation history +2. Get the user input and add it to the conversation history 3. Invoke the LLM with the conversation history, tools, and tool choice 4. Repeat from step 2 until the user decides to stop the conversation From 1065b5d4244ae422b3c466de64ece079f98cd25f Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Tue, 16 Dec 2025 14:59:52 -0300 Subject: [PATCH 06/10] Apply suggestions from code review Co-authored-by: RL "Nearest" Nabors <236306+nearestnabors@users.noreply.github.com> --- app/en/home/connect-arcade-to-your-llm/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 2080d9974..0d7e2a4bd 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -116,7 +116,7 @@ llm_client = OpenAI( In this example, we're implementing a multi-tool agent that can retrieve and send emails, as well as send messages to Slack. In a harness like this one the entire catalog of tools is exposed to the LLM. To make your agent more efficient, choose only the tools that are relevant to the task at hand to avoid overwhelming the LLM and to make your agent more token efficient. ```python filename="main.py" -# We define here the tools that we want to use in the agent +# Define the tools for the agent to use tool_catalog = [ "Gmail.ListEmails", "Gmail.SendEmail", From af24f12417012b35f900a79bc8ab7f71967f9568 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Tue, 16 Dec 2025 17:08:26 -0300 Subject: [PATCH 07/10] addressed nabors' feedback --- .../home/connect-arcade-to-your-llm/page.mdx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 0d7e2a4bd..2eab53db7 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -8,6 +8,8 @@ import { SignupLink } from "@/app/_components/analytics"; # Connect Arcade to your LLM +Arcade tools are designed to be used with LLMs. This requires you to implement a harness that orchestrates the interaction between the LLM, the tools, and the user. While typically this is abstracted away by agentic frameworks, this guide will show you how to implement a simple harness from scratch, so that the underlying mechanics are clear. + @@ -84,11 +86,20 @@ OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY OPENROUTER_MODEL=YOUR_OPENROUTER_MODEL ``` + + The `ARCADE_USER_ID` is the email address you used to sign up for Arcade. When + your app is ready for production, you can set this dynamically based on your + app's auth system. Learn more about how to achieve secure auth in production + [here](/home/auth/secure-auth-production). + + In this example, we're using OpenRouter to access the model, as it makes it very easy to use any model from multiple providers with a single API. - OpenRouter is compliant with the OpenAI API specification, so you can use it - with any OpenAI-compatible library. + +OpenRouter is compliant with the OpenAI API specification, so you can use it +with any OpenAI-compatible library. + Open the `main.py` file in your editor of choice, and replace replace the contents with the following: @@ -124,7 +135,7 @@ tool_catalog = [ "Slack.WhoAmI" ] -# Get the tool definitions from the Arcade API +# Get the tool definitions from the Arcade API tool_definitions = [] for tool in tool_catalog: tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) @@ -132,7 +143,7 @@ for tool in tool_catalog: ### Write a helper function that handles tool authorization and execution -The LLM may choose to call any of the tools in its catalog, some of which may require authorization, interrupting the process. +The LLM may choose to call any of the tools in its catalog, some of which may require authorization, interrupting the process. One way to handle interruptions like this is to evaluate whether the LLM needs to be aware of the authorization process or if it's better to handle it in the harness, keeping the context as if the tool were already authorized. The latter option optimizes for token efficiency, so is used in the following example: From 2d401ce482cccced43884accfbd0850b284c4dc7 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Thu, 18 Dec 2025 10:10:20 -0300 Subject: [PATCH 08/10] client to arcade_client --- .../home/connect-arcade-to-your-llm/page.mdx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 2eab53db7..f2635c032 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -113,7 +113,7 @@ import os load_dotenv() -client = Arcade() +arcade_client = Arcade() arcade_user_id = os.getenv("ARCADE_USER_ID") llm_client = OpenAI( api_key=os.getenv("OPENROUTER_API_KEY"), @@ -138,7 +138,7 @@ tool_catalog = [ # Get the tool definitions from the Arcade API tool_definitions = [] for tool in tool_catalog: - tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) + tool_definitions.append(arcade_client.tools.formatted.get(name=tool, format="openai")) ``` ### Write a helper function that handles tool authorization and execution @@ -151,7 +151,7 @@ One way to handle interruptions like this is to evaluate whether the LLM needs t # Helper function to authorize and run any tool def authorize_and_run_tool(tool_name: str, input: str): # Start the authorization process - auth_response = arcade.tools.authorize( + auth_response = arcade_client.tools.authorize( tool_name=tool_name, user_id=arcade_user_id, ) @@ -160,13 +160,13 @@ def authorize_and_run_tool(tool_name: str, input: str): # Tools that do not require authorization will have the status "completed" already. if auth_response.status != "completed": print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") - arcade.auth.wait_for_completion(auth_response.id) + arcade_client.auth.wait_for_completion(auth_response.id) # Parse the input input_json = json.loads(input) # Run the tool - result = arcade.tools.execute( + result = arcade_client.tools.execute( tool_name=tool_name, input=input_json, user_id=arcade_user_id, @@ -321,7 +321,7 @@ import os load_dotenv() -arcade = Arcade() +arcade_client = Arcade() arcade_user_id = os.getenv("ARCADE_USER_ID") llm_client = OpenAI( base_url="https://openrouter.ai/api/v1", @@ -339,13 +339,13 @@ tool_catalog = [ # We get the tool definitions from the Arcade API, so that we can expose them to the LLM tool_definitions = [] for tool in tool_catalog: - tool_definitions.append(arcade.tools.formatted.get(name=tool, format="openai")) + tool_definitions.append(arcade_client.tools.formatted.get(name=tool, format="openai")) # Helper function to authorize and run any tool def authorize_and_run_tool(tool_name: str, input: str): # Start the authorization process - auth_response = arcade.tools.authorize( + auth_response = arcade_client.tools.authorize( tool_name=tool_name, user_id=arcade_user_id, ) @@ -354,13 +354,13 @@ def authorize_and_run_tool(tool_name: str, input: str): # Tools that do not require authorization will have the status "completed" already. if auth_response.status != "completed": print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") - arcade.auth.wait_for_completion(auth_response.id) + arcade_client.auth.wait_for_completion(auth_response.id) # Parse the input input_json = json.loads(input) # Run the tool - result = arcade.tools.execute( + result = arcade_client.tools.execute( tool_name=tool_name, input=input_json, user_id=arcade_user_id, From f23fa7fb06cb0348237ee523eaa105271266da55 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Thu, 18 Dec 2025 10:19:06 -0300 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: vfanelle --- app/en/home/connect-arcade-to-your-llm/page.mdx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index f2635c032..35c3d64da 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -8,7 +8,7 @@ import { SignupLink } from "@/app/_components/analytics"; # Connect Arcade to your LLM -Arcade tools are designed to be used with LLMs. This requires you to implement a harness that orchestrates the interaction between the LLM, the tools, and the user. While typically this is abstracted away by agentic frameworks, this guide will show you how to implement a simple harness from scratch, so that the underlying mechanics are clear. +Arcade tools are meant to be used alongside an LLM. To make that work, you need a small piece of glue code called a “harness.” The harness orchestrates the back-and-forth between the user, the model, and the tools. In this guide, we’ll build a simple one so you can wire Arcade into your LLM-powered app. @@ -124,7 +124,7 @@ llm_client = OpenAI( ### Select and retrieve the tools from Arcade -In this example, we're implementing a multi-tool agent that can retrieve and send emails, as well as send messages to Slack. In a harness like this one the entire catalog of tools is exposed to the LLM. To make your agent more efficient, choose only the tools that are relevant to the task at hand to avoid overwhelming the LLM and to make your agent more token efficient. +In this example, we're implementing a multi-tool agent that can retrieve and send emails, as well as send messages to Slack. While a harness can expose a broad catalog of tools to the LLM, it’s best to limit that set to what’s relevant for the task to keep the model efficient. ```python filename="main.py" # Define the tools for the agent to use @@ -143,9 +143,8 @@ for tool in tool_catalog: ### Write a helper function that handles tool authorization and execution -The LLM may choose to call any of the tools in its catalog, some of which may require authorization, interrupting the process. +The model can use any tool you give it, and some tools require permission before they work. When this happens, you can either involve the model in the permission step or handle it behind the scenes and continue as if the tool were already authorized. In this guide, authorization is handled outside the model so it can act as if the tool is already available. It’s like ordering a coffee: after you place your order, the barista handles payment behind the counter instead of explaining every step of card verification and receipts. The customer—and the model—gets the result without having to think about any of the intermediate steps. -One way to handle interruptions like this is to evaluate whether the LLM needs to be aware of the authorization process or if it's better to handle it in the harness, keeping the context as if the tool were already authorized. The latter option optimizes for token efficiency, so is used in the following example: ```python filename="main.py" # Helper function to authorize and run any tool From c707a6647f519562f8bc09ce341f12f0ca80332b Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Thu, 18 Dec 2025 10:25:03 -0300 Subject: [PATCH 10/10] implemented val's suggestions --- app/en/home/connect-arcade-to-your-llm/page.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/en/home/connect-arcade-to-your-llm/page.mdx b/app/en/home/connect-arcade-to-your-llm/page.mdx index 35c3d64da..d0d287f61 100644 --- a/app/en/home/connect-arcade-to-your-llm/page.mdx +++ b/app/en/home/connect-arcade-to-your-llm/page.mdx @@ -100,9 +100,17 @@ OPENROUTER_MODEL=YOUR_OPENROUTER_MODEL OpenRouter is compliant with the OpenAI API specification, so you can use it with any OpenAI-compatible library. +If you don't know which model to use, we recommend trying one of these: + +- `anthropic/claude-haiku-4.5` +- `deepseek/deepseek-v3.2` +- `google/gemini-3-flash-preview` +- `google/gemini-2.5-flash-lite` +- `openai/gpt-4o-mini` + -Open the `main.py` file in your editor of choice, and replace replace the contents with the following: +Open the `main.py` file in your editor of choice, and replace the contents with the following: ```python filename="main.py" from arcadepy import Arcade @@ -145,7 +153,6 @@ for tool in tool_catalog: The model can use any tool you give it, and some tools require permission before they work. When this happens, you can either involve the model in the permission step or handle it behind the scenes and continue as if the tool were already authorized. In this guide, authorization is handled outside the model so it can act as if the tool is already available. It’s like ordering a coffee: after you place your order, the barista handles payment behind the counter instead of explaining every step of card verification and receipts. The customer—and the model—gets the result without having to think about any of the intermediate steps. - ```python filename="main.py" # Helper function to authorize and run any tool def authorize_and_run_tool(tool_name: str, input: str):