Adding secrets to your tools
In this guide, you’ll learn how to add secrets to your custom tools, using Arcade.
Secrets are sensitive strings like passwords, api-keys, or other tokens that grant access to a protected resource or API.
In this example, you’ll create a tool that reads data from a postgres database.
Prerequisites
We will be using sqlalchemy
and psycopg2-binary
to access a postgres database.
pip install arcade-ai sqlalchemy psycopg2-binary
Define your tool
Create a new Python file, e.g., sql_tools.py
, and import the necessary modules:
from typing import Annotated
from sqlalchemy import create_engine,Engine,inspect,text
from arcade.sdk import tool, ToolContext
Now, define your tool using the @tool
decorator and specify the needed secrets with requires_secrets
, in this case a DATABASE_CONNECTION_STRING
. In this example, DATABASE_CONNECTION_STRING
, is meant to be a JDBC-style database connection URL, e.g. postgres://user:[email protected]/database_name
. Our DATABASE_CONNECTION_STRING
will contain a username and password in this case, making it very sensitive.
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
def discover_tables(context: ToolContext, schema_name: Annotated[str, "The database schema to discover tables in"] = "public") -> list[str]:
"""Discover all the tables in the database"""
engine = _get_engine(context.get_secret("DATABASE_CONNECTION_STRING"))
tables = _get_tables(engine, schema_name)
return tables
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
def get_table_schema(context: ToolContext, schema_name: Annotated[str, "The database schema to get the table schema of"], table_name: Annotated[str, "The table to get the schema of"]) -> list[str]:
"""Get the schema of a table"""
engine = _get_engine(context.get_secret("DATABASE_CONNECTION_STRING"))
return _get_table_schema(engine, schema_name, table_name)
@tool(requires_secrets=["DATABASE_CONNECTION_STRING"])
def execute_query(context: ToolContext, query: Annotated[str, "The SQL query to execute"]) -> list[str]:
"""Execute a query and return the results"""
engine = _get_engine(context.get_secret("DATABASE_CONNECTION_STRING"))
return _execute_query(engine, query)
def _get_engine(connection_string: str) -> Engine:
"""Get a connection to the database. Note that we build the engine with an isolation level of READ UNCOMMITTED to prevent all writes."""
return create_engine(connection_string, isolation_level='READ UNCOMMITTED')
def _get_tables(engine: Engine, schema_name: str) -> list[str]:
"""Get all the tables in the database"""
inspector = inspect(engine)
schemas = inspector.get_schema_names()
tables = []
for schema in schemas:
if schema == schema_name:
tables.extend(inspector.get_table_names(schema=schema))
return tables
def _get_table_schema(engine: Engine, schema_name: str, table_name: str) -> list[str]:
"""Get the schema of a table"""
inspector = inspect(engine)
columns_table = inspector.get_columns(table_name, schema_name)
return [f"{column['name']}: {column['type'].python_type.__name__}" for column in columns_table]
def _execute_query(engine: Engine, query: str) -> list[str]:
"""Execute a query and return the results."""
with engine.connect() as connection:
result = connection.execute(text(query))
return [str(row) for row in result.fetchall()]
Use your tool with Arcade
Now you can use your custom authorized tool with Arcade in your application.
Here’s an example of how to use your tool. Note that for this example, the table schema includes a users
table and a messages
table, and messsages
has a foreign key back to users
.
See full schema
CREATE TABLE "public"."users" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(256) NOT NULL,
"email" text NOT NULL UNIQUE,
"password_hash" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE TABLE "public"."messages" (
"id" serial PRIMARY KEY NOT NULL,
"body" text NOT NULL,
"user_id" integer NOT NULL REFERENCES "public"."users" (id),
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
import Arcade from "@arcadeai/arcadejs";
const ARCADE_API_KEY = process.env.ARCADE_API_KEY;
const USER_ID = process.env.USER_ID;
const DB_DIALECT = "POSTGRES";
const SYSTEM_PROMPT = `
You are an expert SQL analyst.
For all questions, you will use only the tools provided to you to answer the question, and no prior knowledge.
The SQL dialect is "${DB_DIALECT}".
If a tool call requires a schema, and one has not been provided, assume the schema is "public".
If a tool call produces a response with multiple entries, format your response as a markdown table, with one row per entry.
`;
const client = new Arcade({
apiKey: ARCADE_API_KEY,
});
const tables = await chat("Discover all the tables in the database");
const schemas = await chat(
`Get the schemas of the tables in the database. The tables are: ${tables}`,
tables,
);
await chat(
`Get the first 10 user's names. The database schema is: ${schemas}`,
schemas,
);
await chat(
`Count how many users there are. The database schema is: ${schemas}`,
schemas,
);
await chat(
`How many messages has each user sent? Group by user id and name. The database schema is: ${schemas}`,
schemas,
);
/* --- UTILITIES --- */
function buildPrompt(question: string) {
return {
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: question,
},
],
model: "gpt-4o",
user: USER_ID,
tools: ["Sql.ExecuteQuery"],
tool_choice: "generate",
};
}
async function chat(
question: string,
replace: string = "...",
): Promise<string | undefined> {
console.log(`\r\n[❓] Asking: ${question.replace(replace, " {...}")}\r\n`);
const response = await client.chat.completions.create(buildPrompt(question));
displayResponse(response);
return response.choices?.[0]?.message?.content;
}
function displayResponse(response: Arcade.Chat.ChatResponse) {
console.log("--- response ---");
console.log(response.choices?.[0]?.message?.content);
console.log("\r\n--- tool calls ---");
response.choices?.[0]?.message?.tool_calls?.map((tool) => {
if (!tool || !tool.function) return;
console.log(`${tool.function.name}: ${tool.function.arguments}`);
});
console.log("---");
}
Supplying the Secret
Note how in the example above we never provided a value for DATABASE_CONNECTION_STRING
. This is because we want the Arcade engine to manage this for us, keeping the sercets that the tool needs seperate from the environment that is exceuting the LLM calls (our application above).
Using Arcade Cloud, after publishing your tool with arcade deploy
, you will see that your tool requires the DATABASE_CONNECTION_STRING
secret:
You can manage your secrets from the secrets
section of the authentication section: