Spaces:
Running
Running
Download src/lib/copilotTools.ts from mrfakename/flow: direct link, hf CLI and curl.
- Browser
- Download file 25.9 kB
-
https://huggingface.co/spaces/mrfakename/flow/resolve/main/src/lib/copilotTools.ts
- Command line
-
hf download hf://spaces/mrfakename/flow/src/lib/copilotTools.ts
-
curl -L -o copilotTools.ts https://huggingface.co/spaces/mrfakename/flow/resolve/main/src/lib/copilotTools.ts
25.9 kB
| 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.`; | |