@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/dist/dataset-detail.d.ts +26 -0
  3. package/dist/dataset-detail.d.ts.map +1 -0
  4. package/dist/dataset-detail.js +157 -0
  5. package/dist/dataset-detail.js.map +1 -0
  6. package/dist/dataset-search.d.ts +62 -0
  7. package/dist/dataset-search.d.ts.map +1 -0
  8. package/dist/dataset-search.js +158 -0
  9. package/dist/dataset-search.js.map +1 -0
  10. package/dist/duplicate-space.d.ts +75 -0
  11. package/dist/duplicate-space.d.ts.map +1 -0
  12. package/dist/duplicate-space.js +189 -0
  13. package/dist/duplicate-space.js.map +1 -0
  14. package/dist/error-messages.d.ts +4 -0
  15. package/dist/error-messages.d.ts.map +1 -0
  16. package/dist/error-messages.js +30 -0
  17. package/dist/error-messages.js.map +1 -0
  18. package/dist/hf-api-call.d.ts +18 -0
  19. package/dist/hf-api-call.d.ts.map +1 -0
  20. package/dist/hf-api-call.js +105 -0
  21. package/dist/hf-api-call.js.map +1 -0
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/model-detail.d.ts +26 -0
  27. package/dist/model-detail.d.ts.map +1 -0
  28. package/dist/model-detail.js +224 -0
  29. package/dist/model-detail.js.map +1 -0
  30. package/dist/model-search.d.ts +64 -0
  31. package/dist/model-search.d.ts.map +1 -0
  32. package/dist/model-search.js +161 -0
  33. package/dist/model-search.js.map +1 -0
  34. package/dist/paper-search.d.ts +58 -0
  35. package/dist/paper-search.d.ts.map +1 -0
  36. package/dist/paper-search.js +114 -0
  37. package/dist/paper-search.js.map +1 -0
  38. package/dist/paper-summary.d.ts +35 -0
  39. package/dist/paper-summary.d.ts.map +1 -0
  40. package/dist/paper-summary.js +187 -0
  41. package/dist/paper-summary.js.map +1 -0
  42. package/dist/space-files.d.ts +44 -0
  43. package/dist/space-files.d.ts.map +1 -0
  44. package/dist/space-files.js +242 -0
  45. package/dist/space-files.js.map +1 -0
  46. package/dist/space-info.d.ts +56 -0
  47. package/dist/space-info.d.ts.map +1 -0
  48. package/dist/space-info.js +135 -0
  49. package/dist/space-info.js.map +1 -0
  50. package/dist/space-search.d.ts +71 -0
  51. package/dist/space-search.d.ts.map +1 -0
  52. package/dist/space-search.js +95 -0
  53. package/dist/space-search.js.map +1 -0
  54. package/dist/tool-ids.d.ts +23 -0
  55. package/dist/tool-ids.d.ts.map +1 -0
  56. package/dist/tool-ids.js +55 -0
  57. package/dist/tool-ids.js.map +1 -0
  58. package/dist/user-summary.d.ts +56 -0
  59. package/dist/user-summary.d.ts.map +1 -0
  60. package/dist/user-summary.js +271 -0
  61. package/dist/user-summary.js.map +1 -0
  62. package/dist/utilities.d.ts +8 -0
  63. package/dist/utilities.d.ts.map +1 -0
  64. package/dist/utilities.js +53 -0
  65. package/dist/utilities.js.map +1 -0
  66. package/eslint.config.js +43 -0
  67. package/package.json +47 -0
  68. package/src/dataset-detail.ts +257 -0
  69. package/src/dataset-search.ts +237 -0
  70. package/src/duplicate-space.ts +263 -0
  71. package/src/error-messages.ts +57 -0
  72. package/src/hf-api-call.ts +182 -0
  73. package/src/index.ts +18 -0
  74. package/src/model-detail.ts +359 -0
  75. package/src/model-search.ts +231 -0
  76. package/src/paper-search.ts +188 -0
  77. package/src/paper-summary.ts +303 -0
  78. package/src/space-files.ts +325 -0
  79. package/src/space-info.ts +190 -0
  80. package/src/space-search.ts +177 -0
  81. package/src/tool-ids.ts +84 -0
  82. package/src/user-summary.ts +421 -0
  83. package/src/utilities.ts +64 -0
  84. package/test/duplicate-space.spec.ts +41 -0
  85. package/test/fixtures/paper_result_kazakh.json +854 -0
  86. package/test/fixtures/space-result.json +263 -0
  87. package/test/paper-search.spec.ts +57 -0
  88. package/test/paper-summary.spec.ts +113 -0
  89. package/test/space-files.spec.ts +232 -0
  90. package/test/space-search.spec.ts +29 -0
  91. package/test/user-summary.spec.ts +131 -0
  92. package/tsconfig.json +31 -0
  93. package/vitest.config.ts +11 -0
