UNPKG

@huggingface/tiny-agents

Version:

Lightweight, composable agents for AI applications

580 lines (567 loc) 17.1 kB
#!/usr/bin/env node import "./chunk-FFYIGW52.mjs"; // src/cli.ts import { parseArgs } from "util"; import * as readline2 from "readline/promises"; import { stdin as stdin2, stdout as stdout3 } from "process"; import { z as z3 } from "zod"; import { PROVIDERS_OR_POLICIES } from "@huggingface/inference"; import { Agent } from "@huggingface/mcp-client"; // package.json var version = "0.3.4"; // src/lib/types.ts import { z } from "zod"; var ServerConfigSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("stdio"), command: z.string(), args: z.array(z.string()).optional(), env: z.record(z.string()).optional(), cwd: z.string().optional() }), z.object({ type: z.literal("http"), url: z.union([z.string(), z.string().url()]), headers: z.record(z.string()).optional() }), z.object({ type: z.literal("sse"), url: z.union([z.string(), z.string().url()]), headers: z.record(z.string()).optional() }) ]); var InputConfigSchema = z.object({ id: z.string(), description: z.string(), type: z.string().optional(), password: z.boolean().optional() }); // src/lib/utils.ts import { inspect } from "util"; function debug(...args) { if (process.env.DEBUG) { console.debug(inspect(args, { depth: Infinity, colors: true })); } } function error(...args) { console.error(ANSI.RED + args.join("") + ANSI.RESET); } var ANSI = { BLUE: "\x1B[34m", GRAY: "\x1B[90m", GREEN: "\x1B[32m", RED: "\x1B[31m", RESET: "\x1B[0m", YELLOW: "\x1B[33m" }; // src/lib/mainCliLoop.ts import * as readline from "readline/promises"; import { stdin, stdout } from "process"; async function mainCliLoop(agent) { const rl = readline.createInterface({ input: stdin, output: stdout }); let abortController = new AbortController(); let waitingForInput = false; async function waitForInput() { waitingForInput = true; const input = await rl.question("> "); waitingForInput = false; return input; } rl.on("SIGINT", async () => { if (waitingForInput) { await agent.cleanup(); stdout.write("\n"); rl.close(); } else { abortController.abort(); abortController = new AbortController(); stdout.write("\n"); stdout.write(ANSI.GRAY); stdout.write("Ctrl+C a second time to exit"); stdout.write(ANSI.RESET); stdout.write("\n"); } }); process.on("uncaughtException", (err) => { stdout.write("\n"); rl.close(); throw err; }); stdout.write(ANSI.BLUE); stdout.write(`Agent loaded with ${agent.availableTools.length} tools: `); stdout.write(agent.availableTools.map((t) => `- ${t.function.name}`).join("\n")); stdout.write(ANSI.RESET); stdout.write("\n"); while (true) { const input = await waitForInput(); for await (const chunk of agent.run(input, { abortSignal: abortController.signal })) { if ("choices" in chunk) { const delta = chunk.choices[0]?.delta; if (delta.content) { stdout.write(delta.content); } if (delta.tool_calls) { stdout.write(ANSI.GRAY); for (const deltaToolCall of delta.tool_calls) { if (deltaToolCall.id) { stdout.write(`<Tool ${deltaToolCall.id}> `); } if (deltaToolCall.function.name) { stdout.write(deltaToolCall.function.name + " "); } if (deltaToolCall.function.arguments) { stdout.write(deltaToolCall.function.arguments); } } stdout.write(ANSI.RESET); } } else { stdout.write("\n\n"); stdout.write(ANSI.GREEN); stdout.write(`Tool[${chunk.name}] ${chunk.tool_call_id} `); stdout.write(chunk.content); stdout.write(ANSI.RESET); stdout.write("\n\n"); } } stdout.write("\n"); } } // src/lib/webServer.ts import { createServer, ServerResponse } from "http"; import { z as z2 } from "zod"; import { stdout as stdout2 } from "process"; var REQUEST_ID_HEADER = "X-Request-Id"; var ChatCompletionInputSchema = z2.object({ messages: z2.array( z2.object({ role: z2.enum(["user", "assistant"]), content: z2.string().or( z2.array( z2.object({ type: z2.literal("text"), text: z2.string() }).or( z2.object({ type: z2.literal("image_url"), image_url: z2.object({ url: z2.string() }) }) ) ) ) }) ), /// Only allow stream: true stream: z2.literal(true) }); function getJsonBody(req) { return new Promise((resolve, reject) => { let data = ""; req.on("data", (chunk) => data += chunk); req.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); req.on("error", reject); }); } var ServerResp = class extends ServerResponse { error(statusCode, reason) { this.writeHead(statusCode).end(JSON.stringify({ error: reason })); } }; function startServer(agent) { const server = createServer({ ServerResponse: ServerResp }, async (req, res) => { res.setHeader(REQUEST_ID_HEADER, crypto.randomUUID()); res.setHeader("Content-Type", "application/json"); if (req.method === "POST" && req.url === "/v1/chat/completions") { let body; let requestBody; try { body = await getJsonBody(req); } catch { return res.error(400, "Invalid JSON"); } try { requestBody = ChatCompletionInputSchema.parse(body); } catch (err) { if (err instanceof z2.ZodError) { return res.error(400, "Invalid ChatCompletionInput body \n" + JSON.stringify(err)); } return res.error(400, "Invalid ChatCompletionInput body"); } res.setHeaders( new Headers({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }) ); const messages = [ { role: "system", content: agent.prompt }, ...requestBody.messages ]; for await (const chunk of agent.run(messages)) { if ("choices" in chunk) { res.write(`data: ${JSON.stringify(chunk)} `); } else { const chunkToolcallInfo = { choices: [ { index: 0, delta: { role: "tool", content: `Tool[${chunk.name}] ${chunk.tool_call_id} ` + chunk.content } } ], created: Math.floor(Date.now() / 1e3), id: chunk.tool_call_id, model: "", system_fingerprint: "" }; res.write(`data: ${JSON.stringify(chunkToolcallInfo)} `); } } res.end(); } else { res.error(404, "Route or method not found, try POST /v1/chat/completions"); } }); server.listen(process.env.PORT ? parseInt(process.env.PORT) : 9999, () => { stdout2.write(ANSI.BLUE); stdout2.write(`Agent loaded with ${agent.availableTools.length} tools: `); stdout2.write(agent.availableTools.map((t) => `- ${t.function.name}`).join("\n")); stdout2.write(ANSI.RESET); stdout2.write("\n"); console.log(ANSI.GRAY + `listening on http://localhost:${server.address().port}` + ANSI.RESET); }); } // src/lib/loadConfigFrom.ts import { join } from "path"; import { lstat, readFile } from "fs/promises"; import { downloadFileToCacheDir } from "@huggingface/hub"; var FILENAME_CONFIG = "agent.json"; var PROMPT_FILENAMES = ["AGENTS.md", "PROMPT.md"]; var TINY_AGENTS_HUB_REPO = { name: "tiny-agents/tiny-agents", type: "dataset" }; async function tryLoadFromFile(filePath) { try { const configJson = await readFile(filePath, { encoding: "utf8" }); return { configJson }; } catch { return void 0; } } async function tryLoadFromDirectory(dirPath) { const stats = await lstat(dirPath).catch(() => void 0); if (!stats?.isDirectory()) { return void 0; } let prompt; for (const filename of PROMPT_FILENAMES) { try { prompt = await readFile(join(dirPath, filename), { encoding: "utf8" }); break; } catch { } } if (void 0 == prompt) { debug( `${PROMPT_FILENAMES.join(", ")} could not be loaded locally from ${dirPath}, continuing without prompt template` ); } try { return { configJson: await readFile(join(dirPath, FILENAME_CONFIG), { encoding: "utf8" }), prompt }; } catch { error(`Config file not found in specified local directory.`); process.exit(1); } } async function tryLoadFromHub(agentId) { let configJson; try { const configPath = await downloadFileToCacheDir({ repo: TINY_AGENTS_HUB_REPO, path: `${agentId}/${FILENAME_CONFIG}`, accessToken: process.env.HF_TOKEN }); configJson = await readFile(configPath, { encoding: "utf8" }); } catch { return void 0; } let prompt; for (const filename of PROMPT_FILENAMES) { try { const promptPath = await downloadFileToCacheDir({ repo: TINY_AGENTS_HUB_REPO, path: `${agentId}/${filename}`, accessToken: process.env.HF_TOKEN }); prompt = await readFile(promptPath, { encoding: "utf8" }); break; } catch { } } if (void 0 == prompt) { debug( `${PROMPT_FILENAMES.join( ", " )} not found in https://huggingface.co/datasets/tiny-agents/tiny-agents/tree/main/${agentId}, continuing without prompt template` ); } return { configJson, prompt }; } async function loadConfigFrom(loadFrom) { const fileConfig = await tryLoadFromFile(loadFrom); if (fileConfig) { return fileConfig; } const dirConfig = await tryLoadFromDirectory(loadFrom); if (dirConfig) { return dirConfig; } const repoConfig = await tryLoadFromHub(loadFrom); if (repoConfig) { return repoConfig; } error( `Config file not found in tiny-agents! Please make sure it exists locally or in https://huggingface.co/datasets/tiny-agents/tiny-agents.` ); process.exit(1); } // src/cli.ts var USAGE_HELP = ` Usage: tiny-agents [flags] tiny-agents run "agent/id" tiny-agents serve "agent/id" Available Commands: run Run the Agent in command-line serve Run the Agent as an OpenAI-compatible HTTP server Flags: -h, --help help for tiny-agents -v, --version Show version information `.trim(); var CLI_COMMANDS = ["run", "serve"]; function isValidCommand(command) { return CLI_COMMANDS.includes(command); } async function main() { const { values: { help, version: version2 }, positionals } = parseArgs({ options: { help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" } }, allowPositionals: true }); if (version2) { console.log(version); process.exit(0); } const command = positionals[0]; const loadFrom = positionals[1]; if (help) { console.log(USAGE_HELP); process.exit(0); } if (positionals.length !== 2 || !isValidCommand(command)) { error(`You need to call run or serve, followed by an agent id (local path or Hub identifier).`); console.log(USAGE_HELP); process.exit(1); } const { configJson, prompt } = await loadConfigFrom(loadFrom); const ConfigSchema = z3.object({ model: z3.string(), provider: z3.enum(PROVIDERS_OR_POLICIES).optional(), endpointUrl: z3.string().optional(), apiKey: z3.string().optional(), inputs: z3.array(InputConfigSchema).optional(), servers: z3.array(ServerConfigSchema) }).refine((data) => data.provider !== void 0 || data.endpointUrl !== void 0, { message: "At least one of 'provider' or 'endpointUrl' is required" }); let config; try { const parsedConfig = JSON.parse(configJson); config = ConfigSchema.parse(parsedConfig); } catch (err) { error("Invalid configuration file:", err instanceof Error ? err.message : err); process.exit(1); } if (config.inputs && config.inputs.length > 0) { const rl = readline2.createInterface({ input: stdin2, output: stdout3 }); stdout3.write(ANSI.BLUE); stdout3.write("Some initial inputs are required by the agent. "); stdout3.write("Please provide a value or leave empty to load from env."); stdout3.write(ANSI.RESET); stdout3.write("\n"); for (const inputItem of config.inputs) { const inputId = inputItem.id; const description = inputItem.description; const envSpecialValue = `\${input:${inputId}}`; const inputVars = /* @__PURE__ */ new Set(); for (const server of config.servers) { if (server.type === "stdio" && server.env) { for (const [key, value] of Object.entries(server.env)) { if (value.includes(envSpecialValue)) { inputVars.add(key); } } } if ((server.type === "http" || server.type === "sse") && server.headers) { for (const [key, value] of Object.entries(server.headers)) { if (value.includes(envSpecialValue)) { inputVars.add(key); } } } } if (config.apiKey?.includes(envSpecialValue)) { inputVars.add("apiKey"); } if (inputVars.size === 0) { stdout3.write(ANSI.YELLOW); stdout3.write(`Input ${inputId} defined in config but not used by any server or as an API key. Skipping.`); stdout3.write(ANSI.RESET); stdout3.write("\n"); continue; } const envVariableKey = inputId.replaceAll("-", "_").toUpperCase(); stdout3.write(ANSI.BLUE); stdout3.write(` \u2022 ${inputId}`); stdout3.write(ANSI.RESET); stdout3.write(`: ${description}. (default: load from ${envVariableKey}) `); stdout3.write("\n"); const userInput = (await rl.question("")).trim(); const valueFromEnv = process.env[envVariableKey] || ""; const finalValue = userInput || valueFromEnv; if (!userInput) { if (valueFromEnv) { stdout3.write(ANSI.GREEN); stdout3.write(`Value successfully loaded from '${envVariableKey}'`); stdout3.write(ANSI.RESET); stdout3.write("\n"); } else { stdout3.write(ANSI.YELLOW); stdout3.write(`No value found for '${envVariableKey}' in environment variables. Continuing.`); stdout3.write(ANSI.RESET); stdout3.write("\n"); } } for (const server of config.servers) { if (server.type === "stdio" && server.env) { for (const [key, value] of Object.entries(server.env)) { if (value.includes(envSpecialValue)) { server.env[key] = value.replace(envSpecialValue, finalValue); } } } if ((server.type === "http" || server.type === "sse") && server.headers) { for (const [key, value] of Object.entries(server.headers)) { if (value.includes(envSpecialValue)) { server.headers[key] = value.replace(envSpecialValue, finalValue); } } } } if (config.apiKey?.includes(envSpecialValue)) { config.apiKey = config.apiKey.replace(envSpecialValue, finalValue); } } stdout3.write("\n"); rl.close(); } debug("Processed servers configuration:"); for (const server of config.servers) { if ((server.type === "http" || server.type === "sse") && server.headers) { debug(`${server.type} server headers:`, server.headers); } } const formattedServers = config.servers.map((server) => { switch (server.type) { case "stdio": return { type: "stdio", config: { command: server.command, args: server.args ?? [], env: server.env ?? {}, cwd: server.cwd ?? process.cwd() } }; case "http": case "sse": { const formatted = { type: server.type, config: { url: server.url, options: { requestInit: { headers: server.headers } } } }; debug(`Formatted ${server.type} server:`, formatted); return formatted; } } }); const agent = new Agent( config.endpointUrl ? { endpointUrl: config.endpointUrl, model: config.model, apiKey: config.apiKey ?? process.env.API_KEY ?? process.env.HF_TOKEN, servers: formattedServers, prompt } : { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion provider: config.provider, model: config.model, apiKey: config.apiKey ?? process.env.API_KEY ?? process.env.HF_TOKEN, servers: formattedServers, prompt } ); debug(agent); await agent.loadTools(); if (command === "run") { mainCliLoop(agent); } else { startServer(agent); } } main();