<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Custom agent with human-in-the-loop | Sample | ArcGIS Maps SDK for JavaScript</title>
<script type="module" src="https://js.arcgis.com/5.2/"></script>
display: none !important;
<arcgis-map id="main-map" item-id="c13bffcad4a244a99062e915e9bc1dc3">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-expand slot="top-left"><arcgis-legend></arcgis-legend></arcgis-expand>
<calcite-shell-panel slot="panel-end" width="l" height-scale="l">
reference-element="#main-map"
heading="Export your map"
entry-message="Export this map as a screenshot, a PDF, or a new web map."
always-use-agent-id="mapExport"
<template id="export-template">
<img class="export-thumbnail" slot="thumbnail" alt="Exported map preview" hidden />
<span class="export-title" slot="heading"></span>
<span class="export-description" slot="description"></span>
<div class="export-actions" slot="footer-end">
rel="noopener noreferrer"
{ createHumanInTheLoopToolMiddleware },
] = await $arcgis.import([
"@arcgis/core/portal/Portal.js",
"@arcgis/core/rest/print.js",
"@arcgis/core/rest/support/PrintParameters.js",
"@arcgis/core/rest/support/PrintTemplate.js",
"@arcgis/ai-components/agent-utils/SkillAgent.js",
"@arcgis/ai-components/agent-utils/tools/FunctionTool.js",
"@arcgis/ai-components/agent-utils/createSchema.js",
"@arcgis/ai-components/agent-utils/middlewares/humanInTheLoop.js",
"@arcgis/ai-components/orchestrator/signals.js",
const mapElement = document.querySelector("arcgis-map");
const assistant = document.querySelector("arcgis-assistant");
const panel = document.querySelector("calcite-shell-panel");
const template = document.querySelector("#export-template");
const exportSlots = new Map();
// Handle responsive layout for the panel based on screen width
const narrowScreen = window.matchMedia("(max-width: 760px)");
function updatePanelLayout() {
panel.slot = narrowScreen.matches ? "panel-bottom" : "panel-end";
panel.layout = narrowScreen.matches ? "horizontal" : "vertical";
narrowScreen.addEventListener("change", updatePanelLayout);
// Handle slottable requests from the assistant for export buttons
assistant.addEventListener("arcgisSlottableRequest", () => {
const requests = assistant.slottableRequests.filter(
(request) => request.name === "block" && request.data?.block?.type === "button",
for (const [slotName, element] of exportSlots) {
if (!requests.some((request) => request.slotName === slotName)) {
exportSlots.delete(slotName);
for (const request of requests) {
if (exportSlots.has(request.slotName)) continue;
const data = request.data.block.data;
if (!data?.url) continue;
const element = template.content.firstElementChild.cloneNode(true);
element.slot = request.slotName;
const screenshot = data.label === "Open Screenshot";
element.querySelector(".export-title").textContent = data.title;
element.querySelector(".export-description").textContent = screenshot
? "Screenshot ready to download."
: "Your map export is ready.";
const image = element.querySelector("img");
image.src = data.thumbnailUrl;
const openButton = element.querySelector(".export-open");
openButton.textContent = data.label;
if (!screenshot) openButton.href = data.url;
openButton.addEventListener("click", () => {
const imageWindow = window.open("about:blank", "_blank");
if (!imageWindow) return;
imageWindow.opener = null;
imageWindow.document.title = data.title;
const preview = imageWindow.document.createElement("img");
preview.alt = data.title;
preview.style.maxWidth = "100%";
imageWindow.document.body.append(preview);
assistant.append(element);
exportSlots.set(request.slotName, element);
const approveExport = createHumanInTheLoopToolMiddleware({
interrupt: ({ input, toolName }) => ({
message: `Proceed with ${toolName} using the title or filename "${input.title?.trim() || "Untitled Map"}"?`,
const emptySchema = await createSchema({ type: "object", properties: {} });
async function composeScreenshot(dataUrl, title) {
const image = new Image();
const canvas = document.createElement("canvas");
canvas.width = Math.max(image.naturalWidth, 480) + margin * 2;
canvas.height = image.naturalHeight + titleHeight + margin * 2;
const context = canvas.getContext("2d");
if (!context) throw new Error("Unable to compose the map image.");
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "#202927";
context.textBaseline = "top";
context.font = "bold 24px sans-serif";
context.fillText(title, margin, margin, canvas.width - margin * 2);
context.drawImage(image, margin, margin + titleHeight);
const result = canvas.toDataURL("image/png");
if (result === "data:,") throw new Error("The exported image exceeds the canvas size limit.");
* Export the current map as a PNG image.
* Prompts the user for a title to use as the filename.
const exportScreenshot = new FunctionTool({
name: "exportScreenshot",
description: "Capture the current map as a PNG, include some padding around the map and display its title.",
inputSchema: emptySchema,
createHumanInTheLoopToolMiddleware({
interrupt: () => ({ kind: "textInput", message: "Provide a filename for the exported screenshot." }),
execute: async ({ title }, config) => {
await mapElement.viewOnReady();
const filename = title?.trim() || "Untitled Map";
const screenshot = await mapElement.takeScreenshot();
if (!screenshot?.dataUrl) throw new Error("The screenshot returned no image.");
const url = await composeScreenshot(screenshot.dataUrl, filename);
data: { label: "Open Screenshot", url, title: filename, filename, thumbnailUrl: screenshot.dataUrl },
return `${filename} successfully exported.`;
* Export the current map as a PDF using the portal's print helper service.
* Prompts the user for a title to use as the filename.
const exportToPdf = new FunctionTool({
description: "Export the current map extent as a PDF.",
inputSchema: emptySchema,
createHumanInTheLoopToolMiddleware({
interrupt: () => ({ kind: "textInput", message: "Provide a filename for the exported PDF." }),
execute: async ({ title }, config) => {
await mapElement.viewOnReady();
const filename = title?.trim() || "Untitled Map";
const portal = Portal.getDefault();
const url = portal.helperServices?.printTask?.url;
if (!url) throw new Error("The portal does not provide a print helper service.");
const result = await print.execute(
template: new PrintTemplate({
layout: "letter-ansi-a-landscape",
layoutOptions: { titleText: filename },
{ signal: config?.signal },
if (!result?.url) throw new Error("The print service returned no PDF URL.");
thumbnailUrl = (await mapElement.takeScreenshot())?.dataUrl;
console.warn("PDF preview unavailable.", error);
{ type: "button", data: { label: "Open PDF", url: result.url, title: filename, thumbnailUrl } },
return "PDF exported successfully.";
* Save the current map as a new web map in the portal.
* Prompts the user for a title to use for the web map.
const saveAsWebMap = new FunctionTool({
"Save the current state as a new web map. The middleware obtains its title; do not ask for it in chat.",
inputSchema: emptySchema,
createHumanInTheLoopToolMiddleware({
interrupt: () => ({ kind: "textInput", message: "Provide a title for the exported web map." }),
execute: async ({ title }, config) => {
await mapElement.viewOnReady();
const map = mapElement.map;
await map.updateFrom(mapElement.view);
layer.type === "group" || layer.type === "map-image" ? (layer.layers ?? layer.sublayers ?? []) : [],
if (layer.type === "feature" && layer.featureEffect) layer.featureEffect.excludedLabelsVisible = true;
const item = await map.saveAs({ title: title?.trim() || "Untitled Map" });
if (!item?.id) throw new Error("The portal did not return a saved web map item.");
const url = new URL(`${item.portal.url}/apps/mapviewer/index.html`);
url.search = new URLSearchParams({
configurableview: "true",
center: `${mapElement.center.longitude},${mapElement.center.latitude}`,
scale: String(mapElement.scale),
data: { label: "Open Web Map", url: url.href, title: item.title, thumbnailUrl: item.thumbnailUrl },
return "Web map exported successfully.";
const mapExportAgent = new SkillAgent({
name: "Map Export Agent",
description: "Export the current map as a web map, PDF, or screenshot after human input and approval.",
systemPrompt: "You are an ArcGIS map export agent.",
description: Export the current map as a web map, PDF, or screenshot.
allowed-tools: saveAsWebMap exportToPdf exportScreenshot
- Use "saveAsWebMap" for a new interactive web map, "exportToPdf" for PDF, and "exportScreenshot" for PNG.
- Use exactly the tool matching the requested format. Do not invoke multiple tools for a single export.
- The middleware obtains the title or filename and explicit approval. Never supply these human responses yourself.
- Screenshot export displays the title above the current map image.
- Report success only from successful tool results. Do not retry an export after rejection or failure automatically.`,
saveAsWebMap: async () => saveAsWebMap,
exportToPdf: async () => exportToPdf,
exportScreenshot: async () => exportScreenshot,
// Register the map export agent with the assistant component
const agentElement = document.createElement("arcgis-assistant-agent");
agentElement.agent = mapExportAgent;
assistant.append(agentElement);
assistant.suggestedPrompts = [
"Export this map as a screenshot.",
"Export this map to PDF.",
"Save this map as a new web map.",