Skip to content

Commit 3813660

Browse files
committed
chore: add dewhale based helper
1 parent 72f744f commit 3813660

File tree

3 files changed

+44
-238
lines changed

3 files changed

+44
-238
lines changed

.dewhale/characters/Collector.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: ModelCatalogCollector
2+
labels:
3+
- "collect-model"
4+
- "hf2catalog"
5+
systemPrompt: |
6+
You are Neutree AI Model Catalog Collector, responsible for helping users convert HuggingFace model repositories into standardized model catalog configurations.
7+
8+
Your primary task is to collect HuggingFace repository URLs from users and generate corresponding model catalog YAML configurations using the hf2catalog tool.
9+
10+
A typical workflow is as follows:
11+
1. When a user provides a HuggingFace model repository URL (like https://huggingface.co/microsoft/DialoGPT-medium), immediately process it without asking for confirmation.
12+
2. Use the `hf2catalog` tool to convert the URL into a model catalog configuration, always using YAML output format.
13+
3. Present the results in a clean, formatted YAML code block for easy copy-paste usage.
14+
4. Be proactive and efficient - complete the entire process in one interaction when possible, avoiding unnecessary back-and-forth confirmations.
15+
16+
Key guidelines:
17+
- Always output results in YAML format (never ask users to specify output format)
18+
- Wrap the final catalog configuration in a ```yaml code block for better readability
19+
- Handle multiple URLs in a single request if provided
20+
- Provide brief context about what the generated catalog contains (model type, engine, etc.)
21+
- If a URL is invalid or the model is unsupported, explain the issue and suggest alternatives
22+
23+
Remember: Your goal is to make the model catalog generation process as smooth and efficient as possible for users.

.dewhale/config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
llm:
2+
provider: google
3+
model: gemini-2.0-flash
4+
maxTokens: 8192
5+
maxSteps: 3
6+
maxRetries: 5
7+
mcp:
8+
servers:
9+
- type: stdio
10+
command: deno
11+
args:
12+
- run
13+
- "-A"
14+
- jsr:@arcfra/neutree-mcp-servers/servers/hf2catalog

scripts/hf2catalog.ts

Lines changed: 7 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -9,233 +9,8 @@
99
*/
1010

1111
import { parse } from "https://deno.land/std@0.224.0/flags/mod.ts";
12-
import * as yaml from "https://deno.land/std@0.224.0/yaml/mod.ts";
12+
import { hf2catalog } from "jsr:@arcfra/neutree-mcp-servers@0.3.0/servers/hf2catalog/hf2catalog";
1313

14-
// -----------------------------
15-
// Types & Constants
16-
// -----------------------------
17-
interface SiblingFile {
18-
rfilename: string; // full relative filename in repo
19-
size: number;
20-
}
21-
22-
interface HFModelMeta {
23-
// subset of HF /api/models response we care about
24-
pipeline_tag?: string;
25-
siblings: SiblingFile[];
26-
id?: string; // full model name like "microsoft/DialoGPT-medium"
27-
author?: string; // author/organization name
28-
cardData?: {
29-
thumbnail?: string; // icon/thumbnail URL
30-
};
31-
}
32-
33-
const DEFAULT_SCHEDULER = {
34-
type: "consistent_hash",
35-
virtual_nodes: 150,
36-
load_factor: 1.25,
37-
};
38-
39-
// Supported model tasks
40-
const TEXT_GENERATION_TASK = "text-generation";
41-
const TEXT_EMBEDDING_TASK = "text-embedding";
42-
const TEXT_RERANK_TASK = "text-rerank";
43-
44-
const SUPPORTED_TASKS = [
45-
TEXT_GENERATION_TASK,
46-
TEXT_EMBEDDING_TASK,
47-
TEXT_RERANK_TASK,
48-
] as const;
49-
50-
// Mapping from HuggingFace pipeline tags to our supported tasks
51-
// Keep this mapping conservative - only include well-established mappings
52-
const HF_PIPELINE_TO_TASK_MAP: Record<string, string> = {
53-
// Text generation - only the most common ones
54-
"text-generation": TEXT_GENERATION_TASK,
55-
56-
// Text embedding - only exact matches and feature-extraction (widely used)
57-
"feature-extraction": TEXT_EMBEDDING_TASK,
58-
"text-embedding": TEXT_EMBEDDING_TASK,
59-
"sentence-similarity": TEXT_EMBEDDING_TASK,
60-
61-
// Text rerank - only exact match
62-
"text-rerank": TEXT_RERANK_TASK,
63-
};
64-
65-
// vLLM best-practice args (can be tuned globally here)
66-
const DEFAULT_VLLM_ARGS = {
67-
tensor_parallel_size: 1,
68-
max_model_len: 32768,
69-
enforce_eager: true,
70-
gpu_memory_utilization: 0.95,
71-
enable_chunked_prefill: true,
72-
};
73-
74-
// -----------------------------
75-
// Helpers
76-
// -----------------------------
77-
function fatal(msg: string): never {
78-
console.error(`Error: ${msg}`);
79-
Deno.exit(1);
80-
}
81-
82-
function mapHFPipelineToTask(pipelineTag?: string): string {
83-
if (!pipelineTag) {
84-
fatal("Model pipeline_tag is missing. Cannot determine task type.");
85-
}
86-
87-
const mappedTask = HF_PIPELINE_TO_TASK_MAP[pipelineTag];
88-
if (!mappedTask) {
89-
const supportedPipelines = Object.keys(HF_PIPELINE_TO_TASK_MAP).join(", ");
90-
fatal(
91-
`Unsupported pipeline tag: "${pipelineTag}". ` +
92-
`Supported pipeline tags: ${supportedPipelines}. ` +
93-
`Only tasks [${SUPPORTED_TASKS.join(", ")}] are supported.`
94-
);
95-
}
96-
97-
return mappedTask;
98-
}
99-
100-
function parseRepoUrl(url: string): { owner: string; repo: string } {
101-
try {
102-
const u = new URL(url.replace(/\/$/, ""));
103-
if (u.hostname !== "huggingface.co") {
104-
throw new Error("Not a huggingface.co URL");
105-
}
106-
const segments = u.pathname.replace(/^\/+/, "").split("/");
107-
if (segments.length < 2) throw new Error("URL missing owner or repo");
108-
return { owner: segments[0], repo: segments[1] };
109-
} catch (e: unknown) {
110-
fatal((e as Error).message);
111-
}
112-
}
113-
114-
async function fetchModelMeta(
115-
owner: string,
116-
repo: string
117-
): Promise<HFModelMeta> {
118-
const apiUrl = `https://huggingface.co/api/models/${owner}/${repo}`;
119-
const res = await fetch(apiUrl);
120-
if (!res.ok) fatal(`HF API request failed: ${res.status} ${res.statusText}`);
121-
return await res.json();
122-
}
123-
124-
function pickFile(siblings: SiblingFile[], ext: string): string | undefined {
125-
return siblings.find((s) => s.rfilename.endsWith(ext))?.rfilename;
126-
}
127-
128-
function selectPrimaryFile(meta: HFModelMeta): {
129-
engine: "vllm" | "llama-cpp";
130-
file: string;
131-
} {
132-
const ggufFile = pickFile(meta.siblings, ".gguf");
133-
if (ggufFile) {
134-
return { engine: "llama-cpp", file: ggufFile };
135-
}
136-
137-
// Prefer first shard of a safetensors split, else any safetensors
138-
const firstShard = meta.siblings.find(
139-
(s) => /\.safetensors$/.test(s.rfilename) && /-00001-of-/.test(s.rfilename)
140-
);
141-
if (firstShard) return { engine: "vllm", file: firstShard.rfilename };
142-
const anyST = pickFile(meta.siblings, ".safetensors");
143-
if (anyST) return { engine: "vllm", file: anyST };
144-
145-
fatal("No .gguf or .safetensors file found in repo");
146-
}
147-
148-
function slugifyName(repo: string): string {
149-
return repo
150-
.toLowerCase()
151-
.replace(/[^a-z0-9]+/g, "-")
152-
.replace(/(^-|-$)/g, "");
153-
}
154-
155-
function buildDisplayName(meta: HFModelMeta, repo: string): string {
156-
// Try to use the model ID as display name, fallback to repo name
157-
if (meta.id) {
158-
return meta.id;
159-
}
160-
return repo;
161-
}
162-
163-
function buildLabels(meta: HFModelMeta, owner: string, repo: string): Record<string, string> {
164-
const labels: Record<string, string> = {};
165-
166-
// Priority 1: Use thumbnail from cardData if available
167-
if (meta.cardData?.thumbnail) {
168-
labels.icon_url = meta.cardData.thumbnail;
169-
} else {
170-
// Priority 2: Use HuggingFace social thumbnail for the organization/user
171-
// This provides high-quality avatars for HF organizations
172-
labels.icon_url = `https://cdn-thumbnails.huggingface.co/social-thumbnails/${owner}.png`;
173-
}
174-
175-
// Add original HuggingFace repo URL for traceability
176-
labels.hf_repo_url = `https://huggingface.co/${owner}/${repo}`;
177-
178-
return labels;
179-
}
180-
181-
function buildCatalog(
182-
{ owner, repo }: { owner: string; repo: string },
183-
meta: HFModelMeta
184-
) {
185-
const { engine, file } = selectPrimaryFile(meta);
186-
const task = mapHFPipelineToTask(meta.pipeline_tag);
187-
188-
// metadata.name uses repo slug; workspace omitted
189-
const name = slugifyName(repo);
190-
const displayName = buildDisplayName(meta, repo);
191-
const labels = buildLabels(meta, owner, repo);
192-
193-
const catalog: Record<string, unknown> = {
194-
apiVersion: "v1",
195-
kind: "ModelCatalog",
196-
metadata: {
197-
name,
198-
display_name: displayName,
199-
// workspace intentionally left blank for UI to fill
200-
labels,
201-
},
202-
spec: {
203-
model: {
204-
registry: "",
205-
name: `${owner}/${repo}`,
206-
file,
207-
version: "latest",
208-
task: task,
209-
},
210-
engine: {
211-
engine,
212-
version: "v1",
213-
},
214-
resources: {},
215-
replicas: { num: 1 },
216-
deployment_options: {
217-
scheduler: DEFAULT_SCHEDULER,
218-
},
219-
variables: {
220-
RAY_SCHEDULER_TYPE: DEFAULT_SCHEDULER.type,
221-
...(engine === "vllm"
222-
? {
223-
engine_args: {
224-
...DEFAULT_VLLM_ARGS,
225-
served_model_name: `${owner}/${repo}`,
226-
},
227-
}
228-
: {}),
229-
},
230-
},
231-
};
232-
233-
return catalog;
234-
}
235-
236-
// -----------------------------
237-
// Main
238-
// -----------------------------
23914
async function main() {
24015
const {
24116
_: [repoUrl],
@@ -245,18 +20,12 @@ async function main() {
24520
alias: { j: "json" },
24621
});
24722

248-
if (!repoUrl || typeof repoUrl !== "string")
249-
fatal("Please provide a Hugging Face repo URL.");
250-
251-
const { owner, repo } = parseRepoUrl(repoUrl);
252-
const meta = await fetchModelMeta(owner, repo);
253-
const catalog = buildCatalog({ owner, repo }, meta);
254-
255-
if (jsonFlag) {
256-
console.log(JSON.stringify(catalog, null, 2));
257-
} else {
258-
console.log(yaml.stringify(catalog));
259-
}
23+
console.log(
24+
await hf2catalog({
25+
repoUrl: String(repoUrl),
26+
output: jsonFlag ? "json" : "yaml",
27+
})
28+
);
26029
}
26130

26231
if (import.meta.main) main();

0 commit comments

Comments
 (0)