UNPKG

@agentforce/adk

Version:

AgentForce Agent Development Kit - A powerful framework for building AI agents and servers

246 lines (220 loc) 8.64 kB
import type { ToolImplementation } from "../types"; /** * Maximum response size in bytes (5MB default) */ const MAX_RESPONSE_BYTES_DEFAULT = 5_000_000; /** * Default timeout in milliseconds (30 seconds) */ const TIMEOUT_MS_DEFAULT = 30_000; /** * Maximum number of redirects to follow */ const MAX_REDIRECTS_DEFAULT = 5; /** * API fetch tool - performs HTTP requests with security and resource limits */ export const api_fetch: ToolImplementation = { definition: { type: "function", function: { name: "api_fetch", description: "Fetch content from a web URL with HTTP methods, headers, timeout, and response size limits. Supports GET, POST, PUT, DELETE, PATCH methods.", parameters: { type: "object", properties: { url: { type: "string", description: "The URL to fetch content from (must be http:// or https://)", }, method: { type: "string", description: "HTTP method to use", enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], }, headers: { type: "string", description: "HTTP headers as JSON object (e.g., '{\"Content-Type\":\"application/json\",\"Authorization\":\"Bearer token\"}')", }, body: { type: "string", description: "Request body data (for POST, PUT, PATCH methods)", }, timeout_ms: { type: "number", description: "Request timeout in milliseconds (default 30000)", }, max_response_bytes: { type: "number", description: "Maximum response size in bytes (default 5MB)", }, follow_redirects: { type: "boolean", description: "Whether to follow HTTP redirects (default true)", }, }, required: ["url"], }, }, }, execute: async (args: Record<string, any>) => { const url = String(args.url || "").trim(); const method = String(args.method || "GET").toUpperCase(); const timeoutMs = Number.isFinite(args.timeout_ms) ? Number(args.timeout_ms) : TIMEOUT_MS_DEFAULT; const maxBytes = Number.isFinite(args.max_response_bytes) ? Number(args.max_response_bytes) : MAX_RESPONSE_BYTES_DEFAULT; const followRedirects = args.follow_redirects !== false; // Default to true // Validate URL if (!url) { return { success: false, error: "URL is required and cannot be empty", }; } let parsedUrl: URL; try { parsedUrl = new URL(url); } catch { return { success: false, error: "Invalid URL format", url, }; } // Security: Only allow http and https protocols if (!["http:", "https:"].includes(parsedUrl.protocol)) { return { success: false, error: "Only HTTP and HTTPS protocols are allowed", url, protocol: parsedUrl.protocol, }; } // Validate method const allowedMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; if (!allowedMethods.includes(method)) { return { success: false, error: `HTTP method '${method}' is not allowed. Allowed methods: ${allowedMethods.join(", ")}`, method, }; } // Parse headers let headers: Record<string, string> = {}; if (args.headers) { try { const parsed = JSON.parse(String(args.headers)); if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) { headers = parsed; } else { return { success: false, error: "headers must be a JSON object with string values", }; } } catch { return { success: false, error: "headers must be valid JSON object", }; } } // Set default headers if (!headers["User-Agent"]) { headers["User-Agent"] = "AgentForce-ADK/1.0"; } // Prepare request options const requestOptions: RequestInit = { method, headers, redirect: followRedirects ? "follow" : "manual", }; // Add body for appropriate methods if (["POST", "PUT", "PATCH"].includes(method) && args.body) { requestOptions.body = String(args.body); // Set Content-Type if not provided and body looks like JSON if (!headers["Content-Type"] && !headers["content-type"]) { try { JSON.parse(String(args.body)); headers["Content-Type"] = "application/json"; } catch { // Not JSON, leave Content-Type unset } } } // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); requestOptions.signal = controller.signal; try { const startTime = Date.now(); const response = await fetch(url, requestOptions); clearTimeout(timeoutId); const responseTime = Date.now() - startTime; // Read response with size limit let content = ""; let contentLength = 0; let truncated = false; if (response.body) { const reader = response.body.getReader(); const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; contentLength += value.length; if (contentLength > maxBytes) { truncated = true; // Read up to the limit const remainingBytes = maxBytes - (contentLength - value.length); if (remainingBytes > 0) { const limitedChunk = value.slice(0, remainingBytes); content += decoder.decode(limitedChunk, { stream: false }); } break; } content += decoder.decode(value, { stream: true }); } } finally { reader.releaseLock(); } } // Collect response headers const responseHeaders: Record<string, string> = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); return { success: response.ok, status: response.status, statusText: response.statusText, url: response.url, // Final URL after redirects headers: responseHeaders, content, contentLength, truncated, responseTime, method, requestUrl: url, }; } catch (error: any) { clearTimeout(timeoutId); if (error.name === "AbortError") { return { success: false, error: "Request timed out", timedOut: true, timeoutMs, url, method, }; } return { success: false, error: error.message || "Network request failed", url, method, networkError: true, }; } }, };