@huggingface/tiny-agents
Version:
Lightweight, composable agents for AI applications
580 lines (567 loc) • 17.1 kB
JavaScript
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();