@llmindset/hf-mcp 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/dataset-detail.d.ts +26 -0
- package/dist/dataset-detail.d.ts.map +1 -0
- package/dist/dataset-detail.js +157 -0
- package/dist/dataset-detail.js.map +1 -0
- package/dist/dataset-search.d.ts +62 -0
- package/dist/dataset-search.d.ts.map +1 -0
- package/dist/dataset-search.js +158 -0
- package/dist/dataset-search.js.map +1 -0
- package/dist/duplicate-space.d.ts +75 -0
- package/dist/duplicate-space.d.ts.map +1 -0
- package/dist/duplicate-space.js +189 -0
- package/dist/duplicate-space.js.map +1 -0
- package/dist/error-messages.d.ts +4 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/error-messages.js +30 -0
- package/dist/error-messages.js.map +1 -0
- package/dist/hf-api-call.d.ts +18 -0
- package/dist/hf-api-call.d.ts.map +1 -0
- package/dist/hf-api-call.js +105 -0
- package/dist/hf-api-call.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/model-detail.d.ts +26 -0
- package/dist/model-detail.d.ts.map +1 -0
- package/dist/model-detail.js +224 -0
- package/dist/model-detail.js.map +1 -0
- package/dist/model-search.d.ts +64 -0
- package/dist/model-search.d.ts.map +1 -0
- package/dist/model-search.js +161 -0
- package/dist/model-search.js.map +1 -0
- package/dist/paper-search.d.ts +58 -0
- package/dist/paper-search.d.ts.map +1 -0
- package/dist/paper-search.js +114 -0
- package/dist/paper-search.js.map +1 -0
- package/dist/paper-summary.d.ts +35 -0
- package/dist/paper-summary.d.ts.map +1 -0
- package/dist/paper-summary.js +187 -0
- package/dist/paper-summary.js.map +1 -0
- package/dist/space-files.d.ts +44 -0
- package/dist/space-files.d.ts.map +1 -0
- package/dist/space-files.js +242 -0
- package/dist/space-files.js.map +1 -0
- package/dist/space-info.d.ts +56 -0
- package/dist/space-info.d.ts.map +1 -0
- package/dist/space-info.js +135 -0
- package/dist/space-info.js.map +1 -0
- package/dist/space-search.d.ts +71 -0
- package/dist/space-search.d.ts.map +1 -0
- package/dist/space-search.js +95 -0
- package/dist/space-search.js.map +1 -0
- package/dist/tool-ids.d.ts +23 -0
- package/dist/tool-ids.d.ts.map +1 -0
- package/dist/tool-ids.js +55 -0
- package/dist/tool-ids.js.map +1 -0
- package/dist/user-summary.d.ts +56 -0
- package/dist/user-summary.d.ts.map +1 -0
- package/dist/user-summary.js +271 -0
- package/dist/user-summary.js.map +1 -0
- package/dist/utilities.d.ts +8 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +53 -0
- package/dist/utilities.js.map +1 -0
- package/eslint.config.js +43 -0
- package/package.json +47 -0
- package/src/dataset-detail.ts +257 -0
- package/src/dataset-search.ts +237 -0
- package/src/duplicate-space.ts +263 -0
- package/src/error-messages.ts +57 -0
- package/src/hf-api-call.ts +182 -0
- package/src/index.ts +18 -0
- package/src/model-detail.ts +359 -0
- package/src/model-search.ts +231 -0
- package/src/paper-search.ts +188 -0
- package/src/paper-summary.ts +303 -0
- package/src/space-files.ts +325 -0
- package/src/space-info.ts +190 -0
- package/src/space-search.ts +177 -0
- package/src/tool-ids.ts +84 -0
- package/src/user-summary.ts +421 -0
- package/src/utilities.ts +64 -0
- package/test/duplicate-space.spec.ts +41 -0
- package/test/fixtures/paper_result_kazakh.json +854 -0
- package/test/fixtures/space-result.json +263 -0
- package/test/paper-search.spec.ts +57 -0
- package/test/paper-summary.spec.ts +113 -0
- package/test/space-files.spec.ts +232 -0
- package/test/space-search.spec.ts +29 -0
- package/test/user-summary.spec.ts +131 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listFiles, spaceInfo } from '@huggingface/hub';
|
|
3
|
+
import { formatBytes, escapeMarkdown } from './utilities.js';
|
|
4
|
+
import { HfApiError } from './hf-api-call.js';
|
|
5
|
+
import { explain } from './error-messages.js';
|
|
6
|
+
|
|
7
|
+
// Define the FileWithUrl interface
|
|
8
|
+
export interface FileWithUrl {
|
|
9
|
+
path: string;
|
|
10
|
+
size: number;
|
|
11
|
+
type: 'file' | 'directory' | 'unknown';
|
|
12
|
+
url: string;
|
|
13
|
+
sizeFormatted: string;
|
|
14
|
+
lastModified?: string;
|
|
15
|
+
lfs: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// File type detection helpers
|
|
19
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
20
|
+
'.jpg',
|
|
21
|
+
'.jpeg',
|
|
22
|
+
'.png',
|
|
23
|
+
'.gif',
|
|
24
|
+
'.bmp',
|
|
25
|
+
'.tiff',
|
|
26
|
+
'.tif',
|
|
27
|
+
'.webp',
|
|
28
|
+
'.svg',
|
|
29
|
+
'.ico',
|
|
30
|
+
'.heic',
|
|
31
|
+
'.heif',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const AUDIO_EXTENSIONS = new Set([
|
|
35
|
+
'.mp3',
|
|
36
|
+
'.wav',
|
|
37
|
+
'.flac',
|
|
38
|
+
'.aac',
|
|
39
|
+
'.ogg',
|
|
40
|
+
'.m4a',
|
|
41
|
+
'.wma',
|
|
42
|
+
'.opus',
|
|
43
|
+
'.aiff',
|
|
44
|
+
'.au',
|
|
45
|
+
'.ra',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
function getFileExtension(path: string): string {
|
|
49
|
+
const lastDot = path.lastIndexOf('.');
|
|
50
|
+
return lastDot === -1 ? '' : path.substring(lastDot).toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isImageFile(path: string): boolean {
|
|
54
|
+
return IMAGE_EXTENSIONS.has(getFileExtension(path));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isAudioFile(path: string): boolean {
|
|
58
|
+
return AUDIO_EXTENSIONS.has(getFileExtension(path));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesFileType(file: FileWithUrl, fileType: 'all' | 'image' | 'audio'): boolean {
|
|
62
|
+
switch (fileType) {
|
|
63
|
+
case 'all':
|
|
64
|
+
return true;
|
|
65
|
+
case 'image':
|
|
66
|
+
return isImageFile(file.path);
|
|
67
|
+
case 'audio':
|
|
68
|
+
return isAudioFile(file.path);
|
|
69
|
+
default:
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Tool configuration
|
|
75
|
+
export const SPACE_FILES_TOOL_CONFIG = {
|
|
76
|
+
name: 'space_files',
|
|
77
|
+
description: '', // This will be dynamically set with username
|
|
78
|
+
schema: z.object({
|
|
79
|
+
spaceName: z.string().optional().describe('Space identifier in format "username/spacename"'),
|
|
80
|
+
fileType: z
|
|
81
|
+
.enum(['all', 'image', 'audio'])
|
|
82
|
+
.optional()
|
|
83
|
+
.default('all')
|
|
84
|
+
.describe('Filter files by type: all (default), image, or audio files only'),
|
|
85
|
+
}),
|
|
86
|
+
annotations: {
|
|
87
|
+
title: 'Space Files List',
|
|
88
|
+
destructiveHint: false,
|
|
89
|
+
readOnlyHint: true,
|
|
90
|
+
openWorldHint: true,
|
|
91
|
+
},
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
// Define parameter types
|
|
95
|
+
export type SpaceFilesParams = z.infer<typeof SPACE_FILES_TOOL_CONFIG.schema>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Service for listing files in Hugging Face Spaces
|
|
99
|
+
*/
|
|
100
|
+
export class SpaceFilesTool {
|
|
101
|
+
private readonly accessToken?: string;
|
|
102
|
+
private readonly username?: string;
|
|
103
|
+
|
|
104
|
+
constructor(hfToken?: string, username?: string) {
|
|
105
|
+
this.accessToken = hfToken;
|
|
106
|
+
this.username = username;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static createToolConfig(username?: string): typeof SPACE_FILES_TOOL_CONFIG {
|
|
110
|
+
const description = username
|
|
111
|
+
? `Defaults to ${username}/filedrop. List files in a static Hugging Face Space. Use the URL for specifying Files inputs to Gradio endpoints or downloading files`
|
|
112
|
+
: `List all files in a static Hugging Face Space. Use the URL for specifying Files inputs to Gradio endpoints or downloading files.`;
|
|
113
|
+
return {
|
|
114
|
+
...SPACE_FILES_TOOL_CONFIG,
|
|
115
|
+
description: description as '',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get all files in a space with their URLs
|
|
121
|
+
*/
|
|
122
|
+
async getSpaceFilesWithUrls(spaceName: string): Promise<FileWithUrl[]> {
|
|
123
|
+
try {
|
|
124
|
+
// Get space info to determine subdomain
|
|
125
|
+
const space = await spaceInfo({
|
|
126
|
+
name: spaceName,
|
|
127
|
+
additionalFields: ['subdomain'],
|
|
128
|
+
...(this.accessToken && { accessToken: this.accessToken }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Check if it's a static space
|
|
132
|
+
if (space.sdk !== 'static') {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Space "${spaceName}" is not a static space (found: ${space.sdk}). This tool only works with static spaces.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const files: FileWithUrl[] = [];
|
|
139
|
+
|
|
140
|
+
// List all files recursively
|
|
141
|
+
for await (const file of listFiles({
|
|
142
|
+
repo: { type: 'space', name: spaceName },
|
|
143
|
+
recursive: true,
|
|
144
|
+
expand: true, // Get last commit info
|
|
145
|
+
...(this.accessToken && { credentials: { accessToken: this.accessToken } }),
|
|
146
|
+
})) {
|
|
147
|
+
if (file.type === 'file') {
|
|
148
|
+
files.push({
|
|
149
|
+
path: file.path,
|
|
150
|
+
size: file.size,
|
|
151
|
+
type: file.type,
|
|
152
|
+
url: this.constructFileUrl(spaceName, file.path),
|
|
153
|
+
sizeFormatted: formatBytes(file.size),
|
|
154
|
+
lastModified: file.lastCommit?.date,
|
|
155
|
+
lfs: !!file.lfs,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof HfApiError) {
|
|
163
|
+
throw explain(error, `Failed to list files for space "${spaceName}"`);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Construct the URL for a file
|
|
171
|
+
*/
|
|
172
|
+
private constructFileUrl(spaceName: string, filePath: string): string {
|
|
173
|
+
return `https://huggingface.co/spaces/${spaceName}/resolve/main/${filePath}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate detailed markdown report with files grouped by directory
|
|
178
|
+
*/
|
|
179
|
+
async generateDetailedMarkdown(spaceName: string, fileType: 'all' | 'image' | 'audio' = 'all'): Promise<string> {
|
|
180
|
+
const allFiles = await this.getSpaceFilesWithUrls(spaceName);
|
|
181
|
+
const files = allFiles.filter((file) => matchesFileType(file, fileType));
|
|
182
|
+
|
|
183
|
+
let markdown = `# Files in Space: ${spaceName}\n\n`;
|
|
184
|
+
if (fileType !== 'all') {
|
|
185
|
+
markdown += `**Filter**: ${fileType} files only\n`;
|
|
186
|
+
}
|
|
187
|
+
markdown += `**Total Files**: ${files.length}\n`;
|
|
188
|
+
markdown += `**Total Size**: ${formatBytes(files.reduce((sum, f) => sum + f.size, 0))}\n\n`;
|
|
189
|
+
|
|
190
|
+
// Handle empty results
|
|
191
|
+
if (files.length === 0) {
|
|
192
|
+
if (fileType !== 'all') {
|
|
193
|
+
markdown += `No ${fileType} files found in this space.\n`;
|
|
194
|
+
} else {
|
|
195
|
+
markdown += `No files found in this space.\n`;
|
|
196
|
+
}
|
|
197
|
+
return markdown;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Group files by directory
|
|
201
|
+
const byDirectory = files.reduce(
|
|
202
|
+
(acc, file) => {
|
|
203
|
+
const dir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/')) : '/';
|
|
204
|
+
if (!acc[dir]) acc[dir] = [];
|
|
205
|
+
acc[dir].push(file);
|
|
206
|
+
return acc;
|
|
207
|
+
},
|
|
208
|
+
{} as Record<string, FileWithUrl[]>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Generate table
|
|
212
|
+
markdown += `## All Files\n\n`;
|
|
213
|
+
markdown += `| File Path | Size | Type | Last Modified | URL |\n`;
|
|
214
|
+
markdown += `|-----------|------|------|---------------|-----|\n`;
|
|
215
|
+
|
|
216
|
+
// Sort directories and output files
|
|
217
|
+
const sortedDirs = Object.keys(byDirectory).sort();
|
|
218
|
+
for (const dir of sortedDirs) {
|
|
219
|
+
const dirFiles = byDirectory[dir];
|
|
220
|
+
if (!dirFiles) continue;
|
|
221
|
+
|
|
222
|
+
if (dir !== '/' && dirFiles.length > 0) {
|
|
223
|
+
markdown += `| **📁 ${escapeMarkdown(dir)}/** | | | | |\n`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const file of dirFiles) {
|
|
227
|
+
const fileName = file.path.split('/').pop() || file.path;
|
|
228
|
+
const indent = dir === '/' ? '' : ' ';
|
|
229
|
+
const icon = this.getFileIcon(fileName);
|
|
230
|
+
const lastMod = file.lastModified ? new Date(file.lastModified).toLocaleDateString() : '-';
|
|
231
|
+
|
|
232
|
+
markdown += `| ${indent}${icon} ${escapeMarkdown(fileName)} | ${file.sizeFormatted} | ${file.lfs ? 'LFS' : 'Regular'} | ${lastMod} | ${file.url} |\n`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add direct access examples
|
|
237
|
+
markdown += `\n## Direct Access Examples\n\n`;
|
|
238
|
+
markdown += `\`\`\`bash\n`;
|
|
239
|
+
|
|
240
|
+
// Show a few example URLs
|
|
241
|
+
const examples = files.slice(0, 2);
|
|
242
|
+
for (const file of examples) {
|
|
243
|
+
markdown += `# Download ${file.path}\n`;
|
|
244
|
+
markdown += `curl -L -O ${file.url}\n\n`;
|
|
245
|
+
}
|
|
246
|
+
markdown += `\`\`\`\n`;
|
|
247
|
+
markdown += '## Use the URL when specifying Files inputs for Gradio endpoints.\n\n';
|
|
248
|
+
markdown += 'This space is accessible via `git` with `git clone https://huggingface.co/spaces/' + spaceName + '`\n';
|
|
249
|
+
return markdown;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Generate simple markdown table without grouping
|
|
254
|
+
*/
|
|
255
|
+
async generateSimpleMarkdown(spaceName: string, fileType: 'all' | 'image' | 'audio' = 'all'): Promise<string> {
|
|
256
|
+
const allFiles = await this.getSpaceFilesWithUrls(spaceName);
|
|
257
|
+
const files = allFiles.filter((file) => matchesFileType(file, fileType));
|
|
258
|
+
|
|
259
|
+
let markdown = `# Files in ${spaceName}\n\n`;
|
|
260
|
+
if (fileType !== 'all') {
|
|
261
|
+
markdown += `**Filter**: ${fileType} files only\n\n`;
|
|
262
|
+
}
|
|
263
|
+
markdown += `| File Name | Path | Size | URL |\n`;
|
|
264
|
+
markdown += `|-----------|------|------|-----|\n`;
|
|
265
|
+
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
const fileName = file.path.split('/').pop() || file.path;
|
|
268
|
+
const icon = this.getFileIcon(fileName);
|
|
269
|
+
markdown += `| ${icon} ${escapeMarkdown(fileName)} | ${escapeMarkdown(file.path)} | ${file.sizeFormatted} | [Link](${file.url}) |\n`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return markdown;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* List files with the specified format
|
|
277
|
+
*/
|
|
278
|
+
async listFiles(params: SpaceFilesParams): Promise<string> {
|
|
279
|
+
const { fileType = 'all' } = params;
|
|
280
|
+
|
|
281
|
+
// Use provided spaceName or default to username/filedrop
|
|
282
|
+
const spaceName = params.spaceName || (this.username ? `${this.username}/filedrop` : 'filedrop');
|
|
283
|
+
|
|
284
|
+
return this.generateDetailedMarkdown(spaceName, fileType);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get file icon based on extension
|
|
289
|
+
*/
|
|
290
|
+
private getFileIcon(filename: string): string {
|
|
291
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
292
|
+
const iconMap: Record<string, string> = {
|
|
293
|
+
py: '🐍',
|
|
294
|
+
js: '📜',
|
|
295
|
+
ts: '📘',
|
|
296
|
+
md: '📝',
|
|
297
|
+
txt: '📄',
|
|
298
|
+
json: '📊',
|
|
299
|
+
yaml: '⚙️',
|
|
300
|
+
yml: '⚙️',
|
|
301
|
+
png: '🖼️',
|
|
302
|
+
jpg: '🖼️',
|
|
303
|
+
jpeg: '🖼️',
|
|
304
|
+
gif: '🖼️',
|
|
305
|
+
svg: '🎨',
|
|
306
|
+
mp4: '🎬',
|
|
307
|
+
mp3: '🎵',
|
|
308
|
+
pdf: '📕',
|
|
309
|
+
zip: '📦',
|
|
310
|
+
tar: '📦',
|
|
311
|
+
gz: '📦',
|
|
312
|
+
html: '🌐',
|
|
313
|
+
css: '🎨',
|
|
314
|
+
ipynb: '📓',
|
|
315
|
+
csv: '📊',
|
|
316
|
+
parquet: '🗄️',
|
|
317
|
+
safetensors: '🤖',
|
|
318
|
+
bin: '💾',
|
|
319
|
+
pkl: '🥒',
|
|
320
|
+
h5: '🗃️',
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return iconMap[ext || ''] || '📄';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HfApiCall } from './hf-api-call.js';
|
|
3
|
+
import { listSpaces } from '@huggingface/hub';
|
|
4
|
+
import type { SpaceEntry, SpaceRuntime, SpaceSdk } from '@huggingface/hub';
|
|
5
|
+
import { formatDate, formatNumber, escapeMarkdown, NO_TOKEN_INSTRUCTIONS } from './utilities.js';
|
|
6
|
+
|
|
7
|
+
interface SpaceReport {
|
|
8
|
+
user: string;
|
|
9
|
+
generatedAt: Date;
|
|
10
|
+
totalSpaces: number;
|
|
11
|
+
publicSpaces: number;
|
|
12
|
+
privateSpaces: number;
|
|
13
|
+
runningSpaces: number;
|
|
14
|
+
totalLikes: number;
|
|
15
|
+
sdkCounts: Record<string, number>;
|
|
16
|
+
spaces: SpaceDetails[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SpaceDetails {
|
|
20
|
+
name: string;
|
|
21
|
+
url: string;
|
|
22
|
+
sdk: SpaceSdk | 'unknown'; // Allow 'unknown' in our internal type
|
|
23
|
+
status: SpaceRuntime['stage'] | 'UNKNOWN';
|
|
24
|
+
statusEmoji: string;
|
|
25
|
+
hardware: string;
|
|
26
|
+
storage: string;
|
|
27
|
+
visibility: string;
|
|
28
|
+
likes: number;
|
|
29
|
+
lastModified: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STATUS_EMOJI: Record<string, string> = {
|
|
33
|
+
RUNNING: '🟢 Running',
|
|
34
|
+
RUNNING_BUILDING: '🔄 Building',
|
|
35
|
+
SLEEPING: '😴 Sleeping',
|
|
36
|
+
PAUSED: '⏸️ Paused',
|
|
37
|
+
STOPPED: '⏹️ Stopped',
|
|
38
|
+
RUNTIME_ERROR: '❌ Error',
|
|
39
|
+
BUILD_ERROR: '🚫 Build Error',
|
|
40
|
+
BUILDING: '🔨 Building',
|
|
41
|
+
NO_APP_FILE: '📄 No App',
|
|
42
|
+
CONFIG_ERROR: '⚙️ Config Error',
|
|
43
|
+
DELETING: '🗑️ Deleting',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface SpaceInfoParams {
|
|
47
|
+
username?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const SPACE_INFO_TOOL_CONFIG = {
|
|
51
|
+
name: 'space_info',
|
|
52
|
+
description: '', // This will be dynamically set based on auth
|
|
53
|
+
schema: z.object({
|
|
54
|
+
username: z.string().optional().describe('Username to get spaces for (defaults to authenticated user)'),
|
|
55
|
+
}),
|
|
56
|
+
annotations: {
|
|
57
|
+
title: 'Hugging Face Spaces Information',
|
|
58
|
+
destructiveHint: false,
|
|
59
|
+
readOnlyHint: true,
|
|
60
|
+
openWorldHint: false,
|
|
61
|
+
},
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export class SpaceInfoTool extends HfApiCall<SpaceInfoParams, SpaceReport> {
|
|
65
|
+
private authenticatedUsername?: string;
|
|
66
|
+
|
|
67
|
+
constructor(hfToken?: string, authenticatedUsername?: string) {
|
|
68
|
+
super('https://huggingface.co/api', hfToken);
|
|
69
|
+
this.authenticatedUsername = authenticatedUsername;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static createToolConfig(username?: string): typeof SPACE_INFO_TOOL_CONFIG {
|
|
73
|
+
const description = username
|
|
74
|
+
? `Tabluate Hugging Face Spaces information for ${username}, or another User.`
|
|
75
|
+
: `Tabluate public Hugging Face Spaces for a specific User. Supply a Hugging Face token or go to https://hf.co/join to create an account to view your own private Spaces.`;
|
|
76
|
+
return {
|
|
77
|
+
...SPACE_INFO_TOOL_CONFIG,
|
|
78
|
+
description: description as '',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getSpacesReport(targetUsername?: string): Promise<string> {
|
|
83
|
+
// TODO -- think a bit more about this exact condition
|
|
84
|
+
if (!this.authenticatedUsername && !targetUsername) throw new Error(NO_TOKEN_INSTRUCTIONS);
|
|
85
|
+
|
|
86
|
+
const username = targetUsername || this.authenticatedUsername || 'error';
|
|
87
|
+
const spaces: SpaceEntry[] = [];
|
|
88
|
+
const spacesWithRuntime: SpaceDetails[] = [];
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Fetch all spaces for the user with runtime info in a single request
|
|
92
|
+
for await (const space of listSpaces({
|
|
93
|
+
search: { owner: username },
|
|
94
|
+
additionalFields: ['runtime', 'subdomain'] as const,
|
|
95
|
+
accessToken: this.hfToken,
|
|
96
|
+
})) {
|
|
97
|
+
spaces.push(space);
|
|
98
|
+
|
|
99
|
+
// Use runtime data directly from listSpaces response
|
|
100
|
+
const runtime = space.runtime;
|
|
101
|
+
const hardware = runtime?.hardware?.current || 'cpu-basic';
|
|
102
|
+
|
|
103
|
+
spacesWithRuntime.push({
|
|
104
|
+
name: space.name.split('/')[1] || space.name,
|
|
105
|
+
url: `https://huggingface.co/spaces/${space.name}`,
|
|
106
|
+
sdk: space.sdk || 'unknown',
|
|
107
|
+
status: runtime?.stage || 'UNKNOWN',
|
|
108
|
+
statusEmoji: STATUS_EMOJI[runtime?.stage || 'STOPPED'] || '❓ Unknown',
|
|
109
|
+
hardware: hardware,
|
|
110
|
+
storage: runtime?.resources?.requests?.ephemeral || 'None',
|
|
111
|
+
visibility: space.private ? '🔒 Private' : '🌍 Public',
|
|
112
|
+
likes: space.likes,
|
|
113
|
+
lastModified: space.updatedAt ? formatDate(space.updatedAt) : 'Unknown',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate statistics
|
|
118
|
+
const report: SpaceReport = {
|
|
119
|
+
user: username,
|
|
120
|
+
generatedAt: new Date(),
|
|
121
|
+
totalSpaces: spaces.length,
|
|
122
|
+
publicSpaces: spaces.filter((s) => !s.private).length,
|
|
123
|
+
privateSpaces: spaces.filter((s) => s.private).length,
|
|
124
|
+
runningSpaces: spacesWithRuntime.filter((s) => s.status === 'RUNNING').length,
|
|
125
|
+
totalLikes: spaces.reduce((sum, s) => sum + s.likes, 0),
|
|
126
|
+
sdkCounts: spaces.reduce(
|
|
127
|
+
(acc, s) => {
|
|
128
|
+
const sdk = s.sdk || 'unknown';
|
|
129
|
+
acc[sdk] = (acc[sdk] || 0) + 1;
|
|
130
|
+
return acc;
|
|
131
|
+
},
|
|
132
|
+
{} as Record<string, number>
|
|
133
|
+
),
|
|
134
|
+
spaces: spacesWithRuntime.sort((a, b) => b.lastModified.localeCompare(a.lastModified)),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Generate markdown report
|
|
138
|
+
return this.generateMarkdown(report);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
throw new Error(`Failed to get spaces information: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private generateMarkdown(report: SpaceReport): string {
|
|
148
|
+
// Handle case where user has no spaces
|
|
149
|
+
if (report.totalSpaces === 0) {
|
|
150
|
+
return `# Hugging Face Spaces Report
|
|
151
|
+
**User**: ${report.user}
|
|
152
|
+
**Generated**: ${formatDate(report.generatedAt)}
|
|
153
|
+
|
|
154
|
+
No spaces were found for user '${report.user}'.
|
|
155
|
+
|
|
156
|
+
You can create your first Space at [https://huggingface.co/new-space](https://huggingface.co/new-space).`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sdkSummary = Object.entries(report.sdkCounts)
|
|
160
|
+
.filter(([_, count]) => count > 0)
|
|
161
|
+
.map(([sdk, count]) => `${sdk} (${count})`)
|
|
162
|
+
.join(', ');
|
|
163
|
+
|
|
164
|
+
let markdown = `# Hugging Face Spaces Report
|
|
165
|
+
**User**: ${report.user}
|
|
166
|
+
**Generated**: ${formatDate(report.generatedAt)}
|
|
167
|
+
|
|
168
|
+
## Summary
|
|
169
|
+
- **Total Spaces**: ${report.totalSpaces}
|
|
170
|
+
- **Public**: ${report.publicSpaces}${report.privateSpaces > 0 ? ` | **Private**: ${report.privateSpaces}` : ''}
|
|
171
|
+
- **Currently Running**: ${report.runningSpaces}
|
|
172
|
+
- **Total Likes**: ${formatNumber(report.totalLikes)}
|
|
173
|
+
- **SDKs**: ${sdkSummary || 'None'}
|
|
174
|
+
|
|
175
|
+
## All Spaces
|
|
176
|
+
| Space | SDK | Status | Hardware | Storage | Visibility | Likes ❤️ | Last Modified |
|
|
177
|
+
|-------|-----|--------|----------|---------|------------|----------|--------------|\n`;
|
|
178
|
+
|
|
179
|
+
// Add each space as a table row
|
|
180
|
+
for (const space of report.spaces) {
|
|
181
|
+
markdown += `| [${escapeMarkdown(space.name)}](${space.url}) | ${escapeMarkdown(space.sdk)} | ${space.statusEmoji} | ${escapeMarkdown(space.hardware)} | ${escapeMarkdown(space.storage)} | ${space.visibility} | ${space.likes} | ${space.lastModified} |\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return markdown;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const formatSpaceInfoResult = async (tool: SpaceInfoTool, params: SpaceInfoParams): Promise<string> => {
|
|
189
|
+
return tool.getSpacesReport(params.username);
|
|
190
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HfApiCall } from './hf-api-call.js';
|
|
3
|
+
import { escapeMarkdown } from './utilities.js';
|
|
4
|
+
|
|
5
|
+
// Define the SearchResult interface
|
|
6
|
+
export interface SpaceSearchResult {
|
|
7
|
+
id: string;
|
|
8
|
+
emoji?: string; // Emoji for the space
|
|
9
|
+
likes?: number;
|
|
10
|
+
title?: string;
|
|
11
|
+
author: string;
|
|
12
|
+
runtime: {
|
|
13
|
+
stage?: string; // always seems to be "RUNNING"
|
|
14
|
+
};
|
|
15
|
+
ai_category?: string;
|
|
16
|
+
ai_short_description?: string;
|
|
17
|
+
shortDescription?: string;
|
|
18
|
+
semanticRelevancyScore?: number; // Score from semantic search API
|
|
19
|
+
trendingScore?: number;
|
|
20
|
+
lastModified?: Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Define input types for space search
|
|
24
|
+
interface SpaceSearchParams {
|
|
25
|
+
q: string;
|
|
26
|
+
sdk: string;
|
|
27
|
+
filter?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Default number of results to return
|
|
31
|
+
const RESULTS_TO_RETURN = 10;
|
|
32
|
+
|
|
33
|
+
export const SEMANTIC_SEARCH_TOOL_CONFIG = {
|
|
34
|
+
name: 'space_search',
|
|
35
|
+
description:
|
|
36
|
+
'Find Hugging Face Spaces using semantic search. ' + 'Include links to the Space when presenting the results.',
|
|
37
|
+
schema: z.object({
|
|
38
|
+
query: z.string().min(1, 'Query is required').max(100, 'Query too long').describe('Semantic Search Query'),
|
|
39
|
+
limit: z.number().optional().default(RESULTS_TO_RETURN).describe('Number of results to return'),
|
|
40
|
+
mcp: z.boolean().optional().default(false).describe('Only return MCP Server enabled Spaces'),
|
|
41
|
+
}),
|
|
42
|
+
annotations: {
|
|
43
|
+
title: 'Hugging Face Space Search',
|
|
44
|
+
destructiveHint: false,
|
|
45
|
+
readOnlyHint: true,
|
|
46
|
+
openWorldHint: true,
|
|
47
|
+
},
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service for searching Hugging Face Spaces semantically
|
|
52
|
+
*/
|
|
53
|
+
export class SpaceSearchTool extends HfApiCall<SpaceSearchParams, SpaceSearchResult[]> {
|
|
54
|
+
/**
|
|
55
|
+
* Creates a new semantic search service
|
|
56
|
+
* @param apiUrl The URL of the Hugging Face semantic search API
|
|
57
|
+
* @param hfToken Optional Hugging Face token for API access
|
|
58
|
+
*/
|
|
59
|
+
constructor(hfToken?: string, apiUrl = 'https://huggingface.co/api/spaces/semantic-search') {
|
|
60
|
+
super(apiUrl, hfToken);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Performs a semantic search on Hugging Face Spaces
|
|
65
|
+
* @param query The search query
|
|
66
|
+
* @param limit Maximum number of results to return
|
|
67
|
+
* @returns An array of search results
|
|
68
|
+
*/
|
|
69
|
+
async search(
|
|
70
|
+
query: string,
|
|
71
|
+
limit: number = RESULTS_TO_RETURN,
|
|
72
|
+
mcp: boolean = false
|
|
73
|
+
): Promise<{ results: SpaceSearchResult[]; totalCount: number }> {
|
|
74
|
+
try {
|
|
75
|
+
// Validate input before making API call
|
|
76
|
+
if (!query) {
|
|
77
|
+
return { results: [], totalCount: 0 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Prepare API parameters, adding the filter if mcp is true
|
|
81
|
+
const params: SpaceSearchParams = { q: query, sdk: 'gradio' };
|
|
82
|
+
|
|
83
|
+
if (mcp) {
|
|
84
|
+
params.filter = 'mcp-server';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const results = await this.callApi<SpaceSearchResult[]>(params);
|
|
88
|
+
|
|
89
|
+
return { results: results.slice(0, limit), totalCount: results.length };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof Error) {
|
|
92
|
+
throw new Error(`Failed to search for spaces: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Search for spaces with a specific filter (e.g., arxiv:XXXX.XXXXX)
|
|
100
|
+
* Note: For spaces, we need to use the regular API endpoint with filter parameter
|
|
101
|
+
*/
|
|
102
|
+
async searchWithFilter(filter: string, limit: number = 10, headerLevel: number = 1): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
// For spaces, we need to use the regular spaces API endpoint with filter
|
|
105
|
+
const url = new URL('https://huggingface.co/api/spaces');
|
|
106
|
+
url.searchParams.append('filter', filter);
|
|
107
|
+
url.searchParams.append('limit', limit.toString());
|
|
108
|
+
url.searchParams.append('sort', 'likes');
|
|
109
|
+
url.searchParams.append('direction', '-1');
|
|
110
|
+
|
|
111
|
+
const results = await this.fetchFromApi<SpaceSearchResult[]>(url);
|
|
112
|
+
|
|
113
|
+
if (results.length === 0) {
|
|
114
|
+
return `No matching Hugging Face Spaces found referencing ${filter}.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Format results using the existing formatter
|
|
118
|
+
return formatSearchResults(filter, results, results.length, headerLevel);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
throw new Error(`Failed to search for spaces: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create a schema validator for search parameters
|
|
129
|
+
export const SearchParamsSchema = SEMANTIC_SEARCH_TOOL_CONFIG.schema;
|
|
130
|
+
|
|
131
|
+
export type SearchParams = z.infer<typeof SEMANTIC_SEARCH_TOOL_CONFIG.schema>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Formats search results as a markdown table for MCP friendly output
|
|
135
|
+
* @param results The search results to format
|
|
136
|
+
* @returns A markdown formatted string with the search results
|
|
137
|
+
*/
|
|
138
|
+
export const formatSearchResults = (
|
|
139
|
+
query: string,
|
|
140
|
+
results: SpaceSearchResult[],
|
|
141
|
+
totalCount: number,
|
|
142
|
+
headerLevel: number = 1
|
|
143
|
+
): string => {
|
|
144
|
+
if (results.length === 0) {
|
|
145
|
+
return `No matching Hugging Face Spaces found for the query '${query}'. Try a different query.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const showingText =
|
|
149
|
+
results.length < totalCount
|
|
150
|
+
? `Showing ${results.length.toString()} of ${totalCount.toString()} results`
|
|
151
|
+
: `All ${results.length.toString()} results`;
|
|
152
|
+
const headerPrefix = '#'.repeat(headerLevel);
|
|
153
|
+
let markdown = `${headerPrefix} Space Search Results for the query '${query}' (${showingText})\n\n`;
|
|
154
|
+
markdown += '| Space | Description | Author | ID | Category | Likes | Trending Score | Relevance |\n';
|
|
155
|
+
markdown += '|-------|-------------|--------|----|----------|--------|----------------|-----------|\n';
|
|
156
|
+
|
|
157
|
+
for (const result of results) {
|
|
158
|
+
const title = result.title || 'Untitled';
|
|
159
|
+
const description = result.shortDescription || result.ai_short_description || 'No description';
|
|
160
|
+
const author = result.author || 'Unknown';
|
|
161
|
+
const id = result.id || '';
|
|
162
|
+
const emoji = result.emoji ? escapeMarkdown(result.emoji) + ' ' : '';
|
|
163
|
+
const relevance = result.semanticRelevancyScore ? (result.semanticRelevancyScore * 100).toFixed(1) + '%' : 'N/A';
|
|
164
|
+
|
|
165
|
+
markdown +=
|
|
166
|
+
`| ${emoji}[${escapeMarkdown(title)}](https://hf.co/spaces/${id}) ` +
|
|
167
|
+
`| ${escapeMarkdown(description)} ` +
|
|
168
|
+
`| ${escapeMarkdown(author)} ` +
|
|
169
|
+
`| \`${escapeMarkdown(id)}\` ` +
|
|
170
|
+
`| \`${escapeMarkdown(result.ai_category ?? '-')}\` ` +
|
|
171
|
+
`| ${escapeMarkdown(result.likes?.toString() ?? '-')} ` +
|
|
172
|
+
`| ${escapeMarkdown(result.trendingScore?.toString() ?? '-')} ` +
|
|
173
|
+
`| ${relevance} |\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return markdown;
|
|
177
|
+
};
|