Spaces:
Running
Running
File size: 25,932 Bytes
fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 a50a219 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 86da9e3 fb57ab3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | import { absolutePosition, fnToDef } from "./groups";
import { isUsable, searchSpaces, stageLabel } from "./hub";
import { absRect } from "./layout";
import type { ToolSpec } from "./llm";
import { allDefs, canConnect, getDef, hasDef } from "./registry";
import type { NodeDef, OutputValue } from "./types";
import { requestFit, visibleArea } from "./viewport";
import { useCanvasStore } from "../store/useCanvasStore";
/** read: runs immediately. edit: needs approval in "ask". run: needs approval unless "yolo". */
export type ToolKind = "read" | "edit" | "run";
interface Tool {
kind: ToolKind;
spec: ToolSpec;
/** One line shown in the approval card. */
summarise: (args: Record<string, unknown>) => string;
run: (args: Record<string, unknown>, ctx: { token: string | null; signal: AbortSignal }) => Promise<unknown>;
}
const str = (v: unknown, fallback = "") => (typeof v === "string" ? v : fallback);
function describeDef(d: NodeDef) {
return {
type: d.type,
title: d.title,
description: d.description,
inputs: d.inputs.map((p) => ({ id: p.id, type: p.type, label: p.label, optional: !!p.optional, widget: p.widget ? { kind: p.widget.kind, default: p.widget.default, options: (p.widget as { options?: unknown }).options, min: (p.widget as { min?: number }).min, max: (p.widget as { max?: number }).max } : undefined })),
outputs: d.outputs.map((p) => ({ id: p.id, type: p.type, label: p.label })),
widgets: d.widgets.map((w) => ({ id: w.id, kind: w.kind, label: w.label, default: w.default })),
space: d.space ? { id: d.space.id, endpoint: d.space.apiName } : undefined,
};
}
type AnyNode = ReturnType<typeof useCanvasStore.getState>["nodes"][number];
/** Where a node sits on the canvas, in absolute canvas coordinates (top-left), plus its rendered size. */
function placement(all: AnyNode[], n: AnyNode) {
const r = absRect(all, n);
return { position: { x: r.x, y: r.y }, width: r.width, height: r.height, ...(n.parentId ? { group: n.parentId } : {}) };
}
function snapshotWorkflow() {
const s = useCanvasStore.getState();
const nodes = s.nodes
.filter((n) => n.type !== "group")
.map((n) => {
const def = getDef(n.data.defType);
const linked: Record<string, string> = {};
for (const e of s.edges) if (e.target === n.id && e.targetHandle) linked[e.targetHandle] = `${e.source}.${e.sourceHandle}`;
return {
id: n.id,
type: n.data.defType,
title: n.data.title ?? def.title,
...placement(s.nodes, n),
mode: n.data.mode,
status: n.data.status,
error: n.data.error,
values: n.data.values,
status_text: n.data.statusText,
linked_inputs: linked,
inputs: def.inputs.map((p) => `${p.id}:${p.type}${p.optional ? "?" : ""}`),
outputs: def.outputs.map((p) => `${p.id}:${p.type}`),
last_outputs: n.data.outputs ? summariseOutputs(n.data.outputs, 160) : undefined,
};
});
const edges = s.edges.map((e) => ({ id: e.id, from: `${e.source}.${e.sourceHandle}`, to: `${e.target}.${e.targetHandle}` }));
const groups = s.nodes.filter((n) => n.type === "group").map((n) => ({ id: n.id, title: n.data.title, ...placement(s.nodes, n) }));
return { name: s.workflowName, run: runSummary(), viewport: visibleArea(), nodes, ...(groups.length ? { groups } : {}), edges, spaces: Object.values(s.spaces).map((sp) => ({ id: sp.summary.id, status: sp.status, error: sp.error, endpoints: sp.defs.map((d) => d.type) })) };
}
const GAP = 40;
const NEW_W = 260;
const NEW_H = 160;
type Rect = { x: number; y: number; w: number; h: number };
const overlaps = (a: Rect, b: Rect, pad: number) => a.x < b.x + b.w + pad && a.x + a.w + pad > b.x && a.y < b.y + b.h + pad && a.y + a.h + pad > b.y;
/**
* Picks a spot for a new node: to the right of `near` (or of the whole graph),
* then slides down past anything already occupying that column so nodes never land on top of each other.
*/
export function nextPosition(near?: string) {
const { nodes } = useCanvasStore.getState();
const rects: Rect[] = nodes.map((n) => {
const p = absolutePosition(nodes, n);
return { x: p.x, y: p.y, w: n.measured?.width ?? n.width ?? NEW_W, h: n.measured?.height ?? n.height ?? (n.data.collapsed ? 32 : 140) };
});
const src = near ? nodes.find((n) => n.id === near) : undefined;
let x: number;
let y: number;
if (src) {
const r = rects[nodes.indexOf(src)];
x = r.x + r.w + GAP * 2;
y = r.y;
} else if (!nodes.length) {
return { x: 80, y: 120 };
} else {
x = Math.max(...rects.map((r) => r.x + r.w)) + GAP * 2;
y = Math.min(...rects.map((r) => r.y));
}
const snap = (v: number) => Math.round(v / 20) * 20;
x = snap(x);
y = snap(y);
// Slide down until the slot is free; the column to the right of everything is free by construction,
// but a `near` placement can collide with the source's other neighbours.
for (let i = 0; i < 200; i++) {
const hit = rects.find((r) => overlaps({ x, y, w: NEW_W, h: NEW_H }, r, GAP / 2));
if (!hit) break;
y = snap(hit.y + hit.h + GAP);
}
return { x, y };
}
/** Compact, model-friendly view of node outputs: file URLs kept, long text/JSON truncated. */
function summariseOutputs(out: Record<string, OutputValue>, max: number): Record<string, unknown> {
const one = (v: OutputValue): unknown => {
if (v == null || typeof v === "number" || typeof v === "boolean") return v;
if (typeof v === "string") return v.length > max ? `${v.slice(0, max)}β¦ (${v.length} chars)` : v;
if (Array.isArray(v)) return v.slice(0, 8).map((x) => one(x as OutputValue));
if (typeof v === "object" && "url" in v && typeof v.url === "string") return { url: v.url, name: v.name, mime: v.mime };
const j = JSON.stringify(v);
return j.length > max ? `${j.slice(0, max)}β¦ (${j.length} chars)` : v;
};
return Object.fromEntries(Object.entries(out).map(([k, v]) => [k, one(v)]));
}
function runSummary() {
const s = useCanvasStore.getState();
return {
running: s.run.running,
current_node: s.run.currentNodeId,
completed: s.run.completed,
total: s.run.total,
queued: s.queue.length,
last_result: s.history[0] ? { status: s.history[0].status, duration_ms: s.history[0].durationMs, nodes: s.history[0].nodes, error: s.history[0].error, finished_at: new Date(s.history[0].finishedAt).toISOString() } : undefined,
};
}
const fmtLog = (e: { t: number; level: string; node?: string; msg: string }) => `${new Date(e.t).toISOString().slice(11, 23)} ${e.level.toUpperCase().padEnd(5)} ${e.msg}`;
export const TOOLS: Record<string, Tool> = {
get_workflow: {
kind: "read",
spec: { type: "function", function: { name: "get_workflow", description: "Returns the current workflow: nodes (with ids, types, values, port lists, canvas position, width/height and group), groups, edges, the visible viewport area, and which Spaces are loaded.", parameters: { type: "object", properties: {} } } },
summarise: () => "Read the workflow",
run: async () => snapshotWorkflow(),
},
get_node: {
kind: "read",
spec: { type: "function", function: { name: "get_node", description: "Full detail for one node: definition (ports with labels, widgets with options/ranges), current values, incoming links, status, status text, error, progress and complete last outputs (text up to 4000 chars, file URLs).", parameters: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } } },
summarise: (a) => `Inspect ${nodeLabel(str(a.id))}`,
run: async (a) => {
const s = useCanvasStore.getState();
const n = s.nodes.find((x) => x.id === str(a.id));
if (!n) return { error: "No such node" };
const def = getDef(n.data.defType);
const linked: Record<string, string> = {};
for (const e of s.edges) if (e.target === n.id && e.targetHandle) linked[e.targetHandle] = `${e.source}.${e.sourceHandle}`;
const feeds = s.edges.filter((e) => e.source === n.id).map((e) => `${e.sourceHandle} β ${e.target}.${e.targetHandle}`);
return {
id: n.id,
type: n.data.defType,
title: n.data.title ?? def.title,
description: def.description,
...placement(s.nodes, n),
mode: n.data.mode,
status: n.data.status,
status_text: n.data.statusText,
progress: n.data.progress,
error: n.data.error,
values: n.data.values,
linked_inputs: linked,
feeds,
inputs: def.inputs.map((p) => ({ id: p.id, label: p.label, type: p.type, optional: p.optional, widget: p.widget })),
outputs: def.outputs.map((p) => ({ id: p.id, label: p.label, type: p.type })),
widgets: def.widgets,
last_outputs: n.data.outputs ? summariseOutputs(n.data.outputs, 4000) : undefined,
log: s.log.filter((e) => e.node === n.id).slice(-30).map(fmtLog),
};
},
},
get_run_status: {
kind: "read",
spec: { type: "function", function: { name: "get_run_status", description: "Run state, queue, the last runs, per-node statuses/errors, and the run log (newest last). Use it after run_workflow, or whenever the user reports something failed.", parameters: { type: "object", properties: { log_lines: { type: "number", description: "How many log lines to include (default 60, max 400)" }, node: { type: "string", description: "Only log lines for this node id" } } } } },
summarise: () => "Read run status and log",
run: async (a) => {
const s = useCanvasStore.getState();
const limit = Math.min(400, Math.max(1, Number(a.log_lines) || 60));
const node = str(a.node);
const log = (node ? s.log.filter((e) => e.node === node) : s.log).slice(-limit).map(fmtLog);
return {
...runSummary(),
queue: s.queue.map((q) => ({ id: q.id, queued_at: new Date(q.queuedAt).toISOString() })),
recent_runs: s.history.slice(0, 5).map((h) => ({ status: h.status, duration_ms: h.durationMs, nodes: h.nodes, error: h.error, finished_at: new Date(h.finishedAt).toISOString() })),
nodes: s.nodes.filter((n) => n.type !== "group").map((n) => ({ id: n.id, title: n.data.title ?? getDef(n.data.defType).title, status: n.data.status, status_text: n.data.statusText, error: n.data.error })),
log,
};
},
},
wait_for_run: {
kind: "read",
spec: { type: "function", function: { name: "wait_for_run", description: "Blocks until the current run (and queue) finishes or the timeout passes, then returns the same data as get_run_status. Call it right after run_workflow to see what happened.", parameters: { type: "object", properties: { timeout_s: { type: "number", description: "Max seconds to wait (default 120, max 600)" } } } } },
summarise: (a) => `Wait for the run${a.timeout_s ? ` (β€${a.timeout_s}s)` : ""}`,
run: async (a, ctx) => {
const { signal } = ctx;
const timeout = Math.min(600, Math.max(1, Number(a.timeout_s) || 120)) * 1000;
const start = Date.now();
// Give enqueue a tick to flip `running` on.
await new Promise((r) => setTimeout(r, 300));
const busy = () => {
const s = useCanvasStore.getState();
return s.run.running || s.queue.length > 0;
};
while (busy() && Date.now() - start < timeout && !signal?.aborted) await new Promise((r) => setTimeout(r, 500));
const timed_out = busy();
const status = await TOOLS.get_run_status.run({ log_lines: 80 }, ctx);
return { timed_out, waited_ms: Date.now() - start, ...(status as object) };
},
},
get_space_info: {
kind: "read",
spec: { type: "function", function: { name: "get_space_info", description: "Detail for a loaded Space: hub metadata, runtime stage/hardware, load status or error, and every endpoint with its full ports and widgets.", parameters: { type: "object", properties: { id: { type: "string", description: "owner/name" } }, required: ["id"] } } },
summarise: (a) => `Inspect Space ${str(a.id)}`,
run: async (a) => {
const sp = useCanvasStore.getState().spaces[str(a.id)];
if (!sp) return { error: "Space not loaded. Call add_space first." };
const m = sp.summary;
return {
id: m.id,
title: m.title,
description: m.shortDescription,
likes: m.likes,
sdk: m.sdk,
stage: m.stage,
hardware: m.hardware,
zero_gpu: m.zeroGpu,
host: sp.host,
status: sp.status,
error: sp.error,
loaded_at: sp.loadedAt ? new Date(sp.loadedAt).toISOString() : undefined,
endpoints: sp.defs.map((d) => ({ type: d.type, title: d.title, description: d.description, inputs: d.inputs.map((p) => ({ id: p.id, label: p.label, type: p.type, optional: p.optional, widget: p.widget })), outputs: d.outputs.map((p) => ({ id: p.id, label: p.label, type: p.type })) })),
};
},
},
list_node_types: {
kind: "read",
spec: { type: "function", function: { name: "list_node_types", description: "Lists node types available right now (input nodes, loaded Space endpoints, saved functions) with their ports and widgets. Call add_space first to make a Space's endpoints available.", parameters: { type: "object", properties: { type: { type: "string", description: "Optional: a single node type to describe in full" } } } } },
summarise: () => "List node types",
run: async (a) => {
const t = str(a.type);
const defs = [...allDefs(), ...useCanvasStore.getState().functions.map(fnToDef)];
if (t) {
const d = defs.find((x) => x.type === t);
return d ? describeDef(d) : { error: `Unknown node type ${t}` };
}
return defs.map((d) => ({ type: d.type, title: d.title, category: d.category, inputs: d.inputs.map((p) => `${p.id}:${p.type}`), outputs: d.outputs.map((p) => `${p.id}:${p.type}`) }));
},
},
search_spaces: {
kind: "read",
spec: { type: "function", function: { name: "search_spaces", description: "Searches public Gradio Spaces on the Hugging Face Hub, sorted by likes. Prefer running Spaces (usable=true).", parameters: { type: "object", properties: { query: { type: "string" }, limit: { type: "integer", default: 12 } }, required: ["query"] } } },
summarise: (a) => `Search Spaces for β${str(a.query)}β`,
run: async (a, ctx) => {
const list = await searchSpaces(str(a.query), { token: ctx.token, signal: ctx.signal, limit: Math.min(Number(a.limit) || 12, 30) });
return list.map((s) => ({ id: s.id, title: s.title, likes: s.likes, stage: stageLabel(s.stage), usable: isUsable(s.stage), hardware: s.hardware, zeroGpu: s.zeroGpu, description: s.shortDescription }));
},
},
add_space: {
kind: "edit",
spec: { type: "function", function: { name: "add_space", description: "Loads a Space's API schema and makes its endpoints available as node types (returned in the result). Does not add a node.", parameters: { type: "object", properties: { id: { type: "string", description: "owner/name" } }, required: ["id"] } } },
summarise: (a) => `Add Space ${str(a.id)}`,
run: async (a) => {
const entry = await useCanvasStore.getState().addSpace(str(a.id));
if (!entry) {
const e = useCanvasStore.getState().spaces[str(a.id)];
return { error: e?.error ?? "Could not load this Space" };
}
return { id: entry.summary.id, stage: stageLabel(entry.summary.stage), endpoints: entry.defs.map(describeDef) };
},
},
add_node: {
kind: "edit",
spec: { type: "function", function: { name: "add_node", description: "Adds a node of the given type. Returns its id. Optionally sets widget values and a title.", parameters: { type: "object", properties: { type: { type: "string" }, title: { type: "string" }, values: { type: "object", additionalProperties: true }, near: { type: "string", description: "Node id to place this node to the right of" } }, required: ["type"] } } },
summarise: (a) => `Add node ${hasDef(str(a.type)) ? getDef(str(a.type)).title : str(a.type)}${a.title ? ` β${str(a.title)}β` : ""}`,
run: async (a) => {
const type = str(a.type);
const def = hasDef(type) ? getDef(type) : useCanvasStore.getState().functions.map(fnToDef).find((d) => d.type === type);
if (!def) return { error: `Unknown node type ${type}. Call list_node_types or add_space first.` };
const s = useCanvasStore.getState();
const id = s.addNode(type, nextPosition(str(a.near) || undefined), false);
if (a.title) s.patchNode(id, { title: str(a.title) });
const vals = (a.values ?? {}) as Record<string, string | number | boolean>;
const rejected: string[] = [];
for (const [k, v] of Object.entries(vals)) {
const known = def.widgets.some((w) => w.id === k) || def.inputs.some((p) => p.id === k && p.widget);
if (known) s.updateWidget(id, k, v);
else rejected.push(k);
}
const added = useCanvasStore.getState().nodes.find((x) => x.id === id);
return { id, ...(added ? { position: placement(useCanvasStore.getState().nodes, added).position } : {}), ...(rejected.length ? { ignored_values: rejected } : {}) };
},
},
set_values: {
kind: "edit",
spec: { type: "function", function: { name: "set_values", description: "Sets widget / input values on a node.", parameters: { type: "object", properties: { node: { type: "string" }, values: { type: "object", additionalProperties: true } }, required: ["node", "values"] } } },
summarise: (a) => `Set ${Object.keys((a.values as object) ?? {}).join(", ")} on ${nodeLabel(str(a.node))}`,
run: async (a) => {
const s = useCanvasStore.getState();
const n = s.nodes.find((x) => x.id === str(a.node));
if (!n) return { error: "No such node" };
const def = getDef(n.data.defType);
const rejected: string[] = [];
for (const [k, v] of Object.entries((a.values ?? {}) as Record<string, string | number | boolean>)) {
const known = def.widgets.some((w) => w.id === k) || def.inputs.some((p) => p.id === k && p.widget);
if (known) s.updateWidget(n.id, k, v);
else rejected.push(k);
}
return { ok: true, ...(rejected.length ? { ignored_values: rejected } : {}) };
},
},
connect: {
kind: "edit",
spec: { type: "function", function: { name: "connect", description: "Links an output port to an input port. Types must be compatible. Replaces any existing link on that input.", parameters: { type: "object", properties: { from_node: { type: "string" }, from_port: { type: "string" }, to_node: { type: "string" }, to_port: { type: "string" } }, required: ["from_node", "from_port", "to_node", "to_port"] } } },
summarise: (a) => `Link ${nodeLabel(str(a.from_node))}.${str(a.from_port)} β ${nodeLabel(str(a.to_node))}.${str(a.to_port)}`,
run: async (a) => {
const s = useCanvasStore.getState();
const src = s.nodes.find((x) => x.id === str(a.from_node));
const tgt = s.nodes.find((x) => x.id === str(a.to_node));
if (!src || !tgt) return { error: "No such node" };
const sp = getDef(src.data.defType).outputs.find((p) => p.id === str(a.from_port));
const tp = getDef(tgt.data.defType).inputs.find((p) => p.id === str(a.to_port));
if (!sp) return { error: `${src.id} has no output ${str(a.from_port)}` };
if (!tp) return { error: `${tgt.id} has no input ${str(a.to_port)}` };
if (!canConnect(sp.type, tp.type)) return { error: `Type mismatch: ${sp.type} β ${tp.type}` };
s.setInputSource(tgt.id, tp.id, { nodeId: src.id, portId: sp.id });
return { ok: true };
},
},
disconnect: {
kind: "edit",
spec: { type: "function", function: { name: "disconnect", description: "Removes the link feeding an input port.", parameters: { type: "object", properties: { node: { type: "string" }, port: { type: "string" } }, required: ["node", "port"] } } },
summarise: (a) => `Unlink ${nodeLabel(str(a.node))}.${str(a.port)}`,
run: async (a) => {
useCanvasStore.getState().setInputSource(str(a.node), str(a.port), null);
return { ok: true };
},
},
remove_node: {
kind: "edit",
spec: { type: "function", function: { name: "remove_node", description: "Deletes a node and its links.", parameters: { type: "object", properties: { node: { type: "string" } }, required: ["node"] } } },
summarise: (a) => `Delete ${nodeLabel(str(a.node))}`,
run: async (a) => {
const s = useCanvasStore.getState();
if (!s.nodes.some((n) => n.id === str(a.node))) return { error: "No such node" };
s.removeNode(str(a.node));
return { ok: true };
},
},
move_nodes: {
kind: "edit",
spec: { type: "function", function: { name: "move_nodes", description: "Moves nodes to absolute canvas positions (top-left corner, same coordinates as get_workflow's position). One undoable step. Leave about 80px horizontally and 40px vertically between nodes, using their width/height so nothing overlaps.", parameters: { type: "object", properties: { moves: { type: "array", items: { type: "object", properties: { node: { type: "string" }, x: { type: "number" }, y: { type: "number" } }, required: ["node", "x", "y"] } } }, required: ["moves"] } } },
summarise: (a) => {
const m = Array.isArray(a.moves) ? (a.moves as { node?: unknown }[]) : [];
return m.length === 1 ? `Move ${nodeLabel(str(m[0].node))}` : `Move ${m.length} nodes`;
},
run: async (a) => {
const raw = Array.isArray(a.moves) ? (a.moves as Record<string, unknown>[]) : [];
const moves = raw.map((m) => ({ id: str(m.node), x: Number(m.x), y: Number(m.y) })).filter((m) => m.id && Number.isFinite(m.x) && Number.isFinite(m.y));
if (!moves.length) return { error: "moves must be a list of {node, x, y} with numeric x and y" };
const missing = useCanvasStore.getState().moveNodes(moves);
requestFit();
return { moved: moves.length - missing.length, ...(missing.length ? { missing } : {}) };
},
},
auto_layout: {
kind: "edit",
spec: { type: "function", function: { name: "auto_layout", description: "Tidies the layout: arranges nodes left to right by data flow (inputs on the left, each node right of what feeds it), with no overlaps. One undoable step. Pass node ids to arrange only those (they keep their parent group); omit to arrange the whole workflow.", parameters: { type: "object", properties: { nodes: { type: "array", items: { type: "string" } } } } } },
summarise: (a) => (Array.isArray(a.nodes) && a.nodes.length ? `Arrange ${a.nodes.length} node${a.nodes.length === 1 ? "" : "s"}` : "Tidy the layout"),
run: async (a) => {
const ids = Array.isArray(a.nodes) && a.nodes.length ? a.nodes.map(String) : undefined;
const known = new Set(useCanvasStore.getState().nodes.map((n) => n.id));
const missing = ids?.filter((id) => !known.has(id)) ?? [];
const moved = useCanvasStore.getState().autoLayout(ids?.filter((id) => known.has(id)));
requestFit();
return { moved, ...(missing.length ? { missing } : {}) };
},
},
run_workflow: {
kind: "run",
spec: { type: "function", function: { name: "run_workflow", description: "Queues a run. With targets, runs only those nodes and what feeds them. Spends the user's Hugging Face quota. Returns immediately; call get_workflow later to read statuses and outputs.", parameters: { type: "object", properties: { targets: { type: "array", items: { type: "string" } } } } } },
summarise: (a) => (Array.isArray(a.targets) && a.targets.length ? `Run up to ${a.targets.map((t) => nodeLabel(String(t))).join(", ")}` : "Run the whole workflow"),
run: async (a) => {
const targets = Array.isArray(a.targets) && a.targets.length ? a.targets.map(String) : undefined;
useCanvasStore.getState().enqueue(targets);
return { queued: true };
},
},
};
function nodeLabel(id: string) {
const n = useCanvasStore.getState().nodes.find((x) => x.id === id);
return n ? `${n.data.title ?? getDef(n.data.defType).title} (${id})` : id;
}
export const TOOL_SPECS = Object.values(TOOLS).map((t) => t.spec);
export const SYSTEM_PROMPT = `You are the copilot inside a node-based workflow editor for the Hugging Face Hub. Every public Gradio Space can become a node: its API endpoints turn into node types whose input ports mirror the Space's parameters. Input nodes (in:text, in:image, in:audio, in:video, in:file, in:number, in:toggle) feed values in. Links go from an output port to an input port with a compatible type (image, audio, video, file, text, number, boolean, gallery, json; "any" matches anything). Outputs from one Space flow into the next as URLs.
How to work:
- Start by calling get_workflow so you know the current node ids and what is loaded.
- To use a Space, search_spaces, pick a running one (usable=true, most likes), then add_space to load its endpoints. Read the returned endpoint ports before adding nodes.
- Build graphs with add_node and connect. Use the node ids you get back. Set sensible widget values.
- Keep the canvas readable. After building or rewiring a workflow, call auto_layout once so it reads left to right with nothing overlapping. Use move_nodes for fine placement (positions and sizes come from get_workflow; x grows right, y grows down).
- Only run when the user asks; runs cost the user's quota. After run_workflow, call wait_for_run to see the outcome, then report it.
- When something fails or the user asks why, read get_run_status (log + per-node errors) and get_node on the failing node before answering. Quote the actual error text.
- Be brief. Say what you did in one or two sentences; no bullet-point recaps of tool calls. Ask a short question only when the request is truly ambiguous.
- Node types look like "space:owner/name/endpoint" or "in:text". Never invent types; use ones returned by tools.`;
|