@@ -0,0 +1,188 @@
1
+ import { z } from 'zod';
2
+ import { HfApiCall } from './hf-api-call.js';
3
+ import { formatUnknownDate } from './utilities.js';
4
+
5
+ // https://github.com/huggingface/huggingface_hub/blob/a26b93e8ba0b51ce76ce5c2044896587c47c6b60/src/huggingface_hub/hf_api.py#L1481-L1542
6
+ // Raw JSON response for https://hf.co/api/papers/search?q=llama%203%20herd Llama Herd is ~50,000 tokens
7
+ // Raw JSON response for https://hf.co/api/papers/search?q=kazakh -> ~ 9 papers,
8
+ // Return papers as delimited markdown (or simplified JSON)
9
+ // ---
10
+ //
11
+ // can we link to Collections, Datasets, Models, Spaces?
12
+ // Create a schema validator for search parameters
13
+
14
+ // 80 papers in full mode is ~ 35,000 tokens
15
+ // 105 papers in summary mode is ~ 23094 tokens
16
+ // 105 papers in full mode is ~ 45797 tokens
17
+
18
+ export const DEFAULT_AUTHORS_TO_SHOW = 8;
19
+ const RESULTS_TO_RETURN = 10;
20
+
21
+ export const PAPER_SEARCH_TOOL_CONFIG = {
22
+ name: 'paper_search',
23
+ description:
24
+ 'Find Machine Learning research papers on the Hugging Face hub. ' +
25
+ "Include 'Link to paper' When presenting the results. " +
26
+ 'Consider whether tabulating results matches user intent.',
27
+ schema: z.object({
28
+ query: z
29
+ .string()
30
+ .min(3, 'Supply at least one search term')
31
+ .max(200, 'Query too long')
32
+ .describe('Semantic Search query'),
33
+ results_limit: z.number().optional().default(12).describe('Number of results to return'),
34
+ concise_only: z
35
+ .boolean()
36
+ .optional()
37
+ .default(false)
38
+ .describe(
39
+ 'Return a 2 sentence summary of the abstract. Use for broad search terms which may return a lot of results. Check with User if unsure.'
40
+ ),
41
+ }),
42
+ annotations: {
43
+ title: 'Paper Search',
44
+ destructiveHint: false,
45
+ readOnlyHint: true,
46
+ openWorldHint: true,
47
+ },
48
+ } as const;
49
+
50
+ export interface Author {
51
+ name?: string;
52
+ user?: {
53
+ user: string;
54
+ };
55
+ }
56
+
57
+ interface Paper {
58
+ id: string;
59
+ authors?: Author[];
60
+ publishedAt?: string;
61
+ title?: string;
62
+ summary?: string;
63
+ upvotes?: number;
64
+ ai_keywords?: string[];
65
+ ai_summary?: string;
66
+ }
67
+
68
+ export interface PaperSearchResult {
69
+ paper: Paper;
70
+ numComments?: number;
71
+ isAuthorParticipating?: boolean;
72
+ }
73
+
74
+ // Define input types for paper search
75
+ interface PaperSearchParams {
76
+ q: string;
77
+ }
78
+
79
+ /**
80
+ * Service for searching Hugging Face Papers
81
+ */
82
+ export class PaperSearchTool extends HfApiCall<PaperSearchParams, PaperSearchResult[]> {
83
+ /**
84
+ * Creates a new papers search service
85
+ * @param apiUrl The URL of the Hugging Face papers search API
86
+ * @param hfToken Optional Hugging Face token for API access
87
+ */
88
+ constructor(hfToken?: string, apiUrl = 'https://huggingface.co/api/papers/search') {
89
+ super(apiUrl, hfToken);
90
+ }
91
+
92
+ /**
93
+ * Searches for papers on the Hugging Face Hub
94
+ * @param query Search query string (e.g. "llama", "attention")
95
+ * @param limit Maximum number of results to return
96
+ * @returns Formatted string with paper information
97
+ */
98
+ async search(query: string, limit: number = RESULTS_TO_RETURN, conciseOnly: boolean = false): Promise<string> {
99
+ try {
100
+ if (!query) return 'No query';
101
+
102
+ const papers = await this.callApi<PaperSearchResult[]>({ q: query });
103
+
104
+ if (papers.length === 0) return `No papers found for query '${query}'`;
105
+ return formatSearchResults(query, papers.slice(0, limit), papers.length, conciseOnly);
106
+ } catch (error) {
107
+ if (error instanceof Error) {
108
+ throw new Error(`Failed to search for papers: ${error.message}`);
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ }
114
+
115
+ export function published(publishedDate: string | undefined): string {
116
+ const formatted = formatUnknownDate(publishedDate ?? '');
117
+ return formatted ? `Published on ${formatted}` : 'Publication date not available';
118
+ }
119
+
120
+ function formatSearchResults(
121
+ query: string,
122
+ papers: PaperSearchResult[],
123
+ totalCount: number,
124
+ conciseOnly: boolean = false
125
+ ): string {
126
+ const r: string[] = [];
127
+ const showingText =
128
+ papers.length < totalCount
129
+ ? `${totalCount} papers matched the query '${query}'. Here are the first ${papers.length} results.`
130
+ : `All ${papers.length} papers that matched the query '${query}'`;
131
+ r.push(showingText);
132
+
133
+ for (const result of papers) {
134
+ r.push('');
135
+ r.push('---');
136
+ const title = result.paper.title ?? `Paper ID ${result.paper.id}`;
137
+ r.push('');
138
+ r.push(`## ${title}`);
139
+ r.push('');
140
+ const publishedDate = result.paper.publishedAt
141
+ ? `Published on ${published(result.paper.publishedAt)}`
142
+ : 'Publication date not available';
143
+ r.push(publishedDate);
144
+ r.push(authors(result.paper.authors));
145
+ r.push('');
146
+ // Handle concise_only option: use ai_summary when enabled, or fallback to ai_summary if summary is blank
147
+ const useAiSummary = conciseOnly || !result.paper.summary;
148
+ const summaryText = useAiSummary ? result.paper.ai_summary : result.paper.summary;
149
+ const summaryHeader = useAiSummary ? '### AI Generated Summary' : '### Abstract';
150
+
151
+ r.push(summaryHeader);
152
+ r.push('');
153
+ r.push(summaryText ?? 'No summary available');
154
+ r.push('');
155
+ r.push(result.paper.ai_keywords ? `**AI Keywords**: ${result.paper.ai_keywords.join(', ')}` : '');
156
+
157
+ const upvotes: string =
158
+ result.paper.upvotes && result.paper.upvotes > 0 ? `Upvoted ${result.paper.upvotes} times` : '';
159
+
160
+ if (result.numComments && result.numComments > 0) {
161
+ if (result.isAuthorParticipating)
162
+ r.push(`${upvotes}. The authors are participating in a discussion with ${result.numComments} comments.`);
163
+ else r.push(`${upvotes}. There is a community discussion with ${result.numComments} comments.`);
164
+ } else {
165
+ if ('' != upvotes) r.push(upvotes);
166
+ }
167
+
168
+ r.push(`**Link to paper:** [https://hf.co/papers/${result.paper.id}](https://hf.co/papers/${result.paper.id})`);
169
+ }
170
+ r.push('');
171
+ r.push('---');
172
+ return r.join('\n');
173
+ }
174
+
175
+ export function authors(authors: Author[] | undefined, authorsToShow: number = DEFAULT_AUTHORS_TO_SHOW): string {
176
+ if (!authors || 0 === authors.length) return '**Authors:** Not available';
177
+ const f: string[] = [];
178
+ for (const author of authors.slice(0, authorsToShow)) {
179
+ const profileLink: string = author.user?.user ? ` ([${author.user.user}](https://hf.co/${author.user.user}))` : '';
180
+ const authorName: string = author.name ?? 'Unknown';
181
+ f.push(`${authorName}${profileLink}`);
182
+ }
183
+
184
+ if (authors.length > authorsToShow) {
185
+ f.push(`and ${authors.length - authorsToShow} more.`);
186
+ }
187
+ return `**Authors:** ${f.join(', ')}`;
188
+ }
@@ -0,0 +1,303 @@
1
+ import { z } from 'zod';
2
+ import { HfApiCall, HfApiError } from './hf-api-call.js';
3
+ import { formatDate, formatNumber, escapeMarkdown } from './utilities.js';
4
+ import { ModelSearchTool } from './model-search.js';
5
+ import { DatasetSearchTool } from './dataset-search.js';
6
+ import { SpaceSearchTool } from './space-search.js';
7
+ import { authors, type Author } from './paper-search.js';
8
+
9
+ // Paper Summary Prompt Configuration
10
+ export const PAPER_SUMMARY_PROMPT_CONFIG = {
11
+ name: 'Paper Summary',
12
+ description:
13
+ 'Generate a comprehensive summary of an arXiv paper including its details and related models, datasets, and spaces on Hugging Face. ' +
14
+ 'Accepts various formats: "2502.16161", "arxiv:2502.16161", "https://arxiv.org/abs/2502.16161", or Hugging Face paper URLs.',
15
+ schema: z.object({
16
+ paper_id: z
17
+ .string()
18
+ .min(1, 'Paper ID is required')
19
+ .describe('arXiv paper ID in various formats (e.g., "2502.16161", "arxiv:2502.16161", or full URL)')
20
+ .max(60)
21
+ .describe('Maximum length is 100 characters'),
22
+ }),
23
+ } as const;
24
+
25
+ // Define parameter types
26
+ export type PaperSummaryParams = z.infer<typeof PAPER_SUMMARY_PROMPT_CONFIG.schema>;
27
+
28
+ // Paper API response interface
29
+ interface PaperDetails {
30
+ id: string;
31
+ title: string;
32
+ authors?: Author[];
33
+ publishedAt: string;
34
+ summary?: string; // This is the abstract field in the API
35
+ upvotes?: number;
36
+ comments?: number;
37
+ pageUrl?: string;
38
+ }
39
+
40
+ /**
41
+ * Validates and extracts arXiv ID from various input formats
42
+ * @param input - The user input (arXiv ID or URL)
43
+ * @returns The extracted arXiv ID in format "YYMM.NNNNN"
44
+ * @throws Error if input is invalid
45
+ */
46
+ export function extractArxivIdFromInput(input: string): string {
47
+ // Remove whitespace
48
+ const trimmed = input.trim();
49
+
50
+ // Check for empty input
51
+ if (!trimmed) {
52
+ throw new Error('Paper ID is required');
53
+ }
54
+
55
+ // Pattern for valid arXiv ID: YYMM.NNNNN (e.g., 2502.16161)
56
+ const arxivPattern = /^\d{4}\.\d{4,5}$/;
57
+
58
+ // Check if it's already a plain arXiv ID
59
+ if (arxivPattern.test(trimmed)) {
60
+ return trimmed;
61
+ }
62
+
63
+ // Handle URL formats first - check if it looks like a URL
64
+ // Check for: protocol, www prefix, domain pattern with TLD, or path separator
65
+ const urlPattern = /^(https?:\/\/|www\.)|^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/|$)/;
66
+ if (urlPattern.test(trimmed) || trimmed.includes('://')) {
67
+ let url: URL;
68
+ try {
69
+ // Try to parse as URL, adding protocol if missing
70
+ if (!trimmed.startsWith('http')) {
71
+ url = new URL(`https://${trimmed}`);
72
+ } else {
73
+ url = new URL(trimmed);
74
+ }
75
+ } catch {
76
+ throw new Error('Invalid URL format');
77
+ }
78
+
79
+ // Check for query parameters or fragments
80
+ if (url.search || url.hash) {
81
+ throw new Error('URL must contain only the paper ID path');
82
+ }
83
+
84
+ // Only accept specific domains
85
+ const allowedHosts = ['arxiv.org', 'www.arxiv.org', 'huggingface.co', 'hf.co'];
86
+ if (!allowedHosts.includes(url.hostname)) {
87
+ throw new Error(`URL must be from arxiv.org, huggingface.co, or hf.co. Got: ${url.hostname}`);
88
+ }
89
+
90
+ // Handle arxiv.org URLs
91
+ if (url.hostname === 'arxiv.org' || url.hostname === 'www.arxiv.org') {
92
+ // Pattern: /abs/YYMM.NNNNN
93
+ const match = url.pathname.match(/\/abs\/(\d{4}\.\d{4,5})/);
94
+ if (match && match[1]) {
95
+ return match[1];
96
+ }
97
+ throw new Error('arXiv URL must be in format: arxiv.org/abs/YYMM.NNNNN');
98
+ }
99
+
100
+ // Handle Hugging Face paper URLs
101
+ if (url.hostname === 'huggingface.co' || url.hostname === 'hf.co') {
102
+ // Pattern: /papers/YYMM.NNNNN
103
+ const match = url.pathname.match(/\/papers\/(\d{4}\.\d{4,5})/);
104
+ if (match && match[1]) {
105
+ return match[1];
106
+ }
107
+ throw new Error('Hugging Face URL must be in format: hf.co/papers/YYMM.NNNNN');
108
+ }
109
+
110
+ // This should never be reached due to the allowedHosts check above
111
+ throw new Error('URL does not contain a valid arXiv ID');
112
+ }
113
+
114
+ // Handle "arxiv:" prefix variations
115
+ if (trimmed.toLowerCase().startsWith('arxiv:')) {
116
+ const id = trimmed.substring(6);
117
+ if (arxivPattern.test(id)) {
118
+ return id;
119
+ }
120
+ throw new Error('Invalid arXiv ID format after "arxiv:" prefix');
121
+ }
122
+
123
+ // Handle "arxiv." prefix (typo)
124
+ if (trimmed.toLowerCase().startsWith('arxiv.')) {
125
+ const id = trimmed.substring(6);
126
+ if (arxivPattern.test(id)) {
127
+ return id;
128
+ }
129
+ throw new Error('Invalid arXiv ID format after "arxiv." prefix');
130
+ }
131
+
132
+ // If we get here, it's not a recognized format
133
+ throw new Error(
134
+ `Invalid arXiv ID format: "${trimmed}". Expected formats: "2502.16161", "arxiv:2502.16161", or paper URL`
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Service for generating comprehensive paper summaries
140
+ */
141
+ export class PaperSummaryPrompt extends HfApiCall<Record<string, string>, PaperDetails> {
142
+ /**
143
+ * @param hfToken Optional Hugging Face token for API access
144
+ */
145
+ constructor(hfToken?: string) {
146
+ super('https://huggingface.co/api/papers', hfToken);
147
+ }
148
+
149
+ /**
150
+ * Generate a comprehensive paper summary
151
+ */
152
+ async generateSummary(params: PaperSummaryParams): Promise<string> {
153
+ try {
154
+ // Extract and validate arXiv ID
155
+ const arxivId = extractArxivIdFromInput(params.paper_id);
156
+
157
+ // Get paper details
158
+ let paperDetails: PaperDetails;
159
+ try {
160
+ paperDetails = await this.getPaperDetails(arxivId);
161
+ } catch (error) {
162
+ if (error instanceof HfApiError && error.status === 404) {
163
+ return "I'm sorry, paper not found.";
164
+ }
165
+ throw error;
166
+ }
167
+
168
+ // Build the summary
169
+ const sections: string[] = [];
170
+
171
+ // Paper details section
172
+ sections.push(this.formatPaperDetails(paperDetails));
173
+
174
+ // Search for related resources
175
+ const relatedResources = await this.getRelatedResources(arxivId);
176
+
177
+ // Add related models section if found
178
+ if (relatedResources.models) {
179
+ sections.push(relatedResources.models);
180
+ }
181
+
182
+ // Add related datasets section if found
183
+ if (relatedResources.datasets) {
184
+ sections.push(relatedResources.datasets);
185
+ }
186
+
187
+ // Add related spaces section if found
188
+ if (relatedResources.spaces) {
189
+ sections.push(relatedResources.spaces);
190
+ }
191
+
192
+ // Add reminder about tags
193
+ sections.push(
194
+ '\n**Note:** Tags and paper references on Hugging Face are not always complete or up-to-date. ' +
195
+ '-- validate information if necessary'
196
+ );
197
+
198
+ // Add final instruction
199
+ sections.push('\nPlease provide a summary of this paper and any associated resources.');
200
+
201
+ return sections.join('\n\n');
202
+ } catch (error) {
203
+ if (error instanceof Error) {
204
+ throw new Error(`Failed to generate paper summary: ${error.message}`);
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Get paper details from HF API
212
+ */
213
+ private async getPaperDetails(arxivId: string): Promise<PaperDetails> {
214
+ const url = new URL(`${this.apiUrl}/${arxivId}`);
215
+ return this.fetchFromApi<PaperDetails>(url);
216
+ }
217
+
218
+ /**
219
+ * Format paper details as markdown
220
+ */
221
+ private formatPaperDetails(paper: PaperDetails): string {
222
+ const lines: string[] = [];
223
+
224
+ // Title as main heading
225
+ lines.push(`# ${escapeMarkdown(paper.title || 'Untitled')}`);
226
+ lines.push('');
227
+
228
+ // Authors - use the existing authors formatting function
229
+ lines.push(authors(paper.authors));
230
+
231
+ // Published date
232
+ lines.push(`**Published:** ${formatDate(paper.publishedAt)}`);
233
+
234
+ // Engagement metrics - only show if they exist and are > 0
235
+ if (paper.upvotes && paper.upvotes > 0) {
236
+ lines.push(`**Upvotes:** ${formatNumber(paper.upvotes)}`);
237
+ }
238
+ if (paper.comments && paper.comments > 0) {
239
+ lines.push(`**Comments:** ${formatNumber(paper.comments)}`);
240
+ }
241
+
242
+ // Links
243
+ lines.push('');
244
+ lines.push('**Links:**');
245
+ lines.push(`- [Hugging Face Paper Page](https://hf.co/papers/${paper.id})`);
246
+ lines.push(`- [arXiv Page](https://arxiv.org/abs/${paper.id})`);
247
+
248
+ // Abstract
249
+ if (paper.summary) {
250
+ lines.push('');
251
+ lines.push('## Abstract');
252
+ lines.push('');
253
+ lines.push(paper.summary);
254
+ }
255
+
256
+ return lines.join('\n');
257
+ }
258
+
259
+ /**
260
+ * Search for related resources (models, datasets, spaces)
261
+ */
262
+ private async getRelatedResources(arxivId: string): Promise<{ models?: string; datasets?: string; spaces?: string }> {
263
+ const results: { models?: string; datasets?: string; spaces?: string } = {};
264
+
265
+ // Search for related models
266
+ try {
267
+ const modelSearch = new ModelSearchTool(this.hfToken);
268
+ // Use the filter parameter to search for models referencing this paper
269
+ const modelResults = await modelSearch.searchWithFilter(`arxiv:${arxivId}`, 25);
270
+ if (modelResults && !modelResults.includes('No models found')) {
271
+ results.models = `## Related Models\n\n${modelResults}`;
272
+ }
273
+ } catch (error) {
274
+ console.warn(`Failed to fetch related models for paper ${arxivId}:`, error);
275
+ }
276
+
277
+ // Search for related datasets
278
+ try {
279
+ const datasetSearch = new DatasetSearchTool(this.hfToken);
280
+ // Use the filter parameter to search for datasets referencing this paper
281
+ const datasetResults = await datasetSearch.searchWithFilter(`arxiv:${arxivId}`, 25);
282
+ if (datasetResults && !datasetResults.includes('No datasets found')) {
283
+ results.datasets = `## Related Datasets\n\n${datasetResults}`;
284
+ }
285
+ } catch (error) {
286
+ console.warn(`Failed to fetch related datasets for paper ${arxivId}:`, error);
287
+ }
288
+
289
+ // Search for related spaces
290
+ try {
291
+ const spaceSearch = new SpaceSearchTool(this.hfToken);
292
+ // Use the filter parameter to search for spaces referencing this paper
293
+ const spaceResults = await spaceSearch.searchWithFilter(`arxiv:${arxivId}`, 25, 2);
294
+ if (spaceResults && !spaceResults.includes('No matching Hugging Face Spaces found')) {
295
+ results.spaces = `## Related Spaces\n\n${spaceResults}`;
296
+ }
297
+ } catch (error) {
298
+ console.warn(`Failed to fetch related spaces for paper ${arxivId}:`, error);
299
+ }
300
+
301
+ return results;
302
+ }
303
+ }