unsloth/studio/frontend/src/features/recipe-studio/utils/node-data.ts
Daniel Han d8cd2f8693 Studio: add github_repo seed reader and GitHub Support Bot recipe
Adds a first-party Data Designer seed reader that scrapes GitHub issues,
pull requests, and commits from one or more repositories via the GraphQL
API, and a learning recipe (GitHub Support Bot) that turns those rows into
synthetic support Q&A pairs for fine-tuning.

Backend (new plugin studio/backend/plugins/data-designer-github-repo-seed):
* GitHubRepoSeedSource config: repos, token (falls back to GH_TOKEN /
  GITHUB_TOKEN env var), item_types (issues / pulls / commits),
  per-resource limit (0 means all), max_comments_per_item.
* Rate-limit-aware GraphQL client (GitHubClient + RepoScraper) shared
  across repos; flattens each item into a uniform row with columns
  item_type, repo, number, title, body, state, author, created_at,
  closed_at, url, labels, comments.
* Registered via the data_designer.plugins entry point.

Frontend:
* New seed_github block variant so the seed node card shows
  "GitHub repositories" instead of the generic "Document file"
  placeholder, with its own icon and inline summary (repo count +
  item-type list).
* Rewritten seed dialog github_repo form: repos textarea pre-filled with
  unslothai/unsloth + unslothai/unsloth-zoo, password input for the GH
  token, items-per-repo number with an "All" toggle, and the noisier
  options (item types, max comments, include comments) tucked under an
  Advanced collapsible.
* Local model auto-load on Run: if a recipe uses an is_local provider
  and the inference server is not already serving that model, the
  executions hook calls /api/inference/load first. Removes the "open
  /chat to load a model" prerequisite that users kept tripping on.
* Honor the recipe's run.rows value in the Run dialog (previously the
  store reset to 5 regardless of what the template shipped).

Recipe (studio/frontend/src/features/data-recipes/learning-recipes/
github-support-bot.json):
* Defaults to the Local Model provider + unsloth/gemma-4-E2B-it-GGUF.
* Scrapes unslothai/unsloth and unslothai/unsloth-zoo, issues and pulls,
  up to 100 items per resource.
* Two LLM blocks: normalized_question (llm-text) rewrites each thread
  into a clean support question, support_answer (llm-structured)
  produces JSON with answer / diagnosis_questions / cites / confidence.
* Run defaults to 10 rows for a quick smoke test.

Verified end-to-end on a running Studio: card renders, source-data
dialog is pre-populated, All toggle disables the limit input, the
recipe executes and produces rows against a loaded local GGUF.
2026-04-24 13:44:30 +00:00

124 lines
3.2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import type { RecipeNodeData, LayoutDirection, NodeConfig } from "../types";
import {
labelForExpression,
labelForLlm,
labelForSampler,
} from "./config-labels";
export function nodeDataFromConfig(
config: NodeConfig,
layoutDirection: LayoutDirection = "LR",
): RecipeNodeData {
if (config.kind === "sampler") {
return {
title: "Generated field",
kind: "sampler",
subtype: labelForSampler(config.sampler_type),
blockType: config.sampler_type,
name: config.name,
layoutDirection,
};
}
if (config.kind === "expression") {
return {
title: "Formula",
kind: "expression",
subtype: labelForExpression(config.dtype),
blockType: "expression",
name: config.name,
layoutDirection,
};
}
if (config.kind === "validator") {
const isOxc = config.validator_type === "oxc";
const isSql = config.code_lang.startsWith("sql:");
let subtype = "Python";
let blockType: RecipeNodeData["blockType"] = "validator_python";
if (isOxc) {
subtype = "OXC";
blockType = "validator_oxc";
} else if (isSql) {
subtype = "SQL";
blockType = "validator_sql";
}
return {
title: "Check",
kind: "validator",
subtype,
blockType,
name: config.name,
layoutDirection,
};
}
if (config.kind === "markdown_note") {
return {
title: "Note",
kind: "note",
subtype: "Markdown",
blockType: "markdown_note",
name: config.name,
layoutDirection,
};
}
if (config.kind === "seed") {
const seedSourceType = config.seed_source_type ?? "hf";
const sourceLabel =
seedSourceType === "hf"
? "Hugging Face dataset"
: seedSourceType === "local"
? "CSV or JSON file"
: seedSourceType === "github_repo"
? "GitHub repositories"
: "Document file";
return {
title: "Source data",
kind: "seed",
subtype: sourceLabel,
blockType: "seed",
name: sourceLabel,
layoutDirection,
};
}
if (config.kind === "model_provider") {
return {
title: "Provider connection",
kind: "model_provider",
subtype: config.provider_type || "Connection",
blockType: "model_provider",
name: config.name,
layoutDirection,
};
}
if (config.kind === "model_config") {
return {
title: "Model preset",
kind: "model_config",
subtype: config.model || "Model",
blockType: "model_config",
name: config.name,
layoutDirection,
};
}
if (config.kind === "tool_config") {
const providerCount = config.mcp_providers.length;
return {
title: "Tool access",
kind: "tool_config",
subtype: providerCount === 1 ? "1 server" : `${providerCount} servers`,
blockType: "tool_config",
name: config.name,
layoutDirection,
};
}
return {
title: "AI step",
kind: "llm",
subtype: labelForLlm(config.llm_type),
blockType: config.llm_type,
name: config.name,
layoutDirection,
};
}