@wentorai/research-plugins 1.2.0 → 1.2.2
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/tools/arxiv.ts +13 -10
- package/src/tools/crossref.ts +13 -10
- package/src/tools/openalex.ts +21 -17
- package/src/tools/pubmed.ts +15 -12
- package/src/tools/semantic-scholar.ts +16 -12
- package/src/tools/unpaywall.ts +8 -6
- package/src/tools/util.ts +11 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/tools/arxiv.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const BASE = "https://export.arxiv.org/api/query";
|
|
5
6
|
|
|
@@ -59,9 +60,10 @@ export function createArxivTools(
|
|
|
59
60
|
return [
|
|
60
61
|
{
|
|
61
62
|
name: "search_arxiv",
|
|
63
|
+
label: "Search Papers (arXiv)",
|
|
62
64
|
description:
|
|
63
65
|
"Search arXiv preprint repository. Covers physics, math, CS, biology, quantitative finance, statistics, and more.",
|
|
64
|
-
|
|
66
|
+
parameters: Type.Object({
|
|
65
67
|
query: Type.String({
|
|
66
68
|
description:
|
|
67
69
|
"Search query. Supports field prefixes: ti: (title), au: (author), abs: (abstract), cat: (category). E.g. 'ti:transformer AND cat:cs.CL'",
|
|
@@ -79,7 +81,7 @@ export function createArxivTools(
|
|
|
79
81
|
Type.String({ description: "Sort order: 'ascending' or 'descending'" }),
|
|
80
82
|
),
|
|
81
83
|
}),
|
|
82
|
-
|
|
84
|
+
execute: async (input: {
|
|
83
85
|
query: string;
|
|
84
86
|
max_results?: number;
|
|
85
87
|
sort_by?: string;
|
|
@@ -93,7 +95,7 @@ export function createArxivTools(
|
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
const res = await fetch(`${BASE}?${params}`);
|
|
96
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
98
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
97
99
|
const xml = await res.text();
|
|
98
100
|
|
|
99
101
|
const totalMatch = xml.match(
|
|
@@ -101,30 +103,31 @@ export function createArxivTools(
|
|
|
101
103
|
);
|
|
102
104
|
const total = totalMatch ? parseInt(totalMatch[1], 10) : 0;
|
|
103
105
|
|
|
104
|
-
return {
|
|
106
|
+
return toolResult({
|
|
105
107
|
total_results: total,
|
|
106
108
|
papers: parseArxivXml(xml),
|
|
107
|
-
};
|
|
109
|
+
});
|
|
108
110
|
},
|
|
109
111
|
},
|
|
110
112
|
{
|
|
111
113
|
name: "get_arxiv_paper",
|
|
114
|
+
label: "Get Paper Details (arXiv)",
|
|
112
115
|
description:
|
|
113
116
|
"Get detailed information about a specific arXiv paper by its ID.",
|
|
114
|
-
|
|
117
|
+
parameters: Type.Object({
|
|
115
118
|
arxiv_id: Type.String({
|
|
116
119
|
description: "arXiv paper ID, e.g. '2301.00001' or '2301.00001v2'",
|
|
117
120
|
}),
|
|
118
121
|
}),
|
|
119
|
-
|
|
122
|
+
execute: async (input: { arxiv_id: string }) => {
|
|
120
123
|
const id = input.arxiv_id.replace("arXiv:", "");
|
|
121
124
|
const params = new URLSearchParams({ id_list: id });
|
|
122
125
|
const res = await fetch(`${BASE}?${params}`);
|
|
123
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
126
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
124
127
|
const xml = await res.text();
|
|
125
128
|
const papers = parseArxivXml(xml);
|
|
126
|
-
if (papers.length === 0) return { error: "Paper not found" };
|
|
127
|
-
return papers[0];
|
|
129
|
+
if (papers.length === 0) return toolResult({ error: "Paper not found" });
|
|
130
|
+
return toolResult(papers[0]);
|
|
128
131
|
},
|
|
129
132
|
},
|
|
130
133
|
];
|
package/src/tools/crossref.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const BASE = "https://api.crossref.org";
|
|
5
6
|
|
|
@@ -14,22 +15,23 @@ export function createCrossRefTools(
|
|
|
14
15
|
return [
|
|
15
16
|
{
|
|
16
17
|
name: "resolve_doi",
|
|
18
|
+
label: "Resolve DOI (CrossRef)",
|
|
17
19
|
description:
|
|
18
20
|
"Resolve a DOI to get full bibliographic metadata from CrossRef (title, authors, journal, dates, references).",
|
|
19
|
-
|
|
21
|
+
parameters: Type.Object({
|
|
20
22
|
doi: Type.String({
|
|
21
23
|
description: "DOI to resolve, e.g. '10.1038/nature12373'",
|
|
22
24
|
}),
|
|
23
25
|
}),
|
|
24
|
-
|
|
26
|
+
execute: async (input: { doi: string }) => {
|
|
25
27
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
26
28
|
const res = await fetch(`${BASE}/works/${encodeURIComponent(doi)}`, {
|
|
27
29
|
headers,
|
|
28
30
|
});
|
|
29
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
31
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
30
32
|
const data = await res.json();
|
|
31
33
|
const w = data.message;
|
|
32
|
-
return {
|
|
34
|
+
return toolResult({
|
|
33
35
|
doi: w.DOI,
|
|
34
36
|
title: w.title?.[0],
|
|
35
37
|
authors: w.author?.map(
|
|
@@ -44,14 +46,15 @@ export function createCrossRefTools(
|
|
|
44
46
|
url: w.URL,
|
|
45
47
|
abstract: w.abstract,
|
|
46
48
|
license: w.license?.[0]?.URL,
|
|
47
|
-
};
|
|
49
|
+
});
|
|
48
50
|
},
|
|
49
51
|
},
|
|
50
52
|
{
|
|
51
53
|
name: "search_crossref",
|
|
54
|
+
label: "Search Works (CrossRef)",
|
|
52
55
|
description:
|
|
53
56
|
"Search CrossRef for scholarly works by query. Covers 150M+ DOIs across all publishers.",
|
|
54
|
-
|
|
57
|
+
parameters: Type.Object({
|
|
55
58
|
query: Type.String({ description: "Search query" }),
|
|
56
59
|
limit: Type.Optional(
|
|
57
60
|
Type.Number({ description: "Max results (default 10, max 100)" }),
|
|
@@ -71,7 +74,7 @@ export function createCrossRefTools(
|
|
|
71
74
|
}),
|
|
72
75
|
),
|
|
73
76
|
}),
|
|
74
|
-
|
|
77
|
+
execute: async (input: {
|
|
75
78
|
query: string;
|
|
76
79
|
limit?: number;
|
|
77
80
|
from_year?: number;
|
|
@@ -89,9 +92,9 @@ export function createCrossRefTools(
|
|
|
89
92
|
if (input.sort) params.set("sort", input.sort);
|
|
90
93
|
|
|
91
94
|
const res = await fetch(`${BASE}/works?${params}`, { headers });
|
|
92
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
95
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
93
96
|
const data = await res.json();
|
|
94
|
-
return {
|
|
97
|
+
return toolResult({
|
|
95
98
|
total_results: data.message?.["total-results"],
|
|
96
99
|
items: data.message?.items?.map((w: Record<string, unknown>) => ({
|
|
97
100
|
doi: w.DOI,
|
|
@@ -105,7 +108,7 @@ export function createCrossRefTools(
|
|
|
105
108
|
type: w.type,
|
|
106
109
|
cited_by: w["is-referenced-by-count"],
|
|
107
110
|
})),
|
|
108
|
-
};
|
|
111
|
+
});
|
|
109
112
|
},
|
|
110
113
|
},
|
|
111
114
|
];
|
package/src/tools/openalex.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const BASE = "https://api.openalex.org";
|
|
5
6
|
|
|
@@ -14,9 +15,10 @@ export function createOpenAlexTools(
|
|
|
14
15
|
return [
|
|
15
16
|
{
|
|
16
17
|
name: "search_openalex",
|
|
18
|
+
label: "Search Works (OpenAlex)",
|
|
17
19
|
description:
|
|
18
20
|
"Search academic works via OpenAlex (free, no key required). Covers 250M+ works across all disciplines.",
|
|
19
|
-
|
|
21
|
+
parameters: Type.Object({
|
|
20
22
|
query: Type.String({ description: "Search query for works" }),
|
|
21
23
|
limit: Type.Optional(
|
|
22
24
|
Type.Number({ description: "Max results (default 10, max 200)" }),
|
|
@@ -36,7 +38,7 @@ export function createOpenAlexTools(
|
|
|
36
38
|
}),
|
|
37
39
|
),
|
|
38
40
|
}),
|
|
39
|
-
|
|
41
|
+
execute: async (input: {
|
|
40
42
|
query: string;
|
|
41
43
|
limit?: number;
|
|
42
44
|
from_year?: number;
|
|
@@ -57,9 +59,9 @@ export function createOpenAlexTools(
|
|
|
57
59
|
if (input.sort_by) params.set("sort", input.sort_by);
|
|
58
60
|
|
|
59
61
|
const res = await fetch(`${BASE}/works?${params}`, { headers });
|
|
60
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
62
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
61
63
|
const data = await res.json();
|
|
62
|
-
return {
|
|
64
|
+
return toolResult({
|
|
63
65
|
total_count: data.meta?.count,
|
|
64
66
|
results: data.results?.map((w: Record<string, unknown>) => ({
|
|
65
67
|
id: w.id,
|
|
@@ -76,29 +78,30 @@ export function createOpenAlexTools(
|
|
|
76
78
|
)
|
|
77
79
|
: [],
|
|
78
80
|
})),
|
|
79
|
-
};
|
|
81
|
+
});
|
|
80
82
|
},
|
|
81
83
|
},
|
|
82
84
|
{
|
|
83
85
|
name: "get_work",
|
|
86
|
+
label: "Get Work Details (OpenAlex)",
|
|
84
87
|
description:
|
|
85
88
|
"Get full details of a work by its OpenAlex ID, DOI, or other identifier.",
|
|
86
|
-
|
|
89
|
+
parameters: Type.Object({
|
|
87
90
|
work_id: Type.String({
|
|
88
91
|
description:
|
|
89
92
|
"Work identifier: OpenAlex ID (e.g. 'W2741809807'), DOI URL, or PMID",
|
|
90
93
|
}),
|
|
91
94
|
}),
|
|
92
|
-
|
|
95
|
+
execute: async (input: { work_id: string }) => {
|
|
93
96
|
const id = input.work_id.startsWith("10.")
|
|
94
97
|
? `https://doi.org/${input.work_id}`
|
|
95
98
|
: input.work_id;
|
|
96
99
|
const res = await fetch(`${BASE}/works/${encodeURIComponent(id)}`, {
|
|
97
100
|
headers,
|
|
98
101
|
});
|
|
99
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
102
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
100
103
|
const w = await res.json();
|
|
101
|
-
return {
|
|
104
|
+
return toolResult({
|
|
102
105
|
id: w.id,
|
|
103
106
|
doi: w.doi,
|
|
104
107
|
title: w.title,
|
|
@@ -117,20 +120,21 @@ export function createOpenAlexTools(
|
|
|
117
120
|
?.slice(0, 10)
|
|
118
121
|
.map((c: Record<string, unknown>) => c.display_name),
|
|
119
122
|
referenced_works_count: w.referenced_works?.length,
|
|
120
|
-
};
|
|
123
|
+
});
|
|
121
124
|
},
|
|
122
125
|
},
|
|
123
126
|
{
|
|
124
127
|
name: "get_author_openalex",
|
|
128
|
+
label: "Get Author (OpenAlex)",
|
|
125
129
|
description:
|
|
126
130
|
"Get author information from OpenAlex including publications, h-index, and affiliations.",
|
|
127
|
-
|
|
131
|
+
parameters: Type.Object({
|
|
128
132
|
author_id: Type.String({
|
|
129
133
|
description:
|
|
130
134
|
"Author identifier: OpenAlex ID (e.g. 'A5023888391'), ORCID, or name search",
|
|
131
135
|
}),
|
|
132
136
|
}),
|
|
133
|
-
|
|
137
|
+
execute: async (input: { author_id: string }) => {
|
|
134
138
|
let url: string;
|
|
135
139
|
if (
|
|
136
140
|
input.author_id.startsWith("A") ||
|
|
@@ -145,14 +149,14 @@ export function createOpenAlexTools(
|
|
|
145
149
|
per_page: "5",
|
|
146
150
|
});
|
|
147
151
|
const res = await fetch(`${BASE}/authors?${params}`, { headers });
|
|
148
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
149
|
-
return res.json();
|
|
152
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
153
|
+
return toolResult(await res.json());
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
const res = await fetch(url, { headers });
|
|
153
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
157
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
154
158
|
const a = await res.json();
|
|
155
|
-
return {
|
|
159
|
+
return toolResult({
|
|
156
160
|
id: a.id,
|
|
157
161
|
display_name: a.display_name,
|
|
158
162
|
orcid: a.orcid,
|
|
@@ -167,7 +171,7 @@ export function createOpenAlexTools(
|
|
|
167
171
|
top_concepts: a.x_concepts
|
|
168
172
|
?.slice(0, 5)
|
|
169
173
|
.map((c: Record<string, unknown>) => c.display_name),
|
|
170
|
-
};
|
|
174
|
+
});
|
|
171
175
|
},
|
|
172
176
|
},
|
|
173
177
|
];
|
package/src/tools/pubmed.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const EUTILS = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
5
6
|
|
|
@@ -10,9 +11,10 @@ export function createPubMedTools(
|
|
|
10
11
|
return [
|
|
11
12
|
{
|
|
12
13
|
name: "search_pubmed",
|
|
14
|
+
label: "Search Articles (PubMed)",
|
|
13
15
|
description:
|
|
14
16
|
"Search PubMed biomedical literature database. Covers 36M+ citations from MEDLINE, life science journals, and online books.",
|
|
15
|
-
|
|
17
|
+
parameters: Type.Object({
|
|
16
18
|
query: Type.String({
|
|
17
19
|
description:
|
|
18
20
|
"PubMed search query. Supports MeSH terms and field tags, e.g. 'CRISPR[Title] AND 2024[PDAT]'",
|
|
@@ -32,7 +34,7 @@ export function createPubMedTools(
|
|
|
32
34
|
Type.String({ description: "Max publication date (YYYY/MM/DD)" }),
|
|
33
35
|
),
|
|
34
36
|
}),
|
|
35
|
-
|
|
37
|
+
execute: async (input: {
|
|
36
38
|
query: string;
|
|
37
39
|
max_results?: number;
|
|
38
40
|
sort?: string;
|
|
@@ -53,11 +55,11 @@ export function createPubMedTools(
|
|
|
53
55
|
|
|
54
56
|
const searchRes = await fetch(`${EUTILS}/esearch.fcgi?${searchParams}`);
|
|
55
57
|
if (!searchRes.ok)
|
|
56
|
-
return { error: `Search error: ${searchRes.status} ${searchRes.statusText}` };
|
|
58
|
+
return toolResult({ error: `Search error: ${searchRes.status} ${searchRes.statusText}` });
|
|
57
59
|
const searchData = await searchRes.json();
|
|
58
60
|
const ids: string[] = searchData.esearchresult?.idlist ?? [];
|
|
59
61
|
|
|
60
|
-
if (ids.length === 0) return { total_count: searchData.esearchresult?.count ?? 0, articles: [] };
|
|
62
|
+
if (ids.length === 0) return toolResult({ total_count: searchData.esearchresult?.count ?? 0, articles: [] });
|
|
61
63
|
|
|
62
64
|
const summaryParams = new URLSearchParams({
|
|
63
65
|
db: "pubmed",
|
|
@@ -66,7 +68,7 @@ export function createPubMedTools(
|
|
|
66
68
|
});
|
|
67
69
|
const summaryRes = await fetch(`${EUTILS}/esummary.fcgi?${summaryParams}`);
|
|
68
70
|
if (!summaryRes.ok)
|
|
69
|
-
return { error: `Summary error: ${summaryRes.status} ${summaryRes.statusText}` };
|
|
71
|
+
return toolResult({ error: `Summary error: ${summaryRes.status} ${summaryRes.statusText}` });
|
|
70
72
|
const summaryData = await summaryRes.json();
|
|
71
73
|
|
|
72
74
|
const articles = ids.map((id) => {
|
|
@@ -87,27 +89,28 @@ export function createPubMedTools(
|
|
|
87
89
|
};
|
|
88
90
|
});
|
|
89
91
|
|
|
90
|
-
return {
|
|
92
|
+
return toolResult({
|
|
91
93
|
total_count: parseInt(searchData.esearchresult?.count ?? "0", 10),
|
|
92
94
|
articles,
|
|
93
|
-
};
|
|
95
|
+
});
|
|
94
96
|
},
|
|
95
97
|
},
|
|
96
98
|
{
|
|
97
99
|
name: "get_article",
|
|
100
|
+
label: "Get Article Details (PubMed)",
|
|
98
101
|
description:
|
|
99
102
|
"Get detailed article metadata from PubMed by PMID, including abstract.",
|
|
100
|
-
|
|
103
|
+
parameters: Type.Object({
|
|
101
104
|
pmid: Type.String({ description: "PubMed ID (numeric string)" }),
|
|
102
105
|
}),
|
|
103
|
-
|
|
106
|
+
execute: async (input: { pmid: string }) => {
|
|
104
107
|
const params = new URLSearchParams({
|
|
105
108
|
db: "pubmed",
|
|
106
109
|
id: input.pmid,
|
|
107
110
|
retmode: "xml",
|
|
108
111
|
});
|
|
109
112
|
const res = await fetch(`${EUTILS}/efetch.fcgi?${params}`);
|
|
110
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
113
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
111
114
|
const xml = await res.text();
|
|
112
115
|
|
|
113
116
|
const getText = (tag: string) => {
|
|
@@ -148,7 +151,7 @@ export function createPubMedTools(
|
|
|
148
151
|
xml.match(/<ArticleId IdType="pmc">([\s\S]*?)<\/ArticleId>/)?.[1]?.trim() ??
|
|
149
152
|
"";
|
|
150
153
|
|
|
151
|
-
return {
|
|
154
|
+
return toolResult({
|
|
152
155
|
pmid: input.pmid,
|
|
153
156
|
title: getText("ArticleTitle"),
|
|
154
157
|
abstract: getAbstract(),
|
|
@@ -159,7 +162,7 @@ export function createPubMedTools(
|
|
|
159
162
|
pmc,
|
|
160
163
|
mesh_terms: getMesh(),
|
|
161
164
|
url: `https://pubmed.ncbi.nlm.nih.gov/${input.pmid}/`,
|
|
162
|
-
};
|
|
165
|
+
});
|
|
163
166
|
},
|
|
164
167
|
},
|
|
165
168
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const BASE = "https://api.semanticscholar.org/graph/v1";
|
|
5
6
|
|
|
@@ -15,9 +16,10 @@ export function createSemanticScholarTools(
|
|
|
15
16
|
return [
|
|
16
17
|
{
|
|
17
18
|
name: "search_papers",
|
|
19
|
+
label: "Search Papers (Semantic Scholar)",
|
|
18
20
|
description:
|
|
19
21
|
"Search academic papers on Semantic Scholar. Returns titles, abstracts, authors, citation counts.",
|
|
20
|
-
|
|
22
|
+
parameters: Type.Object({
|
|
21
23
|
query: Type.String({ description: "Search query keywords" }),
|
|
22
24
|
limit: Type.Optional(
|
|
23
25
|
Type.Number({ description: "Max results to return (default 10, max 100)" }),
|
|
@@ -35,7 +37,7 @@ export function createSemanticScholarTools(
|
|
|
35
37
|
Type.Boolean({ description: "Only return open access papers" }),
|
|
36
38
|
),
|
|
37
39
|
}),
|
|
38
|
-
|
|
40
|
+
execute: async (input: {
|
|
39
41
|
query: string;
|
|
40
42
|
limit?: number;
|
|
41
43
|
year?: string;
|
|
@@ -54,36 +56,38 @@ export function createSemanticScholarTools(
|
|
|
54
56
|
if (input.open_access_only) params.set("openAccessPdf", "");
|
|
55
57
|
|
|
56
58
|
const res = await fetch(`${BASE}/paper/search?${params}`, { headers });
|
|
57
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
58
|
-
return res.json();
|
|
59
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
60
|
+
return toolResult(await res.json());
|
|
59
61
|
},
|
|
60
62
|
},
|
|
61
63
|
{
|
|
62
64
|
name: "get_paper",
|
|
65
|
+
label: "Get Paper Details (Semantic Scholar)",
|
|
63
66
|
description:
|
|
64
67
|
"Get detailed information about a specific paper by its Semantic Scholar ID, DOI, or ArXiv ID.",
|
|
65
|
-
|
|
68
|
+
parameters: Type.Object({
|
|
66
69
|
paper_id: Type.String({
|
|
67
70
|
description:
|
|
68
71
|
"Paper identifier: Semantic Scholar ID, DOI (e.g. '10.1234/...'), or ArXiv ID (e.g. 'arXiv:2301.00001')",
|
|
69
72
|
}),
|
|
70
73
|
}),
|
|
71
|
-
|
|
74
|
+
execute: async (input: { paper_id: string }) => {
|
|
72
75
|
const fields =
|
|
73
76
|
"title,abstract,authors,year,citationCount,referenceCount,tldr,url,venue,isOpenAccess,openAccessPdf,fieldsOfStudy,publicationDate";
|
|
74
77
|
const res = await fetch(
|
|
75
78
|
`${BASE}/paper/${encodeURIComponent(input.paper_id)}?fields=${fields}`,
|
|
76
79
|
{ headers },
|
|
77
80
|
);
|
|
78
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
79
|
-
return res.json();
|
|
81
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
82
|
+
return toolResult(await res.json());
|
|
80
83
|
},
|
|
81
84
|
},
|
|
82
85
|
{
|
|
83
86
|
name: "get_citations",
|
|
87
|
+
label: "Get Citations (Semantic Scholar)",
|
|
84
88
|
description:
|
|
85
89
|
"Get papers that cite a given paper. Useful for forward citation tracking.",
|
|
86
|
-
|
|
90
|
+
parameters: Type.Object({
|
|
87
91
|
paper_id: Type.String({ description: "Paper identifier (S2 ID, DOI, or ArXiv ID)" }),
|
|
88
92
|
limit: Type.Optional(
|
|
89
93
|
Type.Number({ description: "Max citations to return (default 20, max 100)" }),
|
|
@@ -92,7 +96,7 @@ export function createSemanticScholarTools(
|
|
|
92
96
|
Type.Number({ description: "Pagination offset" }),
|
|
93
97
|
),
|
|
94
98
|
}),
|
|
95
|
-
|
|
99
|
+
execute: async (input: { paper_id: string; limit?: number; offset?: number }) => {
|
|
96
100
|
const fields = "title,authors,year,citationCount,url,abstract";
|
|
97
101
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
98
102
|
const offset = input.offset ?? 0;
|
|
@@ -100,8 +104,8 @@ export function createSemanticScholarTools(
|
|
|
100
104
|
`${BASE}/paper/${encodeURIComponent(input.paper_id)}/citations?fields=${fields}&limit=${limit}&offset=${offset}`,
|
|
101
105
|
{ headers },
|
|
102
106
|
);
|
|
103
|
-
if (!res.ok) return { error: `API error: ${res.status} ${res.statusText}` };
|
|
104
|
-
return res.json();
|
|
107
|
+
if (!res.ok) return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
108
|
+
return toolResult(await res.json());
|
|
105
109
|
},
|
|
106
110
|
},
|
|
107
111
|
];
|
package/src/tools/unpaywall.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
|
+
import { toolResult } from "./util.js";
|
|
3
4
|
|
|
4
5
|
const BASE = "https://api.unpaywall.org/v2";
|
|
5
6
|
|
|
@@ -12,21 +13,22 @@ export function createUnpaywallTools(
|
|
|
12
13
|
return [
|
|
13
14
|
{
|
|
14
15
|
name: "find_oa_version",
|
|
16
|
+
label: "Find Open Access Version (Unpaywall)",
|
|
15
17
|
description:
|
|
16
18
|
"Find open access versions of a paper by DOI using Unpaywall. Returns free PDF links from repositories, preprint servers, and publisher OA pages.",
|
|
17
|
-
|
|
19
|
+
parameters: Type.Object({
|
|
18
20
|
doi: Type.String({
|
|
19
21
|
description: "DOI of the paper, e.g. '10.1038/nature12373'",
|
|
20
22
|
}),
|
|
21
23
|
}),
|
|
22
|
-
|
|
24
|
+
execute: async (input: { doi: string }) => {
|
|
23
25
|
const doi = input.doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
24
26
|
const res = await fetch(
|
|
25
27
|
`${BASE}/${encodeURIComponent(doi)}?email=${email}`,
|
|
26
28
|
);
|
|
27
29
|
if (!res.ok) {
|
|
28
|
-
if (res.status === 404) return { error: "DOI not found in Unpaywall" };
|
|
29
|
-
return { error: `API error: ${res.status} ${res.statusText}` };
|
|
30
|
+
if (res.status === 404) return toolResult({ error: "DOI not found in Unpaywall" });
|
|
31
|
+
return toolResult({ error: `API error: ${res.status} ${res.statusText}` });
|
|
30
32
|
}
|
|
31
33
|
const data = await res.json();
|
|
32
34
|
|
|
@@ -40,7 +42,7 @@ export function createUnpaywallTools(
|
|
|
40
42
|
version: loc.version,
|
|
41
43
|
}));
|
|
42
44
|
|
|
43
|
-
return {
|
|
45
|
+
return toolResult({
|
|
44
46
|
doi: data.doi,
|
|
45
47
|
title: data.title,
|
|
46
48
|
is_oa: data.is_oa,
|
|
@@ -51,7 +53,7 @@ export function createUnpaywallTools(
|
|
|
51
53
|
publisher: data.publisher,
|
|
52
54
|
published_date: data.published_date,
|
|
53
55
|
oa_locations: oaLocations,
|
|
54
|
-
};
|
|
56
|
+
});
|
|
55
57
|
},
|
|
56
58
|
},
|
|
57
59
|
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap raw data into the OpenClaw AgentTool return format.
|
|
3
|
+
*
|
|
4
|
+
* Expected shape: { content: [{ type: "text", text: string }], details: unknown }
|
|
5
|
+
* - content → displayed to the user / consumed by the model as text
|
|
6
|
+
* - details → structured data for programmatic access by downstream tools
|
|
7
|
+
*/
|
|
8
|
+
export function toolResult(data: unknown) {
|
|
9
|
+
const text = JSON.stringify(data, null, 2);
|
|
10
|
+
return { content: [{ type: "text" as const, text }], details: data };
|
|
11
|
+
}
|