Skip to content

Commit a0dbd39

Browse files
committed
feat(paperreview): add paperreview.ai adapter
1 parent ed89157 commit a0dbd39

13 files changed

Lines changed: 856 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Run `opencli list` for the live registry.
146146
| **devto** | `top` `tag` `user` | Public |
147147
| **dictionary** | `search` `synonyms` `examples` | Public |
148148
| **arxiv** | `search` `paper` | Public |
149+
| **paperreview** | `submit` `review` `feedback` | Public |
149150
| **wikipedia** | `search` `summary` `random` `trending` | Public |
150151
| **hackernews** | `top` `new` `best` `ask` `show` `jobs` `search` `user` | Public |
151152
| **jd** | `item` | Browser |

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ npm install -g @jackwener/opencli@latest
148148
| **devto** | `top` `tag` `user` | 公开 |
149149
| **dictionary** | `search` `synonyms` `examples` | 公开 |
150150
| **arxiv** | `search` `paper` | 公开 |
151+
| **paperreview** | `submit` `review` `feedback` | 公开 |
151152
| **wikipedia** | `search` `summary` `random` `trending` | 公开 |
152153
| **hackernews** | `top` `new` `best` `ask` `show` `jobs` `search` `user` | 公共 API |
153154
| **jd** | `item` | 浏览器 |

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default defineConfig({
9999
{ text: 'Xiaoyuzhou', link: '/adapters/browser/xiaoyuzhou' },
100100
{ text: 'Yahoo Finance', link: '/adapters/browser/yahoo-finance' },
101101
{ text: 'arXiv', link: '/adapters/browser/arxiv' },
102+
{ text: 'paperreview.ai', link: '/adapters/browser/paperreview' },
102103
{ text: 'Barchart', link: '/adapters/browser/barchart' },
103104
{ text: 'Hugging Face', link: '/adapters/browser/hf' },
104105
{ text: 'Sina Finance', link: '/adapters/browser/sinafinance' },
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# paperreview.ai
2+
3+
**Mode**: 🌐 Public · **Domain**: `paperreview.ai`
4+
5+
## Commands
6+
7+
| Command | Description |
8+
|---------|-------------|
9+
| `opencli paperreview submit` | Submit a PDF to paperreview.ai for review |
10+
| `opencli paperreview review` | Fetch a review by token |
11+
| `opencli paperreview feedback` | Send feedback on a completed review |
12+
13+
## Usage Examples
14+
15+
```bash
16+
# Validate a local PDF without uploading it
17+
opencli paperreview submit ./paper.pdf --email you@example.com --venue RAL --dry-run true
18+
19+
# Request an upload slot but stop before the actual upload
20+
opencli paperreview submit ./paper.pdf --email you@example.com --venue RAL --prepare-only true
21+
22+
# Submit a paper for review
23+
opencli paperreview submit ./paper.pdf --email you@example.com --venue RAL -f json
24+
25+
# Check the review status or fetch the final review
26+
opencli paperreview review tok_123 -f json
27+
28+
# Submit feedback on the review quality
29+
opencli paperreview feedback tok_123 --helpfulness 4 --critical-error no --actionable-suggestions yes
30+
```
31+
32+
## Prerequisites
33+
34+
- No browser required — uses public paperreview.ai endpoints
35+
- The input file must be a local `.pdf`
36+
- paperreview.ai currently rejects files larger than `10MB`
37+
- `submit` requires `--email`; `--venue` is optional
38+
39+
## Notes
40+
41+
- `submit` returns both the review token and the review URL when submission succeeds
42+
- `review` returns `processing` until the paperreview.ai result is ready
43+
- `feedback` expects `yes` / `no` values for `--critical-error` and `--actionable-suggestions`

docs/adapters/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Run `opencli list` for the live registry.
5555
| **[xiaoyuzhou](/adapters/browser/xiaoyuzhou)** | `podcast` `podcast-episodes` `episode` | 🌐 Public |
5656
| **[yahoo-finance](/adapters/browser/yahoo-finance)** | `quote` | 🌐 Public |
5757
| **[arxiv](/adapters/browser/arxiv)** | `search` `paper` | 🌐 Public |
58+
| **[paperreview](/adapters/browser/paperreview)** | `submit` `review` `feedback` | 🌐 Public |
5859
| **[barchart](/adapters/browser/barchart)** | `quote` `options` `greeks` `flow` | 🌐 Public |
5960
| **[hf](/adapters/browser/hf)** | `top` | 🌐 Public |
6061
| **[sinafinance](/adapters/browser/sinafinance)** | `news` | 🌐 Public |
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const {
4+
mockReadPdfFile,
5+
mockRequestJson,
6+
mockUploadPresignedPdf,
7+
mockValidateHelpfulness,
8+
mockParseYesNo,
9+
} = vi.hoisted(() => ({
10+
mockReadPdfFile: vi.fn(),
11+
mockRequestJson: vi.fn(),
12+
mockUploadPresignedPdf: vi.fn(),
13+
mockValidateHelpfulness: vi.fn(),
14+
mockParseYesNo: vi.fn(),
15+
}));
16+
17+
vi.mock('./utils.js', async () => {
18+
const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
19+
return {
20+
...actual,
21+
readPdfFile: mockReadPdfFile,
22+
requestJson: mockRequestJson,
23+
uploadPresignedPdf: mockUploadPresignedPdf,
24+
validateHelpfulness: mockValidateHelpfulness,
25+
parseYesNo: mockParseYesNo,
26+
};
27+
});
28+
29+
import { getRegistry } from '../../registry.js';
30+
import './submit.js';
31+
import './review.js';
32+
import './feedback.js';
33+
34+
describe('paperreview submit command', () => {
35+
beforeEach(() => {
36+
mockReadPdfFile.mockReset();
37+
mockRequestJson.mockReset();
38+
mockUploadPresignedPdf.mockReset();
39+
mockValidateHelpfulness.mockReset();
40+
mockParseYesNo.mockReset();
41+
});
42+
43+
it('supports dry run without any remote request', async () => {
44+
const cmd = getRegistry().get('paperreview/submit');
45+
expect(cmd?.func).toBeTypeOf('function');
46+
47+
mockReadPdfFile.mockResolvedValue({
48+
buffer: Buffer.from('%PDF'),
49+
fileName: 'paper.pdf',
50+
resolvedPath: '/tmp/paper.pdf',
51+
sizeBytes: 4096,
52+
});
53+
54+
const result = await cmd!.func!(null as any, {
55+
pdf: './paper.pdf',
56+
email: 'wang2629651228@gmail.com',
57+
venue: 'RAL',
58+
'dry-run': true,
59+
'prepare-only': false,
60+
});
61+
62+
expect(mockRequestJson).not.toHaveBeenCalled();
63+
expect(result).toMatchObject({
64+
status: 'dry-run',
65+
file: 'paper.pdf',
66+
email: 'wang2629651228@gmail.com',
67+
venue: 'RAL',
68+
});
69+
});
70+
71+
it('requests an upload URL, uploads the PDF, and confirms the submission', async () => {
72+
const cmd = getRegistry().get('paperreview/submit');
73+
expect(cmd?.func).toBeTypeOf('function');
74+
75+
mockReadPdfFile.mockResolvedValue({
76+
buffer: Buffer.from('%PDF'),
77+
fileName: 'paper.pdf',
78+
resolvedPath: '/tmp/paper.pdf',
79+
sizeBytes: 4096,
80+
});
81+
mockRequestJson
82+
.mockResolvedValueOnce({
83+
response: { ok: true, status: 200 } as Response,
84+
payload: {
85+
success: true,
86+
presigned_url: 'https://upload.example.com',
87+
presigned_fields: { key: 'uploads/paper.pdf' },
88+
s3_key: 'uploads/paper.pdf',
89+
},
90+
})
91+
.mockResolvedValueOnce({
92+
response: { ok: true, status: 200 } as Response,
93+
payload: {
94+
success: true,
95+
token: 'tok_123',
96+
message: 'Submission accepted',
97+
},
98+
});
99+
100+
const result = await cmd!.func!(null as any, {
101+
pdf: './paper.pdf',
102+
email: 'wang2629651228@gmail.com',
103+
venue: 'RAL',
104+
'dry-run': false,
105+
'prepare-only': false,
106+
});
107+
108+
expect(mockRequestJson).toHaveBeenNthCalledWith(1, '/api/get-upload-url', expect.objectContaining({
109+
method: 'POST',
110+
body: JSON.stringify({
111+
filename: 'paper.pdf',
112+
venue: 'RAL',
113+
}),
114+
}));
115+
expect(mockUploadPresignedPdf).toHaveBeenCalledWith(
116+
'https://upload.example.com',
117+
expect.objectContaining({ fileName: 'paper.pdf' }),
118+
expect.objectContaining({ s3_key: 'uploads/paper.pdf' }),
119+
);
120+
expect(mockRequestJson).toHaveBeenNthCalledWith(2, '/api/confirm-upload', expect.objectContaining({
121+
method: 'POST',
122+
body: expect.any(FormData),
123+
}));
124+
expect(result).toMatchObject({
125+
status: 'submitted',
126+
token: 'tok_123',
127+
review_url: 'https://paperreview.ai/review?token=tok_123',
128+
});
129+
});
130+
});
131+
132+
describe('paperreview review command', () => {
133+
beforeEach(() => {
134+
mockRequestJson.mockReset();
135+
});
136+
137+
it('returns processing status when the review is not ready yet', async () => {
138+
const cmd = getRegistry().get('paperreview/review');
139+
expect(cmd?.func).toBeTypeOf('function');
140+
141+
mockRequestJson.mockResolvedValue({
142+
response: { status: 202 } as Response,
143+
payload: { detail: 'Review is still processing.' },
144+
});
145+
146+
const result = await cmd!.func!(null as any, { token: 'tok_123' });
147+
148+
expect(result).toMatchObject({
149+
status: 'processing',
150+
token: 'tok_123',
151+
review_url: 'https://paperreview.ai/review?token=tok_123',
152+
message: 'Review is still processing.',
153+
});
154+
});
155+
});
156+
157+
describe('paperreview feedback command', () => {
158+
beforeEach(() => {
159+
mockRequestJson.mockReset();
160+
mockValidateHelpfulness.mockReset();
161+
mockParseYesNo.mockReset();
162+
});
163+
164+
it('normalizes feedback inputs and posts them to the API', async () => {
165+
const cmd = getRegistry().get('paperreview/feedback');
166+
expect(cmd?.func).toBeTypeOf('function');
167+
168+
mockValidateHelpfulness.mockReturnValue(4);
169+
mockParseYesNo.mockReturnValueOnce(true).mockReturnValueOnce(false);
170+
mockRequestJson.mockResolvedValue({
171+
response: { ok: true, status: 200 } as Response,
172+
payload: { message: 'Thanks for the feedback.' },
173+
});
174+
175+
const result = await cmd!.func!(null as any, {
176+
token: 'tok_123',
177+
helpfulness: 4,
178+
'critical-error': 'yes',
179+
'actionable-suggestions': 'no',
180+
'additional-comments': 'Helpful summary, but the contribution section needs more detail.',
181+
});
182+
183+
expect(mockRequestJson).toHaveBeenCalledWith('/api/feedback/tok_123', {
184+
method: 'POST',
185+
headers: { 'Content-Type': 'application/json' },
186+
body: JSON.stringify({
187+
helpfulness: 4,
188+
has_critical_error: true,
189+
has_actionable_suggestions: false,
190+
additional_comments: 'Helpful summary, but the contribution section needs more detail.',
191+
}),
192+
});
193+
expect(result).toMatchObject({
194+
status: 'submitted',
195+
token: 'tok_123',
196+
helpfulness: 4,
197+
critical_error: true,
198+
actionable_suggestions: false,
199+
message: 'Thanks for the feedback.',
200+
});
201+
});
202+
});

src/clis/paperreview/feedback.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { cli, Strategy } from '../../registry.js';
2+
import { CliError } from '../../errors.js';
3+
import {
4+
PAPERREVIEW_DOMAIN,
5+
ensureSuccess,
6+
parseYesNo,
7+
requestJson,
8+
summarizeFeedback,
9+
validateHelpfulness,
10+
} from './utils.js';
11+
12+
cli({
13+
site: 'paperreview',
14+
name: 'feedback',
15+
description: 'Submit feedback for a paperreview.ai review token',
16+
domain: PAPERREVIEW_DOMAIN,
17+
strategy: Strategy.PUBLIC,
18+
browser: false,
19+
timeoutSeconds: 30,
20+
args: [
21+
{ name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
22+
{ name: 'helpfulness', required: true, type: 'int', help: 'Helpfulness score from 1 to 5' },
23+
{ name: 'critical-error', required: true, choices: ['yes', 'no'], help: 'Whether the review contains a critical error' },
24+
{ name: 'actionable-suggestions', required: true, choices: ['yes', 'no'], help: 'Whether the review contains actionable suggestions' },
25+
{ name: 'additional-comments', help: 'Optional free-text feedback' },
26+
],
27+
columns: ['status', 'token', 'helpfulness', 'critical_error', 'actionable_suggestions', 'message'],
28+
func: async (_page, kwargs) => {
29+
const token = String(kwargs.token ?? '').trim();
30+
if (!token) {
31+
throw new CliError('ARGUMENT', 'A review token is required.');
32+
}
33+
34+
const helpfulness = validateHelpfulness(kwargs.helpfulness);
35+
const criticalError = parseYesNo(kwargs['critical-error'], 'critical-error');
36+
const actionableSuggestions = parseYesNo(kwargs['actionable-suggestions'], 'actionable-suggestions');
37+
const comments = String(kwargs['additional-comments'] ?? '').trim();
38+
39+
const payload: Record<string, unknown> = {
40+
helpfulness,
41+
has_critical_error: criticalError,
42+
has_actionable_suggestions: actionableSuggestions,
43+
};
44+
if (comments) {
45+
payload.additional_comments = comments;
46+
}
47+
48+
const { response, payload: responsePayload } = await requestJson(`/api/feedback/${encodeURIComponent(token)}`, {
49+
method: 'POST',
50+
headers: { 'Content-Type': 'application/json' },
51+
body: JSON.stringify(payload),
52+
});
53+
ensureSuccess(response, responsePayload, 'Failed to submit feedback.', 'Check the token and try again');
54+
55+
return summarizeFeedback({
56+
token,
57+
helpfulness,
58+
criticalError,
59+
actionableSuggestions,
60+
comments,
61+
payload: responsePayload,
62+
});
63+
},
64+
});

src/clis/paperreview/review.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { cli, Strategy } from '../../registry.js';
2+
import { CliError } from '../../errors.js';
3+
import {
4+
PAPERREVIEW_DOMAIN,
5+
buildReviewUrl,
6+
ensureSuccess,
7+
requestJson,
8+
summarizeReview,
9+
} from './utils.js';
10+
11+
cli({
12+
site: 'paperreview',
13+
name: 'review',
14+
description: 'Fetch a paperreview.ai review by token',
15+
domain: PAPERREVIEW_DOMAIN,
16+
strategy: Strategy.PUBLIC,
17+
browser: false,
18+
timeoutSeconds: 30,
19+
args: [
20+
{ name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
21+
],
22+
columns: ['status', 'title', 'venue', 'numerical_score', 'has_feedback', 'review_url'],
23+
func: async (_page, kwargs) => {
24+
const token = String(kwargs.token ?? '').trim();
25+
if (!token) {
26+
throw new CliError('ARGUMENT', 'A review token is required.');
27+
}
28+
29+
const { response, payload } = await requestJson(`/api/review/${encodeURIComponent(token)}`);
30+
31+
if (response.status === 202) {
32+
return {
33+
status: 'processing',
34+
token,
35+
review_url: buildReviewUrl(token),
36+
title: '',
37+
venue: '',
38+
numerical_score: '',
39+
has_feedback: '',
40+
message: typeof payload === 'object' && payload ? payload.detail ?? 'Review is still processing.' : 'Review is still processing.',
41+
};
42+
}
43+
44+
ensureSuccess(response, payload, 'Failed to fetch the review.', 'Check the token and try again');
45+
return summarizeReview(token, payload);
46+
},
47+
});

0 commit comments

Comments
 (0)