Spaces:
Running
Running
DVampire
commited on
Commit
·
583741e
1
Parent(s):
bf5c0e0
update website
Browse files- DATABASE_MIGRATION_SUMMARY.md +147 -0
- DATABASE_USAGE.md +182 -0
- Dockerfile +1 -1
- PROJECT_STRUCTURE.md +87 -0
- agents/__init__.py +0 -2
- app.py +715 -6
- cli.py +5 -58
- configs/paper_agent.py +5 -0
- frontend/index.html +12 -3
- frontend/main.js +611 -77
- frontend/paper.html +1 -1
- frontend/paper.js +29 -9
- frontend/styles.css +399 -18
- server.py +0 -731
- src/__init__.py +1 -0
- src/agents/__init__.py +3 -0
- {agents → src/agents}/evaluator.py +138 -45
- {agents → src/agents}/prompt.py +0 -11
- src/cli/__init__.py +1 -0
- src/cli/cli.py +80 -0
- src/config/config.py +4 -51
- src/crawl/__init__.py +5 -0
- src/crawl/huggingface_daily.py +309 -0
- src/database/db.py +138 -1
- src/logger/__init__.py +3 -6
- src/logger/log.py +136 -0
- src/logger/logger.py +0 -229
- src/utils/__init__.py +1 -1
- src/utils/hf_utils.py +0 -0
- test_evaluation.py +181 -0
- workdir/2508.05629.json +0 -57
- papers_cache.db → workdir/paper_agent/papers_cache.db +2 -2
DATABASE_MIGRATION_SUMMARY.md
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 数据库迁移完成总结
|
2 |
+
|
3 |
+
## 概述
|
4 |
+
|
5 |
+
已成功将系统从JSON文件存储迁移到SQLite数据库存储,现在每篇arXiv文章的评价内容都存储在数据库中,支持更好的数据管理和查询功能。
|
6 |
+
|
7 |
+
## 主要修改
|
8 |
+
|
9 |
+
### 1. 数据库结构 (`src/database/db.py`)
|
10 |
+
|
11 |
+
**新增 papers 表:**
|
12 |
+
- `arxiv_id`: 论文唯一标识
|
13 |
+
- `title`, `authors`, `abstract`: 论文基本信息
|
14 |
+
- `evaluation_content`: 评价内容(JSON格式)
|
15 |
+
- `evaluation_score`: 总体自动化评分
|
16 |
+
- `evaluation_tags`: 评价标签
|
17 |
+
- `is_evaluated`: 评价状态标记
|
18 |
+
- `evaluation_date`: 评价时间
|
19 |
+
- `created_at`, `updated_at`: 时间戳
|
20 |
+
|
21 |
+
**新增数据库方法:**
|
22 |
+
- `insert_paper()`: 插入新论文
|
23 |
+
- `get_paper()`: 获取单个论文
|
24 |
+
- `update_paper_evaluation()`: 更新评价内容
|
25 |
+
- `get_evaluated_papers()`: 获取已评价论文
|
26 |
+
- `get_unevaluated_papers()`: 获取未评价论文
|
27 |
+
- `search_papers()`: 搜索论文
|
28 |
+
- `get_papers_count()`: 获取统计信息
|
29 |
+
|
30 |
+
### 2. 评价器修改 (`src/agents/evaluator.py`)
|
31 |
+
|
32 |
+
**ConversationState 类:**
|
33 |
+
- 添加 `arxiv_id` 字段
|
34 |
+
|
35 |
+
**save_node 函数:**
|
36 |
+
- 改为保存到数据库而不是JSON文件
|
37 |
+
- 自动提取评分和标签信息
|
38 |
+
- 支持结构化数据存储
|
39 |
+
|
40 |
+
**run_evaluation 函数:**
|
41 |
+
- 添加 `arxiv_id` 参数支持
|
42 |
+
|
43 |
+
### 3. API接口修改 (`app.py`)
|
44 |
+
|
45 |
+
**修改的接口:**
|
46 |
+
- `/api/evals`: 从数据库获取评价列表
|
47 |
+
- `/api/has-eval/{paper_id}`: 检查数据库中的评价状态
|
48 |
+
- `/api/eval/{paper_id}`: 从数据库获取评价内容
|
49 |
+
|
50 |
+
**新增接口:**
|
51 |
+
- `/api/papers/status`: 获取论文统计信息
|
52 |
+
- `/api/papers/insert`: 插入新论文
|
53 |
+
- `/api/papers/evaluate/{arxiv_id}`: 评价论文
|
54 |
+
|
55 |
+
### 4. CLI工具修改 (`src/cli/cli.py`)
|
56 |
+
|
57 |
+
**新增参数:**
|
58 |
+
- `--arxiv-id`: 指定论文的arXiv ID
|
59 |
+
|
60 |
+
**功能增强:**
|
61 |
+
- 支持将评价结果保存到数据库
|
62 |
+
- 保持向后兼容性(仍可保存到文件)
|
63 |
+
|
64 |
+
## 使用示例
|
65 |
+
|
66 |
+
### 1. 使用CLI评价论文并保存到数据库
|
67 |
+
|
68 |
+
```bash
|
69 |
+
# 评价论文并保存到数据库
|
70 |
+
python cli.py https://arxiv.org/pdf/2508.05629 --arxiv-id 2508.05629
|
71 |
+
|
72 |
+
# 同时保存到文件和数据库
|
73 |
+
python cli.py https://arxiv.org/pdf/2508.05629 --arxiv-id 2508.05629 -o /path/to/output
|
74 |
+
```
|
75 |
+
|
76 |
+
### 2. 使用API插入论文
|
77 |
+
|
78 |
+
```bash
|
79 |
+
curl -X POST "http://localhost:8000/api/papers/insert" \
|
80 |
+
-H "Content-Type: application/json" \
|
81 |
+
-d '{
|
82 |
+
"arxiv_id": "2508.05629",
|
83 |
+
"title": "Your Paper Title",
|
84 |
+
"authors": "Author 1, Author 2",
|
85 |
+
"abstract": "Paper abstract...",
|
86 |
+
"categories": "cs.AI, cs.LG",
|
87 |
+
"published_date": "2024-08-01"
|
88 |
+
}'
|
89 |
+
```
|
90 |
+
|
91 |
+
### 3. 获取评价统计
|
92 |
+
|
93 |
+
```bash
|
94 |
+
curl "http://localhost:8000/api/papers/status"
|
95 |
+
```
|
96 |
+
|
97 |
+
## 数据库优势
|
98 |
+
|
99 |
+
1. **结构化存储**: 论文信息和评价内容分离,便于管理
|
100 |
+
2. **状态跟踪**: 通过 `is_evaluated` 字段跟踪评价状态
|
101 |
+
3. **标签系统**: 支持为评价添加标签,便于分类筛选
|
102 |
+
4. **搜索功能**: 支持按标题、作者、摘要搜索
|
103 |
+
5. **统计功能**: 轻松获取论文统计信息
|
104 |
+
6. **API支持**: 完整的RESTful API接口
|
105 |
+
7. **数据完整性**: SQLite提供ACID特性
|
106 |
+
|
107 |
+
## 迁移注意事项
|
108 |
+
|
109 |
+
1. **现有JSON文件**: 可以编写脚本将现有JSON文件导入数据库
|
110 |
+
2. **数据库备份**: 建议定期备份数据库文件
|
111 |
+
3. **向后兼容**: CLI工具仍支持保存到文件,保持兼容性
|
112 |
+
4. **配置路径**: 数据库文件路径在 `configs/paper_agent.py` 中配置
|
113 |
+
|
114 |
+
## 测试验证
|
115 |
+
|
116 |
+
已创建并运行测试脚本验证所有数据库功能:
|
117 |
+
- ✅ 论文插入
|
118 |
+
- ✅ 论文查询
|
119 |
+
- ✅ 评价更新
|
120 |
+
- ✅ 状态检查
|
121 |
+
- ✅ 统计功能
|
122 |
+
- ✅ 搜索功能
|
123 |
+
|
124 |
+
## 下一步建议
|
125 |
+
|
126 |
+
1. **数据迁移**: 编写脚本将现有JSON文件导入数据库
|
127 |
+
2. **前端更新**: 更新前端界面以支持新的数据库功能
|
128 |
+
3. **批量操作**: 添加批量论文插入和评价功能
|
129 |
+
4. **数据导出**: 添加数据导出功能
|
130 |
+
5. **性能优化**: 为大量数据添加索引优化
|
131 |
+
|
132 |
+
## 文件清单
|
133 |
+
|
134 |
+
**修改的文件:**
|
135 |
+
- `src/database/db.py` - 数据库结构和操作
|
136 |
+
- `src/agents/evaluator.py` - 评价器修改
|
137 |
+
- `app.py` - API接口修改
|
138 |
+
- `src/cli/cli.py` - CLI工具修改
|
139 |
+
|
140 |
+
**新增的文件:**
|
141 |
+
- `DATABASE_USAGE.md` - 使用说明文档
|
142 |
+
- `DATABASE_MIGRATION_SUMMARY.md` - 本总结文档
|
143 |
+
|
144 |
+
**配置文件:**
|
145 |
+
- `configs/paper_agent.py` - 数据库路径配置
|
146 |
+
|
147 |
+
现在系统已经完全支持数据库存储,可以更好地管理论文评价数据!
|
DATABASE_USAGE.md
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Papers Database 使用说明
|
2 |
+
|
3 |
+
## 概述
|
4 |
+
|
5 |
+
现在系统已经支持将arXiv文章和评价内容存储到SQLite数据库中,而不是保存在JSON文件中。这样可以更好地管理论文数据,支持查询、统计和标签管理。
|
6 |
+
|
7 |
+
## 数据库结构
|
8 |
+
|
9 |
+
### papers 表
|
10 |
+
|
11 |
+
| 字段 | 类型 | 说明 |
|
12 |
+
|------|------|------|
|
13 |
+
| arxiv_id | TEXT PRIMARY KEY | arXiv论文ID |
|
14 |
+
| title | TEXT NOT NULL | 论文标题 |
|
15 |
+
| authors | TEXT NOT NULL | 作者列表 |
|
16 |
+
| abstract | TEXT | 论文摘要 |
|
17 |
+
| categories | TEXT | 论文分类 |
|
18 |
+
| published_date | TEXT | 发布日期 |
|
19 |
+
| evaluation_content | TEXT | 评价内容(JSON格式) |
|
20 |
+
| evaluation_score | REAL | 总体自动化评分 |
|
21 |
+
| evaluation_tags | TEXT | 评价标签 |
|
22 |
+
| is_evaluated | BOOLEAN | 是否已评价 |
|
23 |
+
| evaluation_date | TIMESTAMP | 评价日期 |
|
24 |
+
| created_at | TIMESTAMP | 创建时间 |
|
25 |
+
| updated_at | TIMESTAMP | 更新时间 |
|
26 |
+
|
27 |
+
## 使用方法
|
28 |
+
|
29 |
+
### 1. 插入论文
|
30 |
+
|
31 |
+
```python
|
32 |
+
from src.database.db import db
|
33 |
+
|
34 |
+
# 插入新论文
|
35 |
+
db.insert_paper(
|
36 |
+
arxiv_id="2508.05629",
|
37 |
+
title="Your Paper Title",
|
38 |
+
authors="Author 1, Author 2",
|
39 |
+
abstract="Paper abstract...",
|
40 |
+
categories="cs.AI, cs.LG",
|
41 |
+
published_date="2024-08-01"
|
42 |
+
)
|
43 |
+
```
|
44 |
+
|
45 |
+
### 2. 更新评价
|
46 |
+
|
47 |
+
```python
|
48 |
+
# 更新论文评价
|
49 |
+
db.update_paper_evaluation(
|
50 |
+
arxiv_id="2508.05629",
|
51 |
+
evaluation_content='{"overall_automatability": 3, "three_year_feasibility": 75}',
|
52 |
+
evaluation_score=3.0,
|
53 |
+
evaluation_tags="3yr_feasibility:75%,overall_automatability:3/4"
|
54 |
+
)
|
55 |
+
```
|
56 |
+
|
57 |
+
### 3. 查询论文
|
58 |
+
|
59 |
+
```python
|
60 |
+
# 获取单个论文
|
61 |
+
paper = db.get_paper("2508.05629")
|
62 |
+
|
63 |
+
# 获取所有已评价的论文
|
64 |
+
evaluated_papers = db.get_evaluated_papers()
|
65 |
+
|
66 |
+
# 获取所有未评价的论文
|
67 |
+
unevaluated_papers = db.get_unevaluated_papers()
|
68 |
+
|
69 |
+
# 搜索论文
|
70 |
+
search_results = db.search_papers("AI")
|
71 |
+
```
|
72 |
+
|
73 |
+
### 4. 统计信息
|
74 |
+
|
75 |
+
```python
|
76 |
+
# 获取论文统计
|
77 |
+
count = db.get_papers_count()
|
78 |
+
print(f"总论文数: {count['total']}")
|
79 |
+
print(f"已评价: {count['evaluated']}")
|
80 |
+
print(f"未评价: {count['unevaluated']}")
|
81 |
+
```
|
82 |
+
|
83 |
+
## API 接口
|
84 |
+
|
85 |
+
### 获取评价列表
|
86 |
+
```
|
87 |
+
GET /api/evals
|
88 |
+
```
|
89 |
+
|
90 |
+
### 检查论文是否已评价
|
91 |
+
```
|
92 |
+
GET /api/has-eval/{paper_id}
|
93 |
+
```
|
94 |
+
|
95 |
+
### 获取论文评价
|
96 |
+
```
|
97 |
+
GET /api/eval/{paper_id}
|
98 |
+
```
|
99 |
+
|
100 |
+
### 获取论文统计
|
101 |
+
```
|
102 |
+
GET /api/papers/status
|
103 |
+
```
|
104 |
+
|
105 |
+
### 插入新论文
|
106 |
+
```
|
107 |
+
POST /api/papers/insert
|
108 |
+
Content-Type: application/json
|
109 |
+
|
110 |
+
{
|
111 |
+
"arxiv_id": "2508.05629",
|
112 |
+
"title": "Paper Title",
|
113 |
+
"authors": "Author 1, Author 2",
|
114 |
+
"abstract": "Abstract...",
|
115 |
+
"categories": "cs.AI",
|
116 |
+
"published_date": "2024-08-01"
|
117 |
+
}
|
118 |
+
```
|
119 |
+
|
120 |
+
### 评价论文
|
121 |
+
```
|
122 |
+
POST /api/papers/evaluate/{arxiv_id}
|
123 |
+
```
|
124 |
+
|
125 |
+
## CLI 工具使用
|
126 |
+
|
127 |
+
### 评价论文并保存到数据库
|
128 |
+
|
129 |
+
```bash
|
130 |
+
# 使用arxiv_id参数将评价保存到数据库
|
131 |
+
python cli.py https://arxiv.org/pdf/2508.05629 --arxiv-id 2508.05629
|
132 |
+
|
133 |
+
# 同时保存到文件和数据库
|
134 |
+
python cli.py https://arxiv.org/pdf/2508.05629 --arxiv-id 2508.05629 -o /path/to/output
|
135 |
+
```
|
136 |
+
|
137 |
+
## 迁移现有数据
|
138 |
+
|
139 |
+
如果你有现有的JSON评价文件,可以编写脚本将它们导入到数据库中:
|
140 |
+
|
141 |
+
```python
|
142 |
+
import json
|
143 |
+
import os
|
144 |
+
from src.database.db import db
|
145 |
+
|
146 |
+
def migrate_json_to_db(json_dir="workdir"):
|
147 |
+
"""将JSON文件迁移到数据库"""
|
148 |
+
for filename in os.listdir(json_dir):
|
149 |
+
if filename.endswith('.json'):
|
150 |
+
filepath = os.path.join(json_dir, filename)
|
151 |
+
with open(filepath, 'r') as f:
|
152 |
+
data = json.load(f)
|
153 |
+
|
154 |
+
# 提取arxiv_id(假设文件名包含arxiv_id)
|
155 |
+
arxiv_id = filename.split('_')[0] # 根据实际文件名格式调整
|
156 |
+
|
157 |
+
# 更新数据库中的评价
|
158 |
+
if 'response' in data:
|
159 |
+
db.update_paper_evaluation(
|
160 |
+
arxiv_id=arxiv_id,
|
161 |
+
evaluation_content=data['response'],
|
162 |
+
evaluation_score=None, # 需要从内容中解析
|
163 |
+
evaluation_tags=None
|
164 |
+
)
|
165 |
+
print(f"Migrated {filename} for paper {arxiv_id}")
|
166 |
+
```
|
167 |
+
|
168 |
+
## 优势
|
169 |
+
|
170 |
+
1. **结构化存储**: 论文信息和评价内容分开存储,便于查询
|
171 |
+
2. **标签系统**: 支持为评价添加标签,便于分类和筛选
|
172 |
+
3. **统计功能**: 可以轻松获取论文统计信息
|
173 |
+
4. **搜索功能**: 支持按标题、作者、摘要搜索论文
|
174 |
+
5. **状态管理**: 通过`is_evaluated`字段跟踪评价状态
|
175 |
+
6. **API支持**: 提供完整的RESTful API接口
|
176 |
+
|
177 |
+
## 注意事项
|
178 |
+
|
179 |
+
1. 确保在评价论文前先插入论文基本信息
|
180 |
+
2. 评价内容建议使用JSON格式,便于解析和展示
|
181 |
+
3. 定期备份数据库文件
|
182 |
+
4. 可以使用`evaluation_tags`字段存储关键评分信息,便于快速筛选
|
Dockerfile
CHANGED
@@ -10,4 +10,4 @@ COPY --chown=user ./requirements.txt requirements.txt
|
|
10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
11 |
|
12 |
COPY --chown=user . /app
|
13 |
-
CMD ["
|
|
|
10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
11 |
|
12 |
COPY --chown=user . /app
|
13 |
+
CMD ["python", "app.py"]
|
PROJECT_STRUCTURE.md
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# PaperIndex 项目结构
|
2 |
+
|
3 |
+
## 目录组织
|
4 |
+
|
5 |
+
```
|
6 |
+
paperindex/
|
7 |
+
├── app.py # 主应用程序入口点
|
8 |
+
├── cli.py # 命令行工具入口点
|
9 |
+
├── src/ # 源代码目录
|
10 |
+
│ ├── __init__.py
|
11 |
+
│ ├── app.py # 内部应用入口(已废弃)
|
12 |
+
│ ├── agents/ # AI 代理模块
|
13 |
+
│ │ ├── __init__.py
|
14 |
+
│ │ ├── evaluator.py # 论文评估器
|
15 |
+
│ │ └── prompt.py # 评估提示词
|
16 |
+
│ ├── database/ # 数据库模块
|
17 |
+
│ │ ├── __init__.py
|
18 |
+
│ │ ├── models.py # 数据库模型和类
|
19 |
+
│ │ └── papers_cache.db
|
20 |
+
│ ├── server/ # 服务器模块
|
21 |
+
│ │ ├── __init__.py
|
22 |
+
│ │ └── server.py # FastAPI 服务器
|
23 |
+
│ └── cli/ # 命令行工具模块
|
24 |
+
│ ├── __init__.py
|
25 |
+
│ └── cli.py # CLI 实现
|
26 |
+
├── frontend/ # 前端文件
|
27 |
+
│ ├── index.html
|
28 |
+
│ ├── paper.html
|
29 |
+
│ ├── main.js
|
30 |
+
│ ├── paper.js
|
31 |
+
│ └── styles.css
|
32 |
+
├── data/ # 数据目录
|
33 |
+
│ └── pdfs/
|
34 |
+
├── workdir/ # 工作目录
|
35 |
+
├── requirements.txt # Python 依赖
|
36 |
+
├── Dockerfile # Docker 配置
|
37 |
+
└── README.md # 项目说明
|
38 |
+
```
|
39 |
+
|
40 |
+
## 模块说明
|
41 |
+
|
42 |
+
### `src/agents/`
|
43 |
+
AI 代理模块,负责论文评估功能:
|
44 |
+
- `evaluator.py`: 使用 LangGraph 和 Claude API 进行论文评估
|
45 |
+
- `prompt.py`: 包含评估提示词和工具定义
|
46 |
+
|
47 |
+
### `src/database/`
|
48 |
+
数据库管理模块:
|
49 |
+
- `models.py`: 包含 PapersDatabase 类和数据库操作
|
50 |
+
- 包含 SQLite 数据库文件
|
51 |
+
- 负责论文缓存和状态管理
|
52 |
+
|
53 |
+
### `src/server/`
|
54 |
+
FastAPI 服务器模块:
|
55 |
+
- `server.py`: 主要的 Web 服务器实现
|
56 |
+
- 提供 RESTful API 接口
|
57 |
+
- 处理前端请求
|
58 |
+
|
59 |
+
### `src/cli/`
|
60 |
+
命令行工具模块:
|
61 |
+
- `cli.py`: 独立的论文评估命令行工具
|
62 |
+
- 支持本地 PDF 和在线 URL 评估
|
63 |
+
|
64 |
+
## 使用方法
|
65 |
+
|
66 |
+
### 启动 Web 应用
|
67 |
+
```bash
|
68 |
+
python app.py
|
69 |
+
```
|
70 |
+
|
71 |
+
### 使用命令行工具
|
72 |
+
```bash
|
73 |
+
python cli.py <pdf_path_or_url> [options]
|
74 |
+
```
|
75 |
+
|
76 |
+
### 开发模式
|
77 |
+
```bash
|
78 |
+
# 在 src 目录下运行
|
79 |
+
cd src
|
80 |
+
python -m uvicorn server.server:app --reload --host 0.0.0.0 --port 8000
|
81 |
+
```
|
82 |
+
|
83 |
+
## 导入路径
|
84 |
+
|
85 |
+
- 从根目录导入:`from src.agents.evaluator import Evaluator`
|
86 |
+
- 在 src 目录内导入:`from agents.evaluator import Evaluator`
|
87 |
+
- 模块间导入使用相对路径或绝对路径
|
agents/__init__.py
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
|
2 |
-
|
|
|
|
|
|
app.py
CHANGED
@@ -1,13 +1,722 @@
|
|
1 |
import os
|
2 |
import sys
|
|
|
|
|
|
|
3 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
# Add the current directory to Python path
|
6 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
7 |
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
if __name__ == "__main__":
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import sys
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
load_dotenv(verbose=True)
|
5 |
+
|
6 |
from pathlib import Path
|
7 |
+
import argparse
|
8 |
+
from mmengine import DictAction
|
9 |
+
from datetime import date, datetime, timedelta
|
10 |
+
from typing import Any, Dict, List, Optional
|
11 |
+
from fastapi.staticfiles import StaticFiles
|
12 |
+
from fastapi import FastAPI, HTTPException
|
13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
14 |
+
from fastapi.responses import FileResponse
|
15 |
+
import httpx
|
16 |
+
from bs4 import BeautifulSoup
|
17 |
+
import json
|
18 |
+
import asyncio
|
19 |
+
import uvicorn
|
20 |
+
|
21 |
+
root = str(Path(__file__).parent)
|
22 |
+
sys.path.append(root)
|
23 |
+
|
24 |
+
from src.database import db
|
25 |
+
from src.logger import logger
|
26 |
+
from src.config import config
|
27 |
+
from src.crawl import HuggingFaceDailyPapers
|
28 |
+
from src.utils import assemble_project_path
|
29 |
+
from src.agents.evaluator import run_evaluation
|
30 |
+
|
31 |
+
app = FastAPI(title="PaperAgent")
|
32 |
+
|
33 |
+
# Local development: allow same-origin and localhost
|
34 |
+
app.add_middleware(
|
35 |
+
CORSMiddleware,
|
36 |
+
allow_origins=["*"],
|
37 |
+
allow_credentials=True,
|
38 |
+
allow_methods=["*"],
|
39 |
+
allow_headers=["*"],
|
40 |
+
)
|
41 |
+
|
42 |
+
def parse_args():
|
43 |
+
parser = argparse.ArgumentParser(description='main')
|
44 |
+
parser.add_argument("--config", default=os.path.join(root, "configs", "paper_agent.py"), help="config file path")
|
45 |
+
|
46 |
+
parser.add_argument(
|
47 |
+
'--cfg-options',
|
48 |
+
nargs='+',
|
49 |
+
action=DictAction,
|
50 |
+
help='override some settings in the used config, the key-value pair '
|
51 |
+
'in xxx=yyy format will be merged into config file. If the value to '
|
52 |
+
'be overwritten is a list, it should be like key="[a,b]" or key=a,b '
|
53 |
+
'It also allows nested list/tuple values, e.g. key="[(a,b),(c,d)]" '
|
54 |
+
'Note that the quotation marks are necessary and that no white space '
|
55 |
+
'is allowed.')
|
56 |
+
args = parser.parse_args()
|
57 |
+
return args
|
58 |
+
|
59 |
+
# Remove the find_next_available_date function since we're using HuggingFace's redirect mechanism
|
60 |
+
|
61 |
+
|
62 |
+
@app.get("/api/daily")
|
63 |
+
async def get_daily(date_str: Optional[str] = None, direction: Optional[str] = None) -> Dict[str, Any]:
|
64 |
+
target_date = date_str or date.today().isoformat()
|
65 |
+
|
66 |
+
# Initialize HuggingFaceDailyPapers
|
67 |
+
hf_daily = HuggingFaceDailyPapers()
|
68 |
+
|
69 |
+
# First, check if we have fresh cache for the requested date
|
70 |
+
cached_data = db.get_cached_papers(target_date)
|
71 |
+
if cached_data and db.is_cache_fresh(target_date):
|
72 |
+
print(f"Using cached data for {target_date}")
|
73 |
+
return {
|
74 |
+
"date": target_date,
|
75 |
+
"requested_date": target_date,
|
76 |
+
"cards": cached_data['cards'],
|
77 |
+
"fallback_used": False,
|
78 |
+
"cached": True,
|
79 |
+
"cached_at": cached_data['cached_at']
|
80 |
+
}
|
81 |
+
|
82 |
+
# Handle different navigation directions
|
83 |
+
if direction == "prev":
|
84 |
+
# For previous navigation, use redirect mechanism to find the most recent available date
|
85 |
+
try:
|
86 |
+
actual_date, html = await hf_daily.fetch_daily_html(target_date)
|
87 |
+
print(f"Previous navigation: fetched {actual_date} (requested {target_date})")
|
88 |
+
|
89 |
+
# If we got redirected to a different date, that's our fallback
|
90 |
+
if actual_date != target_date:
|
91 |
+
print(f"Redirected from {target_date} to {actual_date}")
|
92 |
+
|
93 |
+
# Check if the redirected date has fresh cache
|
94 |
+
cached_data = db.get_cached_papers(actual_date)
|
95 |
+
if cached_data and db.is_cache_fresh(actual_date):
|
96 |
+
print(f"Using cached data for redirected date {actual_date}")
|
97 |
+
return {
|
98 |
+
"date": actual_date,
|
99 |
+
"requested_date": target_date,
|
100 |
+
"cards": cached_data['cards'],
|
101 |
+
"fallback_used": True,
|
102 |
+
"cached": True,
|
103 |
+
"cached_at": cached_data['cached_at']
|
104 |
+
}
|
105 |
+
|
106 |
+
# Process the HTML we got
|
107 |
+
cards = hf_daily.parse_daily_cards(html)
|
108 |
+
enriched_cards = await enrich_cards(cards)
|
109 |
+
|
110 |
+
# Cache the results for the redirected date
|
111 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
112 |
+
|
113 |
+
return {
|
114 |
+
"date": actual_date,
|
115 |
+
"requested_date": target_date,
|
116 |
+
"cards": enriched_cards,
|
117 |
+
"fallback_used": True,
|
118 |
+
"cached": False
|
119 |
+
}
|
120 |
+
|
121 |
+
# If we got the exact date we requested, process normally
|
122 |
+
cards = hf_daily.parse_daily_cards(html)
|
123 |
+
enriched_cards = await enrich_cards(cards)
|
124 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
125 |
+
|
126 |
+
return {
|
127 |
+
"date": actual_date,
|
128 |
+
"requested_date": target_date,
|
129 |
+
"cards": enriched_cards,
|
130 |
+
"fallback_used": False,
|
131 |
+
"cached": False
|
132 |
+
}
|
133 |
+
|
134 |
+
except Exception as e:
|
135 |
+
print(f"Failed to fetch {target_date} for previous navigation: {e}")
|
136 |
+
# Fallback to cached data if available
|
137 |
+
cached_data = db.get_cached_papers(target_date)
|
138 |
+
if cached_data:
|
139 |
+
return {
|
140 |
+
"date": target_date,
|
141 |
+
"requested_date": target_date,
|
142 |
+
"cards": cached_data['cards'],
|
143 |
+
"fallback_used": False,
|
144 |
+
"cached": True,
|
145 |
+
"cached_at": cached_data['cached_at']
|
146 |
+
}
|
147 |
+
raise HTTPException(status_code=503, detail="Unable to fetch papers and no cache available")
|
148 |
+
|
149 |
+
elif direction == "next":
|
150 |
+
# For next navigation, we need to find the next available date
|
151 |
+
# First try the exact date
|
152 |
+
try:
|
153 |
+
actual_date, html = await hf_daily.fetch_daily_html(target_date)
|
154 |
+
print(f"Next navigation: fetched {actual_date} (requested {target_date})")
|
155 |
+
|
156 |
+
# If we got the exact date we requested, that's perfect
|
157 |
+
if actual_date == target_date:
|
158 |
+
cards = hf_daily.parse_daily_cards(html)
|
159 |
+
enriched_cards = await enrich_cards(cards)
|
160 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
161 |
+
|
162 |
+
return {
|
163 |
+
"date": actual_date,
|
164 |
+
"requested_date": target_date,
|
165 |
+
"cards": enriched_cards,
|
166 |
+
"fallback_used": False,
|
167 |
+
"cached": False
|
168 |
+
}
|
169 |
+
|
170 |
+
# If we got redirected, it means the requested date doesn't exist
|
171 |
+
# We need to find the next available date by incrementing
|
172 |
+
print(f"Requested date {target_date} doesn't exist, searching for next available date")
|
173 |
+
|
174 |
+
# Try to find the next available date by incrementing
|
175 |
+
next_date = await find_next_available_date_forward(target_date)
|
176 |
+
if next_date:
|
177 |
+
cached_data = db.get_cached_papers(next_date)
|
178 |
+
if cached_data and db.is_cache_fresh(next_date):
|
179 |
+
print(f"Using cached data for next available date {next_date}")
|
180 |
+
return {
|
181 |
+
"date": next_date,
|
182 |
+
"requested_date": target_date,
|
183 |
+
"cards": cached_data['cards'],
|
184 |
+
"fallback_used": True,
|
185 |
+
"cached": True,
|
186 |
+
"cached_at": cached_data['cached_at']
|
187 |
+
}
|
188 |
+
|
189 |
+
# Fetch the next available date
|
190 |
+
actual_date, html = await hf_daily.fetch_daily_html(next_date)
|
191 |
+
cards = hf_daily.parse_daily_cards(html)
|
192 |
+
enriched_cards = await enrich_cards(cards)
|
193 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
194 |
+
|
195 |
+
return {
|
196 |
+
"date": actual_date,
|
197 |
+
"requested_date": target_date,
|
198 |
+
"cards": enriched_cards,
|
199 |
+
"fallback_used": True,
|
200 |
+
"cached": False
|
201 |
+
}
|
202 |
+
|
203 |
+
# If no next date found, return empty
|
204 |
+
return {
|
205 |
+
"date": target_date,
|
206 |
+
"requested_date": target_date,
|
207 |
+
"cards": [],
|
208 |
+
"fallback_used": False,
|
209 |
+
"cached": False
|
210 |
+
}
|
211 |
+
|
212 |
+
except Exception as e:
|
213 |
+
print(f"Failed to fetch {target_date} for next navigation: {e}")
|
214 |
+
# Try to find next available date
|
215 |
+
next_date = await find_next_available_date_forward(target_date)
|
216 |
+
if next_date:
|
217 |
+
cached_data = db.get_cached_papers(next_date)
|
218 |
+
if cached_data:
|
219 |
+
return {
|
220 |
+
"date": next_date,
|
221 |
+
"requested_date": target_date,
|
222 |
+
"cards": cached_data['cards'],
|
223 |
+
"fallback_used": True,
|
224 |
+
"cached": True,
|
225 |
+
"cached_at": cached_data['cached_at']
|
226 |
+
}
|
227 |
+
|
228 |
+
# If no cache available, return error
|
229 |
+
raise HTTPException(status_code=503, detail="Unable to fetch papers and no cache available")
|
230 |
+
|
231 |
+
else:
|
232 |
+
# No direction specified, try the exact date first
|
233 |
+
try:
|
234 |
+
actual_date, html = await hf_daily.fetch_daily_html(target_date)
|
235 |
+
print(f"Direct fetch: fetched {actual_date} (requested {target_date})")
|
236 |
+
|
237 |
+
# If we got redirected, that's our fallback
|
238 |
+
if actual_date != target_date:
|
239 |
+
print(f"Redirected from {target_date} to {actual_date}")
|
240 |
+
|
241 |
+
# Check if the redirected date has fresh cache
|
242 |
+
cached_data = db.get_cached_papers(actual_date)
|
243 |
+
if cached_data and db.is_cache_fresh(actual_date):
|
244 |
+
print(f"Using cached data for redirected date {actual_date}")
|
245 |
+
return {
|
246 |
+
"date": actual_date,
|
247 |
+
"requested_date": target_date,
|
248 |
+
"cards": cached_data['cards'],
|
249 |
+
"fallback_used": True,
|
250 |
+
"cached": True,
|
251 |
+
"cached_at": cached_data['cached_at']
|
252 |
+
}
|
253 |
+
|
254 |
+
# Process the HTML we got
|
255 |
+
cards = hf_daily.parse_daily_cards(html)
|
256 |
+
enriched_cards = await enrich_cards(cards)
|
257 |
+
|
258 |
+
# Cache the results for the redirected date
|
259 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
260 |
+
|
261 |
+
return {
|
262 |
+
"date": actual_date,
|
263 |
+
"requested_date": target_date,
|
264 |
+
"cards": enriched_cards,
|
265 |
+
"fallback_used": True,
|
266 |
+
"cached": False
|
267 |
+
}
|
268 |
+
|
269 |
+
# If we got the exact date we requested, process normally
|
270 |
+
cards = hf_daily.parse_daily_cards(html)
|
271 |
+
enriched_cards = await enrich_cards(cards)
|
272 |
+
db.cache_papers(actual_date, html, enriched_cards)
|
273 |
+
|
274 |
+
return {
|
275 |
+
"date": actual_date,
|
276 |
+
"requested_date": target_date,
|
277 |
+
"cards": enriched_cards,
|
278 |
+
"fallback_used": False,
|
279 |
+
"cached": False
|
280 |
+
}
|
281 |
+
|
282 |
+
except Exception as e:
|
283 |
+
print(f"Failed to fetch {target_date}: {e}")
|
284 |
+
|
285 |
+
# If everything fails, return cached data if available
|
286 |
+
cached_data = db.get_cached_papers(target_date)
|
287 |
+
if cached_data:
|
288 |
+
return {
|
289 |
+
"date": target_date,
|
290 |
+
"requested_date": target_date,
|
291 |
+
"cards": cached_data['cards'],
|
292 |
+
"fallback_used": False,
|
293 |
+
"cached": True,
|
294 |
+
"cached_at": cached_data['cached_at']
|
295 |
+
}
|
296 |
+
|
297 |
+
# If no cache available, return error
|
298 |
+
raise HTTPException(status_code=503, detail="Unable to fetch papers and no cache available")
|
299 |
+
|
300 |
+
|
301 |
+
async def find_next_available_date_forward(start_date: str, max_attempts: int = 30) -> Optional[str]:
|
302 |
+
"""Find the next available date by incrementing and checking"""
|
303 |
+
from datetime import datetime, timedelta
|
304 |
+
|
305 |
+
current_date = datetime.strptime(start_date, "%Y-%m-%d")
|
306 |
+
|
307 |
+
for i in range(max_attempts):
|
308 |
+
current_date += timedelta(days=1)
|
309 |
+
date_str = current_date.strftime("%Y-%m-%d")
|
310 |
+
|
311 |
+
# Check if we have cache for this date
|
312 |
+
cached_data = db.get_cached_papers(date_str)
|
313 |
+
if cached_data:
|
314 |
+
return date_str
|
315 |
+
|
316 |
+
# Try to fetch this date (but don't wait too long)
|
317 |
+
try:
|
318 |
+
import httpx
|
319 |
+
from src.crawl.huggingface_daily import HuggingFaceDailyPapers
|
320 |
+
|
321 |
+
hf_daily = HuggingFaceDailyPapers()
|
322 |
+
|
323 |
+
# Use a shorter timeout for quick checks
|
324 |
+
async with httpx.AsyncClient(timeout=5) as client:
|
325 |
+
actual_date, html = await hf_daily.fetch_daily_html(date_str)
|
326 |
+
if actual_date == date_str:
|
327 |
+
return date_str
|
328 |
+
|
329 |
+
except Exception as e:
|
330 |
+
print(f"Failed to check {date_str}: {e}")
|
331 |
+
continue
|
332 |
+
|
333 |
+
return None
|
334 |
+
|
335 |
+
|
336 |
+
async def enrich_cards(cards):
|
337 |
+
"""Enrich cards with paper details from database"""
|
338 |
+
for c in cards:
|
339 |
+
arxiv_id = c.get("arxiv_id")
|
340 |
+
if arxiv_id:
|
341 |
+
paper = db.get_paper(arxiv_id)
|
342 |
+
if paper:
|
343 |
+
# Add evaluation status
|
344 |
+
c["has_eval"] = paper.get('is_evaluated', False)
|
345 |
+
c["is_evaluated"] = paper.get('is_evaluated', False)
|
346 |
+
|
347 |
+
# Add evaluation details if available
|
348 |
+
if paper.get('is_evaluated'):
|
349 |
+
c["evaluation_score"] = paper.get('evaluation_score')
|
350 |
+
c["overall_score"] = paper.get('overall_score')
|
351 |
+
c["evaluation_date"] = paper.get('evaluation_date')
|
352 |
+
c["evaluation_tags"] = paper.get('evaluation_tags')
|
353 |
+
|
354 |
+
# Add paper details (use cached data as fallback)
|
355 |
+
if not c.get("title") and paper.get("title"):
|
356 |
+
c["title"] = paper["title"]
|
357 |
+
if not c.get("authors") and paper.get("authors"):
|
358 |
+
c["authors"] = paper["authors"]
|
359 |
+
if not c.get("abstract") and paper.get("abstract"):
|
360 |
+
c["abstract"] = paper["abstract"]
|
361 |
+
else:
|
362 |
+
c["has_eval"] = False
|
363 |
+
c["is_evaluated"] = False
|
364 |
+
else:
|
365 |
+
c["has_eval"] = False
|
366 |
+
c["is_evaluated"] = False
|
367 |
+
|
368 |
+
return cards
|
369 |
+
|
370 |
+
|
371 |
+
@app.get("/api/evals")
|
372 |
+
def list_evals() -> Dict[str, Any]:
|
373 |
+
# Get evaluated papers from database
|
374 |
+
evaluated_papers = db.get_evaluated_papers()
|
375 |
+
items: List[Dict[str, Any]] = []
|
376 |
+
|
377 |
+
for paper in evaluated_papers:
|
378 |
+
items.append({
|
379 |
+
"arxiv_id": paper['arxiv_id'],
|
380 |
+
"title": paper['title'],
|
381 |
+
"authors": paper['authors'],
|
382 |
+
"evaluation_date": paper['evaluation_date'],
|
383 |
+
"evaluation_score": paper['evaluation_score'],
|
384 |
+
"evaluation_tags": paper['evaluation_tags']
|
385 |
+
})
|
386 |
+
|
387 |
+
return {"count": len(items), "items": items}
|
388 |
+
|
389 |
+
|
390 |
+
@app.get("/api/has-eval/{paper_id}")
|
391 |
+
def has_eval(paper_id: str) -> Dict[str, bool]:
|
392 |
+
paper = db.get_paper(paper_id)
|
393 |
+
exists = paper is not None and paper.get('is_evaluated', False)
|
394 |
+
return {"exists": exists}
|
395 |
+
|
396 |
+
|
397 |
+
@app.get("/api/paper/{paper_id}")
|
398 |
+
def get_paper_details(paper_id: str) -> Dict[str, Any]:
|
399 |
+
"""Get detailed paper information from database"""
|
400 |
+
paper = db.get_paper(paper_id)
|
401 |
+
if not paper:
|
402 |
+
raise HTTPException(status_code=404, detail="Paper not found")
|
403 |
+
|
404 |
+
return {
|
405 |
+
"arxiv_id": paper.get('arxiv_id'),
|
406 |
+
"title": paper.get('title'),
|
407 |
+
"authors": paper.get('authors'),
|
408 |
+
"abstract": paper.get('abstract'),
|
409 |
+
"categories": paper.get('categories'),
|
410 |
+
"published_date": paper.get('published_date'),
|
411 |
+
"is_evaluated": paper.get('is_evaluated', False),
|
412 |
+
"evaluation_date": paper.get('evaluation_date'),
|
413 |
+
"created_at": paper.get('created_at'),
|
414 |
+
"updated_at": paper.get('updated_at')
|
415 |
+
}
|
416 |
+
|
417 |
+
|
418 |
+
@app.get("/api/paper-score/{paper_id}")
|
419 |
+
def get_paper_score(paper_id: str) -> Dict[str, Any]:
|
420 |
+
paper = db.get_paper(paper_id)
|
421 |
+
print(f"Paper data for {paper_id}:", paper)
|
422 |
+
|
423 |
+
if not paper or not paper.get('is_evaluated', False):
|
424 |
+
print(f"Paper {paper_id} not found or not evaluated")
|
425 |
+
return {"has_score": False}
|
426 |
+
|
427 |
+
# Calculate overall score as average of all dimensions (same as radar chart)
|
428 |
+
try:
|
429 |
+
evaluation_content = paper.get('evaluation_content')
|
430 |
+
if evaluation_content:
|
431 |
+
evaluation_json = json.loads(evaluation_content)
|
432 |
+
if 'scorecard' in evaluation_json:
|
433 |
+
scorecard = evaluation_json['scorecard']
|
434 |
+
values = [
|
435 |
+
scorecard.get('task_formalization', 0),
|
436 |
+
scorecard.get('data_resource_availability', 0),
|
437 |
+
scorecard.get('input_output_complexity', 0),
|
438 |
+
scorecard.get('real_world_interaction', 0),
|
439 |
+
scorecard.get('existing_ai_coverage', 0),
|
440 |
+
scorecard.get('human_originality', 0),
|
441 |
+
scorecard.get('safety_ethics', 0),
|
442 |
+
scorecard.get('technical_maturity_needed', 0),
|
443 |
+
scorecard.get('three_year_feasibility_pct', 0) / 25, # Convert percentage to 0-4 scale
|
444 |
+
scorecard.get('overall_automatability', 0)
|
445 |
+
]
|
446 |
+
valid_scores = [v for v in values if v > 0]
|
447 |
+
overall_score = sum(valid_scores) / len(valid_scores) if valid_scores else 0
|
448 |
+
print(f"Calculated overall score: {overall_score}")
|
449 |
+
|
450 |
+
return {
|
451 |
+
"has_score": True,
|
452 |
+
"score": overall_score,
|
453 |
+
"evaluation_date": paper.get('evaluation_date')
|
454 |
+
}
|
455 |
+
except Exception as e:
|
456 |
+
print(f"Error calculating overall score: {e}")
|
457 |
+
|
458 |
+
# Fallback to stored values
|
459 |
+
overall_score = paper.get('overall_score')
|
460 |
+
evaluation_score = paper.get('evaluation_score')
|
461 |
+
print(f"Fallback - Overall score: {overall_score}, Evaluation score: {evaluation_score}")
|
462 |
+
|
463 |
+
return {
|
464 |
+
"has_score": True,
|
465 |
+
"score": overall_score if overall_score is not None else evaluation_score,
|
466 |
+
"evaluation_date": paper.get('evaluation_date')
|
467 |
+
}
|
468 |
|
|
|
|
|
469 |
|
470 |
+
@app.get("/api/eval/{paper_id}")
|
471 |
+
def get_eval(paper_id: str) -> Any:
|
472 |
+
paper = db.get_paper(paper_id)
|
473 |
+
if not paper or not paper.get('is_evaluated', False):
|
474 |
+
raise HTTPException(status_code=404, detail="Evaluation not found")
|
475 |
+
|
476 |
+
# Parse evaluation content if it's JSON
|
477 |
+
evaluation_content = paper['evaluation_content']
|
478 |
+
try:
|
479 |
+
evaluation_json = json.loads(evaluation_content)
|
480 |
+
except json.JSONDecodeError:
|
481 |
+
# If not JSON, create a simple structure
|
482 |
+
evaluation_json = {
|
483 |
+
"evaluation_content": evaluation_content,
|
484 |
+
"arxiv_id": paper_id,
|
485 |
+
"evaluation_date": paper['evaluation_date'],
|
486 |
+
"evaluation_score": paper['evaluation_score'],
|
487 |
+
"evaluation_tags": paper['evaluation_tags']
|
488 |
+
}
|
489 |
+
|
490 |
+
return evaluation_json
|
491 |
+
|
492 |
+
|
493 |
+
@app.get("/api/available-dates")
|
494 |
+
def get_available_dates() -> Dict[str, Any]:
|
495 |
+
"""Get list of available dates in the cache"""
|
496 |
+
with db.get_connection() as conn:
|
497 |
+
cursor = conn.cursor()
|
498 |
+
cursor.execute('SELECT date_str FROM papers_cache ORDER BY date_str DESC LIMIT 30')
|
499 |
+
dates = [row['date_str'] for row in cursor.fetchall()]
|
500 |
+
|
501 |
+
return {
|
502 |
+
"available_dates": dates,
|
503 |
+
"count": len(dates)
|
504 |
+
}
|
505 |
+
|
506 |
+
|
507 |
+
@app.get("/api/cache/status")
|
508 |
+
def get_cache_status() -> Dict[str, Any]:
|
509 |
+
"""Get cache status and statistics"""
|
510 |
+
with db.get_connection() as conn:
|
511 |
+
cursor = conn.cursor()
|
512 |
+
|
513 |
+
# Get total cached dates
|
514 |
+
cursor.execute('SELECT COUNT(*) as count FROM papers_cache')
|
515 |
+
total_cached = cursor.fetchone()['count']
|
516 |
+
|
517 |
+
# Get latest cached date
|
518 |
+
cursor.execute('SELECT date_str, updated_at FROM latest_date WHERE id = 1')
|
519 |
+
latest_info = cursor.fetchone()
|
520 |
+
|
521 |
+
# Get cache age distribution
|
522 |
+
cursor.execute('''
|
523 |
+
SELECT
|
524 |
+
CASE
|
525 |
+
WHEN updated_at > datetime('now', '-1 hour') THEN '1 hour'
|
526 |
+
WHEN updated_at > datetime('now', '-24 hours') THEN '24 hours'
|
527 |
+
WHEN updated_at > datetime('now', '-7 days') THEN '7 days'
|
528 |
+
ELSE 'older'
|
529 |
+
END as age_group,
|
530 |
+
COUNT(*) as count
|
531 |
+
FROM papers_cache
|
532 |
+
GROUP BY age_group
|
533 |
+
''')
|
534 |
+
age_distribution = {row['age_group']: row['count'] for row in cursor.fetchall()}
|
535 |
+
|
536 |
+
return {
|
537 |
+
"total_cached_dates": total_cached,
|
538 |
+
"latest_cached_date": latest_info['date_str'] if latest_info else None,
|
539 |
+
"latest_updated": latest_info['updated_at'] if latest_info else None,
|
540 |
+
"age_distribution": age_distribution
|
541 |
+
}
|
542 |
+
|
543 |
+
|
544 |
+
@app.get("/api/papers/status")
|
545 |
+
def get_papers_status() -> Dict[str, Any]:
|
546 |
+
"""Get papers database status and statistics"""
|
547 |
+
papers_count = db.get_papers_count()
|
548 |
+
|
549 |
+
# Get recent evaluations
|
550 |
+
recent_papers = db.get_evaluated_papers()
|
551 |
+
recent_evaluations = []
|
552 |
+
for paper in recent_papers[:10]: # Get last 10 evaluations
|
553 |
+
recent_evaluations.append({
|
554 |
+
"arxiv_id": paper['arxiv_id'],
|
555 |
+
"title": paper['title'],
|
556 |
+
"evaluation_date": paper['evaluation_date'],
|
557 |
+
"evaluation_score": paper['evaluation_score']
|
558 |
+
})
|
559 |
+
|
560 |
+
return {
|
561 |
+
"papers_count": papers_count,
|
562 |
+
"recent_evaluations": recent_evaluations
|
563 |
+
}
|
564 |
+
|
565 |
+
|
566 |
+
@app.post("/api/papers/insert")
|
567 |
+
def insert_paper(paper_data: Dict[str, Any]) -> Dict[str, Any]:
|
568 |
+
"""Insert a new paper into the database"""
|
569 |
+
try:
|
570 |
+
required_fields = ['arxiv_id', 'title', 'authors']
|
571 |
+
for field in required_fields:
|
572 |
+
if field not in paper_data:
|
573 |
+
raise HTTPException(status_code=400, detail=f"Missing required field: {field}")
|
574 |
+
|
575 |
+
db.insert_paper(
|
576 |
+
arxiv_id=paper_data['arxiv_id'],
|
577 |
+
title=paper_data['title'],
|
578 |
+
authors=paper_data['authors'],
|
579 |
+
abstract=paper_data.get('abstract'),
|
580 |
+
categories=paper_data.get('categories'),
|
581 |
+
published_date=paper_data.get('published_date')
|
582 |
+
)
|
583 |
+
|
584 |
+
return {"message": f"Paper {paper_data['arxiv_id']} inserted successfully"}
|
585 |
+
except Exception as e:
|
586 |
+
raise HTTPException(status_code=500, detail=f"Failed to insert paper: {str(e)}")
|
587 |
+
|
588 |
+
|
589 |
+
@app.post("/api/papers/evaluate/{arxiv_id}")
|
590 |
+
async def evaluate_paper(arxiv_id: str) -> Dict[str, Any]:
|
591 |
+
"""Evaluate a paper by its arxiv_id"""
|
592 |
+
try:
|
593 |
+
# Check if paper exists in database
|
594 |
+
paper = db.get_paper(arxiv_id)
|
595 |
+
if not paper:
|
596 |
+
raise HTTPException(status_code=404, detail="Paper not found in database")
|
597 |
+
|
598 |
+
# Check if already evaluated
|
599 |
+
if paper.get('is_evaluated', False):
|
600 |
+
return {"message": f"Paper {arxiv_id} already evaluated", "status": "already_evaluated"}
|
601 |
+
|
602 |
+
# Create PDF URL from arxiv_id
|
603 |
+
pdf_url = f"https://arxiv.org/pdf/{arxiv_id}.pdf"
|
604 |
+
|
605 |
+
# Run evaluation in background task
|
606 |
+
async def run_eval():
|
607 |
+
try:
|
608 |
+
# Update paper status to "evaluating"
|
609 |
+
db.update_paper_status(arxiv_id, "evaluating")
|
610 |
+
logger.info(f"Started evaluation for {arxiv_id}")
|
611 |
+
|
612 |
+
result = await run_evaluation(
|
613 |
+
pdf_path=pdf_url,
|
614 |
+
arxiv_id=arxiv_id,
|
615 |
+
api_key=os.getenv("ANTHROPIC_API_KEY")
|
616 |
+
)
|
617 |
+
|
618 |
+
# Update paper status to "completed"
|
619 |
+
db.update_paper_status(arxiv_id, "completed")
|
620 |
+
logger.info(f"Evaluation completed for {arxiv_id}")
|
621 |
+
except Exception as e:
|
622 |
+
# Update paper status to "failed"
|
623 |
+
db.update_paper_status(arxiv_id, "failed")
|
624 |
+
logger.error(f"Evaluation failed for {arxiv_id}: {str(e)}")
|
625 |
+
|
626 |
+
# Start evaluation in background
|
627 |
+
asyncio.create_task(run_eval())
|
628 |
+
|
629 |
+
return {
|
630 |
+
"message": f"Evaluation started for paper {arxiv_id}",
|
631 |
+
"status": "started",
|
632 |
+
"pdf_url": pdf_url
|
633 |
+
}
|
634 |
+
except Exception as e:
|
635 |
+
raise HTTPException(status_code=500, detail=f"Failed to evaluate paper: {str(e)}")
|
636 |
+
|
637 |
+
|
638 |
+
@app.get("/api/papers/evaluate/{arxiv_id}/status")
|
639 |
+
def get_evaluation_status(arxiv_id: str) -> Dict[str, Any]:
|
640 |
+
"""Get evaluation status for a paper"""
|
641 |
+
try:
|
642 |
+
paper = db.get_paper(arxiv_id)
|
643 |
+
if not paper:
|
644 |
+
raise HTTPException(status_code=404, detail="Paper not found")
|
645 |
+
|
646 |
+
status = paper.get('evaluation_status', 'not_started')
|
647 |
+
is_evaluated = paper.get('is_evaluated', False)
|
648 |
+
|
649 |
+
return {
|
650 |
+
"arxiv_id": arxiv_id,
|
651 |
+
"status": status,
|
652 |
+
"is_evaluated": is_evaluated,
|
653 |
+
"evaluation_date": paper.get('evaluation_date'),
|
654 |
+
"evaluation_score": paper.get('evaluation_score')
|
655 |
+
}
|
656 |
+
except Exception as e:
|
657 |
+
raise HTTPException(status_code=500, detail=f"Failed to get evaluation status: {str(e)}")
|
658 |
+
|
659 |
+
|
660 |
+
@app.post("/api/cache/clear")
|
661 |
+
def clear_cache() -> Dict[str, str]:
|
662 |
+
"""Clear all cached data"""
|
663 |
+
with db.get_connection() as conn:
|
664 |
+
cursor = conn.cursor()
|
665 |
+
cursor.execute('DELETE FROM papers_cache')
|
666 |
+
conn.commit()
|
667 |
+
return {"message": "Cache cleared successfully"}
|
668 |
+
|
669 |
+
|
670 |
+
@app.post("/api/cache/refresh/{date_str}")
|
671 |
+
async def refresh_cache(date_str: str) -> Dict[str, Any]:
|
672 |
+
"""Force refresh cache for a specific date"""
|
673 |
+
try:
|
674 |
+
# Initialize HuggingFaceDailyPapers
|
675 |
+
hf_daily = HuggingFaceDailyPapers()
|
676 |
+
|
677 |
+
# Force fetch fresh data
|
678 |
+
actual_date, html = await hf_daily.fetch_daily_html(date_str)
|
679 |
+
cards = hf_daily.parse_daily_cards(html)
|
680 |
+
|
681 |
+
# Cache the results
|
682 |
+
db.cache_papers(actual_date, html, cards)
|
683 |
+
|
684 |
+
return {
|
685 |
+
"message": f"Cache refreshed for {actual_date}",
|
686 |
+
"cards_count": len(cards)
|
687 |
+
}
|
688 |
+
except Exception as e:
|
689 |
+
raise HTTPException(status_code=500, detail=f"Failed to refresh cache: {str(e)}")
|
690 |
+
|
691 |
+
|
692 |
+
@app.get("/styles.css")
|
693 |
+
async def get_styles():
|
694 |
+
"""Serve CSS with no-cache headers to prevent caching issues during development"""
|
695 |
+
response = FileResponse("frontend/styles.css", media_type="text/css")
|
696 |
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
697 |
+
response.headers["Pragma"] = "no-cache"
|
698 |
+
response.headers["Expires"] = "0"
|
699 |
+
return response
|
700 |
|
701 |
if __name__ == "__main__":
|
702 |
+
# Parse command line arguments
|
703 |
+
args = parse_args()
|
704 |
+
|
705 |
+
# Initialize the configuration
|
706 |
+
config.init_config(args.config, args)
|
707 |
+
|
708 |
+
# Initialize the logger
|
709 |
+
logger.init_logger(config=config)
|
710 |
+
logger.info(f"| Logger initialized at: {config.log_path}")
|
711 |
+
logger.info(f"| Config:\n{config.pretty_text}")
|
712 |
+
|
713 |
+
# Initialize the database
|
714 |
+
db.init_db(config=config)
|
715 |
+
logger.info(f"| Database initialized at: {config.db_path}")
|
716 |
+
|
717 |
+
# Load Frontend
|
718 |
+
os.makedirs(config.frontend_path, exist_ok=True)
|
719 |
+
app.mount("/", StaticFiles(directory=config.frontend_path, html=True), name="static")
|
720 |
+
logger.info(f"| Frontend initialized at: {config.frontend_path}")
|
721 |
+
|
722 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
cli.py
CHANGED
@@ -1,65 +1,12 @@
|
|
1 |
-
import argparse
|
2 |
import os
|
3 |
import sys
|
4 |
-
from
|
5 |
-
from dotenv import load_dotenv
|
6 |
-
load_dotenv()
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
from agents.evaluator import run_evaluation
|
12 |
-
|
13 |
-
|
14 |
-
console = Console()
|
15 |
-
|
16 |
-
|
17 |
-
def build_parser() -> argparse.ArgumentParser:
|
18 |
-
parser = argparse.ArgumentParser(
|
19 |
-
description="AI Automation Evaluator (LangGraph) — evaluate a paper PDF or arXiv URL",
|
20 |
-
epilog="Example: python cli.py https://arxiv.org/pdf/2507.14683 -o /abs/path/save_dir/eval_2507_14683",
|
21 |
-
)
|
22 |
-
parser.add_argument("pdf", help="Local PDF absolute path or URL (e.g., https://arxiv.org/pdf/xxxx)")
|
23 |
-
parser.add_argument(
|
24 |
-
"-o",
|
25 |
-
"--output-prefix",
|
26 |
-
dest="output_prefix",
|
27 |
-
help="Output file prefix (if provided, will save as <prefix>_YYYYMMDD_HHMMSS.md)",
|
28 |
-
)
|
29 |
-
parser.add_argument(
|
30 |
-
"--api-key",
|
31 |
-
dest="api_key",
|
32 |
-
default=os.getenv("ANTHROPIC_API_KEY"),
|
33 |
-
help="Anthropic API key (overrides ANTHROPIC_API_KEY env)",
|
34 |
-
)
|
35 |
-
return parser
|
36 |
-
|
37 |
-
|
38 |
-
def main(argv: Optional[list[str]] = None):
|
39 |
-
parser = build_parser()
|
40 |
-
args = parser.parse_args(argv)
|
41 |
-
|
42 |
-
pdf_path: str = args.pdf
|
43 |
-
output_prefix: Optional[str] = args.output_prefix
|
44 |
-
api_key: Optional[str] = args.api_key or os.getenv("ANTHROPIC_API_KEY")
|
45 |
-
|
46 |
-
if not api_key:
|
47 |
-
console.print("[yellow]Warning:[/yellow] ANTHROPIC_API_KEY not set and --api-key not provided.", highlight=False)
|
48 |
-
|
49 |
-
console.print(Panel.fit(f"Evaluating: {pdf_path}"))
|
50 |
-
try:
|
51 |
-
result = run_evaluation(pdf_path=pdf_path, output_file=output_prefix, api_key=api_key)
|
52 |
-
console.print("\n[bold green]Done.[/bold green]\n")
|
53 |
-
if output_prefix:
|
54 |
-
console.print(f"Saved to prefix: {output_prefix}_<timestamp>.md")
|
55 |
-
else:
|
56 |
-
console.print(result)
|
57 |
-
except Exception as e:
|
58 |
-
console.print(f"[bold red]Error:[/bold red] {e}")
|
59 |
-
sys.exit(2)
|
60 |
|
|
|
|
|
61 |
|
62 |
if __name__ == "__main__":
|
63 |
main()
|
64 |
-
|
65 |
-
|
|
|
|
|
1 |
import os
|
2 |
import sys
|
3 |
+
from pathlib import Path
|
|
|
|
|
4 |
|
5 |
+
# Add the src directory to Python path
|
6 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
+
# Import and run the CLI
|
9 |
+
from src.cli.cli import main
|
10 |
|
11 |
if __name__ == "__main__":
|
12 |
main()
|
|
|
|
configs/paper_agent.py
CHANGED
@@ -2,3 +2,8 @@ workdir = "workdir"
|
|
2 |
tag = "paper_agent"
|
3 |
exp_path = f"{workdir}/{tag}"
|
4 |
log_path = "agent.log"
|
|
|
|
|
|
|
|
|
|
|
|
2 |
tag = "paper_agent"
|
3 |
exp_path = f"{workdir}/{tag}"
|
4 |
log_path = "agent.log"
|
5 |
+
db_path = "papers_cache.db"
|
6 |
+
frontend_path = "frontend"
|
7 |
+
|
8 |
+
model_id = "claude-sonnet-4-20250514"
|
9 |
+
version = "0.1.0"
|
frontend/index.html
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
-
<title>
|
7 |
<link rel="stylesheet" href="/styles.css?v=9" />
|
8 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
9 |
</head>
|
@@ -14,7 +14,7 @@
|
|
14 |
<div class="nav-left">
|
15 |
<div class="logo">
|
16 |
<i class="fas fa-book-open"></i>
|
17 |
-
<span>
|
18 |
</div>
|
19 |
</div>
|
20 |
|
@@ -40,7 +40,7 @@
|
|
40 |
<div class="header-container">
|
41 |
<div class="header-left">
|
42 |
<h1>Daily Papers</h1>
|
43 |
-
<p class="subtitle">by
|
44 |
</div>
|
45 |
|
46 |
<div class="header-center">
|
@@ -81,6 +81,15 @@
|
|
81 |
</div>
|
82 |
</main>
|
83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
<script src="/main.js"></script>
|
85 |
</body>
|
86 |
</html>
|
|
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
+
<title>AIR Index — Daily Papers</title>
|
7 |
<link rel="stylesheet" href="/styles.css?v=9" />
|
8 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
9 |
</head>
|
|
|
14 |
<div class="nav-left">
|
15 |
<div class="logo">
|
16 |
<i class="fas fa-book-open"></i>
|
17 |
+
<span>AIR Index</span>
|
18 |
</div>
|
19 |
</div>
|
20 |
|
|
|
40 |
<div class="header-container">
|
41 |
<div class="header-left">
|
42 |
<h1>Daily Papers</h1>
|
43 |
+
<p class="subtitle">by AI Realizability Index Agent</p>
|
44 |
</div>
|
45 |
|
46 |
<div class="header-center">
|
|
|
81 |
</div>
|
82 |
</main>
|
83 |
|
84 |
+
<!-- Loading Overlay -->
|
85 |
+
<div id="loadingOverlay" class="loading-overlay">
|
86 |
+
<div class="loading-spinner">
|
87 |
+
<div class="spinner"></div>
|
88 |
+
<div class="loading-text">Loading papers...</div>
|
89 |
+
<div class="loading-subtext">Fetching data from Hugging Face</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
|
93 |
<script src="/main.js"></script>
|
94 |
</body>
|
95 |
</html>
|
frontend/main.js
CHANGED
@@ -36,6 +36,7 @@ class DateManager {
|
|
36 |
constructor() {
|
37 |
// Start with today's date, but it will be updated when we get the actual available date
|
38 |
this.currentDate = new Date();
|
|
|
39 |
this.init();
|
40 |
}
|
41 |
|
@@ -44,6 +45,10 @@ class DateManager {
|
|
44 |
this.bindEvents();
|
45 |
}
|
46 |
|
|
|
|
|
|
|
|
|
47 |
formatDate(date) {
|
48 |
const options = {
|
49 |
year: 'numeric',
|
@@ -53,27 +58,121 @@ class DateManager {
|
|
53 |
return date.toLocaleDateString('en-US', options);
|
54 |
}
|
55 |
|
56 |
-
updateDateDisplay() {
|
57 |
const dateDisplay = document.getElementById('dateDisplay');
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
59 |
}
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
}
|
68 |
|
69 |
bindEvents() {
|
70 |
-
document.getElementById('prevDate')
|
71 |
-
|
72 |
-
|
|
|
|
|
|
|
|
|
|
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
77 |
}
|
78 |
|
79 |
getDateString() {
|
@@ -258,13 +357,24 @@ class PaperCardRenderer {
|
|
258 |
|
259 |
${paper.arxiv_id ? `
|
260 |
<div class="card-actions">
|
261 |
-
<
|
262 |
-
<i class="fas fa-
|
263 |
-
|
|
|
|
|
264 |
</div>
|
265 |
` : ''}
|
266 |
`;
|
267 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
268 |
return card;
|
269 |
}
|
270 |
|
@@ -287,6 +397,326 @@ class PaperCardRenderer {
|
|
287 |
this.cardsContainer.appendChild(card);
|
288 |
});
|
289 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
290 |
}
|
291 |
|
292 |
// Main Application
|
@@ -294,6 +724,7 @@ class PaperIndexApp {
|
|
294 |
constructor() {
|
295 |
this.themeManager = new ThemeManager();
|
296 |
this.dateManager = new DateManager();
|
|
|
297 |
this.searchManager = new SearchManager();
|
298 |
this.cardRenderer = new PaperCardRenderer();
|
299 |
this.init();
|
@@ -301,6 +732,7 @@ class PaperIndexApp {
|
|
301 |
|
302 |
init() {
|
303 |
this.bindEvents();
|
|
|
304 |
this.loadDaily();
|
305 |
}
|
306 |
|
@@ -319,11 +751,17 @@ class PaperIndexApp {
|
|
319 |
});
|
320 |
}
|
321 |
|
322 |
-
async loadDaily() {
|
323 |
const dateStr = this.dateManager.getDateString();
|
324 |
|
325 |
try {
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
|
328 |
if (!response.ok) {
|
329 |
throw new Error('Failed to load daily papers');
|
@@ -335,18 +773,24 @@ class PaperIndexApp {
|
|
335 |
requested_date: data.requested_date,
|
336 |
actual_date: data.date,
|
337 |
fallback_used: data.fallback_used,
|
338 |
-
cards_count: data.cards?.length
|
|
|
339 |
});
|
340 |
|
341 |
-
//
|
342 |
if (data.date && data.requested_date && data.date !== data.requested_date) {
|
343 |
-
console.log('
|
344 |
-
|
345 |
-
|
|
|
|
|
346 |
this.dateManager.updateDateDisplay();
|
347 |
|
348 |
-
// Show a notification about the
|
349 |
-
this.
|
|
|
|
|
|
|
350 |
}
|
351 |
|
352 |
// Show cache status if available
|
@@ -371,83 +815,173 @@ class PaperIndexApp {
|
|
371 |
</a>
|
372 |
</div>
|
373 |
`;
|
|
|
|
|
|
|
|
|
374 |
}
|
375 |
}
|
376 |
|
377 |
-
showFallbackNotification
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
box-shadow: var(--shadow-lg);
|
389 |
-
z-index: 1000;
|
390 |
-
max-width: 300px;
|
391 |
-
color: var(--text-primary);
|
392 |
-
`;
|
393 |
-
|
394 |
-
notification.innerHTML = `
|
395 |
-
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
396 |
-
<i class="fas fa-info-circle" style="color: var(--accent-primary);"></i>
|
397 |
-
<strong>Date Updated</strong>
|
398 |
-
</div>
|
399 |
-
<p style="margin: 0; font-size: 14px; color: var(--text-secondary);">
|
400 |
-
Papers for ${requestedDate} not available. Showing latest available: ${actualDate}
|
401 |
-
</p>
|
402 |
-
`;
|
403 |
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
setTimeout(() => {
|
408 |
if (notification.parentNode) {
|
409 |
notification.parentNode.removeChild(notification);
|
410 |
}
|
411 |
-
}
|
412 |
-
|
413 |
-
|
414 |
-
showCacheNotification(cachedAt) {
|
415 |
-
// Create a temporary notification
|
416 |
const notification = document.createElement('div');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
417 |
notification.style.cssText = `
|
418 |
position: fixed;
|
419 |
top: 20px;
|
420 |
right: 20px;
|
421 |
background: var(--bg-primary);
|
422 |
-
border: 1px solid
|
423 |
border-radius: 8px;
|
424 |
padding: 16px;
|
425 |
box-shadow: var(--shadow-lg);
|
426 |
z-index: 1000;
|
427 |
-
max-width:
|
428 |
color: var(--text-primary);
|
|
|
429 |
`;
|
430 |
|
431 |
-
const cacheTime = new Date(cachedAt).toLocaleString();
|
432 |
-
|
433 |
notification.innerHTML = `
|
434 |
-
<div style="display: flex; align-items:
|
435 |
-
<i class="
|
436 |
-
<
|
|
|
|
|
|
|
437 |
</div>
|
438 |
-
<p style="margin: 0; font-size: 14px; color: var(--text-secondary);">
|
439 |
-
Showing cached data from ${cacheTime}
|
440 |
-
</p>
|
441 |
`;
|
442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
document.body.appendChild(notification);
|
444 |
|
445 |
-
// Remove notification after
|
446 |
-
|
447 |
-
|
448 |
-
notification.parentNode
|
449 |
-
|
450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
451 |
}
|
452 |
}
|
453 |
|
|
|
36 |
constructor() {
|
37 |
// Start with today's date, but it will be updated when we get the actual available date
|
38 |
this.currentDate = new Date();
|
39 |
+
this.app = null; // Reference to the main app
|
40 |
this.init();
|
41 |
}
|
42 |
|
|
|
45 |
this.bindEvents();
|
46 |
}
|
47 |
|
48 |
+
setApp(app) {
|
49 |
+
this.app = app;
|
50 |
+
}
|
51 |
+
|
52 |
formatDate(date) {
|
53 |
const options = {
|
54 |
year: 'numeric',
|
|
|
58 |
return date.toLocaleDateString('en-US', options);
|
59 |
}
|
60 |
|
61 |
+
async updateDateDisplay() {
|
62 |
const dateDisplay = document.getElementById('dateDisplay');
|
63 |
+
if (dateDisplay) {
|
64 |
+
dateDisplay.textContent = this.formatDate(this.currentDate);
|
65 |
+
}
|
66 |
+
|
67 |
+
// Update button states based on available dates
|
68 |
+
await this.updateButtonStates();
|
69 |
}
|
70 |
|
71 |
+
async updateButtonStates() {
|
72 |
+
try {
|
73 |
+
// Check if current date is in the future
|
74 |
+
const today = new Date();
|
75 |
+
today.setHours(23, 59, 59, 999);
|
76 |
+
|
77 |
+
if (this.currentDate > today) {
|
78 |
+
this.setButtonState('nextDate', false);
|
79 |
+
this.setButtonState('prevDate', true);
|
80 |
+
return;
|
81 |
+
}
|
82 |
+
|
83 |
+
// For previous button, always allow going back (unless it's too far in the past)
|
84 |
+
const minDate = new Date('2020-01-01'); // Reasonable minimum date
|
85 |
+
this.setButtonState('prevDate', this.currentDate > minDate);
|
86 |
+
|
87 |
+
// For next button, only disable if it's today or in the future
|
88 |
+
this.setButtonState('nextDate', this.currentDate < today);
|
89 |
+
|
90 |
+
} catch (error) {
|
91 |
+
console.error('Error updating button states:', error);
|
92 |
+
}
|
93 |
+
}
|
94 |
+
|
95 |
+
setButtonState(buttonId, enabled) {
|
96 |
+
const button = document.getElementById(buttonId);
|
97 |
+
if (button) {
|
98 |
+
button.disabled = !enabled;
|
99 |
+
button.style.opacity = enabled ? '1' : '0.5';
|
100 |
+
button.style.cursor = enabled ? 'pointer' : 'not-allowed';
|
101 |
+
}
|
102 |
+
}
|
103 |
+
|
104 |
+
async navigateDate(direction) {
|
105 |
+
try {
|
106 |
+
// Calculate target date first
|
107 |
+
const newDate = new Date(this.currentDate);
|
108 |
+
newDate.setDate(newDate.getDate() + direction);
|
109 |
+
|
110 |
+
// Check if the new date is in the future
|
111 |
+
const today = new Date();
|
112 |
+
today.setHours(23, 59, 59, 999); // End of today
|
113 |
+
|
114 |
+
if (newDate > today) {
|
115 |
+
this.showDateLimitNotification('Cannot navigate to future dates');
|
116 |
+
return;
|
117 |
+
}
|
118 |
+
|
119 |
+
// Update current date
|
120 |
+
this.currentDate = newDate;
|
121 |
+
this.updateDateDisplay();
|
122 |
+
|
123 |
+
// Show loading animation
|
124 |
+
const dateStr = this.formatDate(this.currentDate);
|
125 |
+
const direction_str = direction > 0 ? "next" : "prev";
|
126 |
+
this.showLoading(`Loading papers for ${dateStr}...`, `Navigating ${direction_str} from Hugging Face`);
|
127 |
+
|
128 |
+
// Try to load the target date with direction
|
129 |
+
if (this.app && this.app.loadDaily) {
|
130 |
+
await this.app.loadDaily(direction_str);
|
131 |
+
}
|
132 |
+
|
133 |
+
} catch (error) {
|
134 |
+
console.error('Error navigating date:', error);
|
135 |
+
this.showDateLimitNotification('Error loading date');
|
136 |
+
}
|
137 |
+
}
|
138 |
+
|
139 |
+
// Removed old notification functions - now using unified notification system
|
140 |
+
|
141 |
+
showLoading(message = 'Loading papers...', submessage = 'Fetching data from Hugging Face') {
|
142 |
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
143 |
+
if (loadingOverlay) {
|
144 |
+
const loadingText = loadingOverlay.querySelector('.loading-text');
|
145 |
+
const loadingSubtext = loadingOverlay.querySelector('.loading-subtext');
|
146 |
+
|
147 |
+
if (loadingText) loadingText.textContent = message;
|
148 |
+
if (loadingSubtext) loadingSubtext.textContent = submessage;
|
149 |
+
|
150 |
+
loadingOverlay.classList.add('show');
|
151 |
+
}
|
152 |
+
}
|
153 |
+
|
154 |
+
hideLoading() {
|
155 |
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
156 |
+
if (loadingOverlay) {
|
157 |
+
loadingOverlay.classList.remove('show');
|
158 |
+
}
|
159 |
}
|
160 |
|
161 |
bindEvents() {
|
162 |
+
const prevBtn = document.getElementById('prevDate');
|
163 |
+
const nextBtn = document.getElementById('nextDate');
|
164 |
+
|
165 |
+
if (prevBtn) {
|
166 |
+
prevBtn.addEventListener('click', async () => {
|
167 |
+
await this.navigateDate(-1);
|
168 |
+
});
|
169 |
+
}
|
170 |
|
171 |
+
if (nextBtn) {
|
172 |
+
nextBtn.addEventListener('click', async () => {
|
173 |
+
await this.navigateDate(1);
|
174 |
+
});
|
175 |
+
}
|
176 |
}
|
177 |
|
178 |
getDateString() {
|
|
|
357 |
|
358 |
${paper.arxiv_id ? `
|
359 |
<div class="card-actions">
|
360 |
+
<button class="eval-button" data-arxiv-id="${paper.arxiv_id}" data-paper-title="${encodeURIComponent(title)}">
|
361 |
+
<i class="fas fa-spinner fa-spin" style="display: none;"></i>
|
362 |
+
<i class="fas fa-chart-line eval-icon"></i>
|
363 |
+
<span class="eval-text">Checking...</span>
|
364 |
+
</button>
|
365 |
</div>
|
366 |
` : ''}
|
367 |
`;
|
368 |
|
369 |
+
// Check evaluation status for this paper
|
370 |
+
if (paper.arxiv_id) {
|
371 |
+
this.checkEvaluationStatus(card, paper.arxiv_id);
|
372 |
+
|
373 |
+
// Store paper data in card for score checking
|
374 |
+
card.setAttribute('data-paper-data', JSON.stringify(paper));
|
375 |
+
this.checkPaperScore(card, paper.arxiv_id);
|
376 |
+
}
|
377 |
+
|
378 |
return card;
|
379 |
}
|
380 |
|
|
|
397 |
this.cardsContainer.appendChild(card);
|
398 |
});
|
399 |
}
|
400 |
+
|
401 |
+
async checkEvaluationStatus(card, arxivId) {
|
402 |
+
const button = card.querySelector('.eval-button');
|
403 |
+
const spinner = button.querySelector('.fa-spinner');
|
404 |
+
const evalIcon = button.querySelector('.eval-icon');
|
405 |
+
const evalText = button.querySelector('.eval-text');
|
406 |
+
|
407 |
+
try {
|
408 |
+
const response = await fetch(`/api/has-eval/${encodeURIComponent(arxivId)}`);
|
409 |
+
const data = await response.json();
|
410 |
+
|
411 |
+
if (data.exists) {
|
412 |
+
// Paper has evaluation - show evaluation button
|
413 |
+
evalIcon.className = 'fas fa-chart-line eval-icon';
|
414 |
+
evalText.textContent = 'Evaluation';
|
415 |
+
button.className = 'eval-button evaluation-state';
|
416 |
+
button.onclick = () => {
|
417 |
+
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
|
418 |
+
};
|
419 |
+
} else {
|
420 |
+
// Paper doesn't have evaluation - show evaluate button
|
421 |
+
evalIcon.className = 'fas fa-play eval-icon';
|
422 |
+
evalText.textContent = 'Evaluate';
|
423 |
+
button.className = 'eval-button evaluate-state';
|
424 |
+
button.onclick = () => {
|
425 |
+
this.evaluatePaper(button, arxivId);
|
426 |
+
};
|
427 |
+
}
|
428 |
+
} catch (error) {
|
429 |
+
console.error('Error checking evaluation status:', error);
|
430 |
+
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
|
431 |
+
evalText.textContent = 'Error';
|
432 |
+
button.className = 'eval-button error-state';
|
433 |
+
}
|
434 |
+
}
|
435 |
+
|
436 |
+
async checkPaperScore(card, arxivId) {
|
437 |
+
try {
|
438 |
+
// First check if the card already has score data from the API response
|
439 |
+
const cardData = card.getAttribute('data-paper-data');
|
440 |
+
if (cardData) {
|
441 |
+
const paperData = JSON.parse(cardData);
|
442 |
+
if (paperData.overall_score !== null && paperData.overall_score !== undefined) {
|
443 |
+
this.displayScoreBadge(card, paperData.overall_score, arxivId);
|
444 |
+
return;
|
445 |
+
}
|
446 |
+
}
|
447 |
+
|
448 |
+
// Fallback to API call if no score data in card
|
449 |
+
const response = await fetch(`/api/paper-score/${encodeURIComponent(arxivId)}`);
|
450 |
+
const data = await response.json();
|
451 |
+
|
452 |
+
console.log(`Paper score data for ${arxivId}:`, data);
|
453 |
+
|
454 |
+
if (data.has_score && data.score !== null) {
|
455 |
+
this.displayScoreBadge(card, data.score, arxivId);
|
456 |
+
}
|
457 |
+
} catch (error) {
|
458 |
+
console.error('Error checking paper score:', error);
|
459 |
+
}
|
460 |
+
}
|
461 |
+
|
462 |
+
displayScoreBadge(card, score, arxivId) {
|
463 |
+
// Create score badge
|
464 |
+
const scoreBadge = document.createElement('div');
|
465 |
+
scoreBadge.className = 'score-badge';
|
466 |
+
const formattedScore = parseFloat(score).toFixed(1);
|
467 |
+
|
468 |
+
// Determine score color based on value (0-4 scale)
|
469 |
+
const scoreValue = parseFloat(score);
|
470 |
+
let scoreColor = 'var(--accent-primary)';
|
471 |
+
if (scoreValue >= 3.0) {
|
472 |
+
scoreColor = 'var(--accent-success)';
|
473 |
+
} else if (scoreValue >= 2.0) {
|
474 |
+
scoreColor = 'var(--accent-warning)';
|
475 |
+
} else if (scoreValue < 1.0) {
|
476 |
+
scoreColor = 'var(--accent-danger)';
|
477 |
+
}
|
478 |
+
|
479 |
+
scoreBadge.style.background = `linear-gradient(135deg, ${scoreColor}, ${scoreColor}dd)`;
|
480 |
+
scoreBadge.innerHTML = `
|
481 |
+
<span class="score-number">${formattedScore}</span>
|
482 |
+
<span class="score-label">Overall</span>
|
483 |
+
`;
|
484 |
+
|
485 |
+
// Add click handler to navigate to evaluation page
|
486 |
+
scoreBadge.onclick = () => {
|
487 |
+
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
|
488 |
+
};
|
489 |
+
|
490 |
+
// Add to card with animation
|
491 |
+
card.appendChild(scoreBadge);
|
492 |
+
scoreBadge.style.opacity = '0';
|
493 |
+
scoreBadge.style.transform = 'scale(0.8) translateY(10px)';
|
494 |
+
|
495 |
+
// Animate in
|
496 |
+
setTimeout(() => {
|
497 |
+
scoreBadge.style.transition = 'all 0.3s ease';
|
498 |
+
scoreBadge.style.opacity = '1';
|
499 |
+
scoreBadge.style.transform = 'scale(1) translateY(0)';
|
500 |
+
}, 100);
|
501 |
+
}
|
502 |
+
|
503 |
+
async evaluatePaper(button, arxivId) {
|
504 |
+
const spinner = button.querySelector('.fa-spinner');
|
505 |
+
const evalIcon = button.querySelector('.eval-icon');
|
506 |
+
const evalText = button.querySelector('.eval-text');
|
507 |
+
const paperTitle = button.getAttribute('data-paper-title');
|
508 |
+
|
509 |
+
// Show loading state
|
510 |
+
spinner.style.display = 'inline-block';
|
511 |
+
evalIcon.style.display = 'none';
|
512 |
+
evalText.textContent = 'Evaluating...';
|
513 |
+
button.className = 'eval-button evaluating-state';
|
514 |
+
button.disabled = true;
|
515 |
+
|
516 |
+
try {
|
517 |
+
// First, check if paper exists in database, if not, insert it
|
518 |
+
const paperData = {
|
519 |
+
arxiv_id: arxivId,
|
520 |
+
title: decodeURIComponent(paperTitle),
|
521 |
+
authors: "Unknown Authors", // We don't have authors in the card data
|
522 |
+
abstract: "No abstract available",
|
523 |
+
categories: "Unknown",
|
524 |
+
published_date: new Date().toISOString().split('T')[0]
|
525 |
+
};
|
526 |
+
|
527 |
+
// Try to insert the paper (this will work even if it already exists)
|
528 |
+
await fetch('/api/papers/insert', {
|
529 |
+
method: 'POST',
|
530 |
+
headers: {
|
531 |
+
'Content-Type': 'application/json',
|
532 |
+
},
|
533 |
+
body: JSON.stringify(paperData)
|
534 |
+
});
|
535 |
+
|
536 |
+
// Start evaluation
|
537 |
+
const response = await fetch(`/api/papers/evaluate/${encodeURIComponent(arxivId)}`, {
|
538 |
+
method: 'POST'
|
539 |
+
});
|
540 |
+
|
541 |
+
if (response.ok) {
|
542 |
+
const result = await response.json();
|
543 |
+
|
544 |
+
if (result.status === 'already_evaluated') {
|
545 |
+
// Paper was already evaluated, redirect to evaluation page
|
546 |
+
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
|
547 |
+
} else {
|
548 |
+
// Evaluation started, show progress and poll for status
|
549 |
+
evalText.textContent = 'Started...';
|
550 |
+
button.className = 'eval-button started-state';
|
551 |
+
|
552 |
+
// Start polling for status
|
553 |
+
this.pollEvaluationStatus(button, arxivId);
|
554 |
+
}
|
555 |
+
} else {
|
556 |
+
throw new Error('Failed to start evaluation');
|
557 |
+
}
|
558 |
+
} catch (error) {
|
559 |
+
console.error('Error evaluating paper:', error);
|
560 |
+
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
|
561 |
+
evalText.textContent = 'Error';
|
562 |
+
button.className = 'eval-button error-state';
|
563 |
+
button.disabled = false;
|
564 |
+
} finally {
|
565 |
+
spinner.style.display = 'none';
|
566 |
+
evalIcon.style.display = 'inline-block';
|
567 |
+
}
|
568 |
+
}
|
569 |
+
|
570 |
+
async pollEvaluationStatus(button, arxivId) {
|
571 |
+
const evalIcon = button.querySelector('.eval-icon');
|
572 |
+
const evalText = button.querySelector('.eval-text');
|
573 |
+
let pollCount = 0;
|
574 |
+
const maxPolls = 60; // Poll for up to 5 minutes (5s intervals)
|
575 |
+
|
576 |
+
// Show log message
|
577 |
+
this.showLogMessage(`Started evaluation for paper ${arxivId}`, 'info');
|
578 |
+
|
579 |
+
const poll = async () => {
|
580 |
+
try {
|
581 |
+
const response = await fetch(`/api/papers/evaluate/${encodeURIComponent(arxivId)}/status`);
|
582 |
+
if (response.ok) {
|
583 |
+
const status = await response.json();
|
584 |
+
|
585 |
+
switch (status.status) {
|
586 |
+
case 'evaluating':
|
587 |
+
evalText.textContent = `Evaluating... (${pollCount * 5}s)`;
|
588 |
+
evalIcon.className = 'fas fa-spinner fa-spin eval-icon';
|
589 |
+
button.className = 'eval-button evaluating-state';
|
590 |
+
this.showLogMessage(`Evaluating paper ${arxivId}... (${pollCount * 5}s)`, 'info');
|
591 |
+
break;
|
592 |
+
|
593 |
+
case 'completed':
|
594 |
+
evalIcon.className = 'fas fa-check eval-icon';
|
595 |
+
evalText.textContent = 'Completed';
|
596 |
+
button.className = 'eval-button evaluation-state';
|
597 |
+
button.onclick = () => {
|
598 |
+
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
|
599 |
+
};
|
600 |
+
this.showLogMessage(`Evaluation completed for paper ${arxivId}`, 'success');
|
601 |
+
|
602 |
+
// Add score badge after completion
|
603 |
+
this.checkPaperScore(button.closest('.hf-paper-card'), arxivId);
|
604 |
+
|
605 |
+
return; // Stop polling
|
606 |
+
|
607 |
+
case 'failed':
|
608 |
+
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
|
609 |
+
evalText.textContent = 'Failed';
|
610 |
+
button.className = 'eval-button error-state';
|
611 |
+
button.disabled = false;
|
612 |
+
this.showLogMessage(`Evaluation failed for paper ${arxivId}`, 'error');
|
613 |
+
return; // Stop polling
|
614 |
+
|
615 |
+
default:
|
616 |
+
evalText.textContent = `Processing... (${pollCount * 5}s)`;
|
617 |
+
button.className = 'eval-button processing-state';
|
618 |
+
}
|
619 |
+
}
|
620 |
+
} catch (error) {
|
621 |
+
console.error('Error polling status:', error);
|
622 |
+
this.showLogMessage(`Error checking status for paper ${arxivId}`, 'error');
|
623 |
+
}
|
624 |
+
|
625 |
+
pollCount++;
|
626 |
+
if (pollCount < maxPolls) {
|
627 |
+
setTimeout(poll, 5000); // Poll every 5 seconds
|
628 |
+
} else {
|
629 |
+
// Timeout
|
630 |
+
evalIcon.className = 'fas fa-clock eval-icon';
|
631 |
+
evalText.textContent = 'Timeout';
|
632 |
+
button.className = 'eval-button error-state';
|
633 |
+
button.disabled = false;
|
634 |
+
this.showLogMessage(`Evaluation timeout for paper ${arxivId}`, 'warning');
|
635 |
+
}
|
636 |
+
};
|
637 |
+
|
638 |
+
// Start polling
|
639 |
+
setTimeout(poll, 5000); // First poll after 5 seconds
|
640 |
+
}
|
641 |
+
|
642 |
+
showLogMessage(message, type = 'info') {
|
643 |
+
// Create or get log container
|
644 |
+
let logContainer = document.getElementById('evaluation-log');
|
645 |
+
if (!logContainer) {
|
646 |
+
logContainer = document.createElement('div');
|
647 |
+
logContainer.id = 'evaluation-log';
|
648 |
+
logContainer.className = 'evaluation-log';
|
649 |
+
logContainer.style.cssText = `
|
650 |
+
position: fixed;
|
651 |
+
bottom: 20px;
|
652 |
+
right: 20px;
|
653 |
+
max-width: 400px;
|
654 |
+
max-height: 300px;
|
655 |
+
overflow-y: auto;
|
656 |
+
background: var(--bg-primary);
|
657 |
+
border: 1px solid var(--border-medium);
|
658 |
+
border-radius: 8px;
|
659 |
+
padding: 12px;
|
660 |
+
box-shadow: var(--shadow-lg);
|
661 |
+
z-index: 1000;
|
662 |
+
font-size: 12px;
|
663 |
+
`;
|
664 |
+
document.body.appendChild(logContainer);
|
665 |
+
}
|
666 |
+
|
667 |
+
// Create log entry
|
668 |
+
const logEntry = document.createElement('div');
|
669 |
+
logEntry.className = `log-entry log-${type}`;
|
670 |
+
logEntry.style.cssText = `
|
671 |
+
margin-bottom: 8px;
|
672 |
+
padding: 8px;
|
673 |
+
border-radius: 4px;
|
674 |
+
border-left: 3px solid;
|
675 |
+
`;
|
676 |
+
|
677 |
+
// Set color based on type
|
678 |
+
switch (type) {
|
679 |
+
case 'success':
|
680 |
+
logEntry.style.borderLeftColor = 'var(--accent-success)';
|
681 |
+
logEntry.style.backgroundColor = 'rgba(16, 185, 129, 0.1)';
|
682 |
+
break;
|
683 |
+
case 'error':
|
684 |
+
logEntry.style.borderLeftColor = 'var(--accent-danger)';
|
685 |
+
logEntry.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
|
686 |
+
break;
|
687 |
+
case 'warning':
|
688 |
+
logEntry.style.borderLeftColor = 'var(--accent-warning)';
|
689 |
+
logEntry.style.backgroundColor = 'rgba(245, 158, 11, 0.1)';
|
690 |
+
break;
|
691 |
+
default:
|
692 |
+
logEntry.style.borderLeftColor = 'var(--accent-primary)';
|
693 |
+
logEntry.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
694 |
+
}
|
695 |
+
|
696 |
+
const timestamp = new Date().toLocaleTimeString();
|
697 |
+
logEntry.innerHTML = `
|
698 |
+
<div style="font-weight: 500; margin-bottom: 2px;">${timestamp}</div>
|
699 |
+
<div>${message}</div>
|
700 |
+
`;
|
701 |
+
|
702 |
+
logContainer.appendChild(logEntry);
|
703 |
+
logContainer.scrollTop = logContainer.scrollHeight;
|
704 |
+
|
705 |
+
// Auto-remove old entries (keep last 10)
|
706 |
+
const entries = logContainer.querySelectorAll('.log-entry');
|
707 |
+
if (entries.length > 10) {
|
708 |
+
entries[0].remove();
|
709 |
+
}
|
710 |
+
|
711 |
+
// Auto-hide success messages after 5 seconds
|
712 |
+
if (type === 'success') {
|
713 |
+
setTimeout(() => {
|
714 |
+
if (logEntry.parentNode) {
|
715 |
+
logEntry.style.opacity = '0.5';
|
716 |
+
}
|
717 |
+
}, 5000);
|
718 |
+
}
|
719 |
+
}
|
720 |
}
|
721 |
|
722 |
// Main Application
|
|
|
724 |
constructor() {
|
725 |
this.themeManager = new ThemeManager();
|
726 |
this.dateManager = new DateManager();
|
727 |
+
this.dateManager.setApp(this); // Pass app reference to date manager
|
728 |
this.searchManager = new SearchManager();
|
729 |
this.cardRenderer = new PaperCardRenderer();
|
730 |
this.init();
|
|
|
732 |
|
733 |
init() {
|
734 |
this.bindEvents();
|
735 |
+
this.dateManager.showLoading('Loading papers...', 'Initializing application');
|
736 |
this.loadDaily();
|
737 |
}
|
738 |
|
|
|
751 |
});
|
752 |
}
|
753 |
|
754 |
+
async loadDaily(direction = null) {
|
755 |
const dateStr = this.dateManager.getDateString();
|
756 |
|
757 |
try {
|
758 |
+
// Build URL with direction parameter if provided
|
759 |
+
let url = `/api/daily?date_str=${encodeURIComponent(dateStr)}`;
|
760 |
+
if (direction) {
|
761 |
+
url += `&direction=${direction}`;
|
762 |
+
}
|
763 |
+
|
764 |
+
const response = await fetch(url);
|
765 |
|
766 |
if (!response.ok) {
|
767 |
throw new Error('Failed to load daily papers');
|
|
|
773 |
requested_date: data.requested_date,
|
774 |
actual_date: data.date,
|
775 |
fallback_used: data.fallback_used,
|
776 |
+
cards_count: data.cards?.length,
|
777 |
+
direction: direction
|
778 |
});
|
779 |
|
780 |
+
// Handle fallback cases - if we got redirected to a different date
|
781 |
if (data.date && data.requested_date && data.date !== data.requested_date) {
|
782 |
+
console.log('Redirected from', data.requested_date, 'to', data.date);
|
783 |
+
|
784 |
+
// Update to the actual date that was found
|
785 |
+
const actualDate = new Date(data.date);
|
786 |
+
this.dateManager.currentDate = actualDate;
|
787 |
this.dateManager.updateDateDisplay();
|
788 |
|
789 |
+
// Show a notification about the redirect
|
790 |
+
this.showRedirectNotification(data.requested_date, data.date);
|
791 |
+
} else if (data.cards && data.cards.length === 0) {
|
792 |
+
// No papers found for the requested date
|
793 |
+
this.showNoPapersNotification(data.requested_date);
|
794 |
}
|
795 |
|
796 |
// Show cache status if available
|
|
|
815 |
</a>
|
816 |
</div>
|
817 |
`;
|
818 |
+
} finally {
|
819 |
+
// Hide loading animation and update button states
|
820 |
+
this.dateManager.hideLoading();
|
821 |
+
await this.dateManager.updateDateDisplay();
|
822 |
}
|
823 |
}
|
824 |
|
825 |
+
// Removed showFallbackNotification - now using unified notification system
|
826 |
+
|
827 |
+
// Unified notification system
|
828 |
+
showNotification(options) {
|
829 |
+
const {
|
830 |
+
type = 'info', // 'info', 'success', 'warning', 'error'
|
831 |
+
title = '',
|
832 |
+
message = '',
|
833 |
+
duration = 4000,
|
834 |
+
icon = null
|
835 |
+
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
836 |
|
837 |
+
// Remove existing notifications
|
838 |
+
const existingNotifications = document.querySelectorAll('.notification');
|
839 |
+
existingNotifications.forEach(notification => {
|
|
|
840 |
if (notification.parentNode) {
|
841 |
notification.parentNode.removeChild(notification);
|
842 |
}
|
843 |
+
});
|
844 |
+
|
845 |
+
// Create notification element
|
|
|
|
|
846 |
const notification = document.createElement('div');
|
847 |
+
notification.className = 'notification';
|
848 |
+
|
849 |
+
// Set icon based on type if not provided
|
850 |
+
let iconClass = icon;
|
851 |
+
if (!iconClass) {
|
852 |
+
switch (type) {
|
853 |
+
case 'success':
|
854 |
+
iconClass = 'fas fa-check-circle';
|
855 |
+
break;
|
856 |
+
case 'warning':
|
857 |
+
iconClass = 'fas fa-exclamation-triangle';
|
858 |
+
break;
|
859 |
+
case 'error':
|
860 |
+
iconClass = 'fas fa-times-circle';
|
861 |
+
break;
|
862 |
+
case 'info':
|
863 |
+
default:
|
864 |
+
iconClass = 'fas fa-info-circle';
|
865 |
+
break;
|
866 |
+
}
|
867 |
+
}
|
868 |
+
|
869 |
+
// Set colors based on type
|
870 |
+
let borderColor = 'var(--accent-info)';
|
871 |
+
let iconColor = 'var(--accent-info)';
|
872 |
+
|
873 |
+
switch (type) {
|
874 |
+
case 'success':
|
875 |
+
borderColor = 'var(--accent-success)';
|
876 |
+
iconColor = 'var(--accent-success)';
|
877 |
+
break;
|
878 |
+
case 'warning':
|
879 |
+
borderColor = 'var(--accent-warning)';
|
880 |
+
iconColor = 'var(--accent-warning)';
|
881 |
+
break;
|
882 |
+
case 'error':
|
883 |
+
borderColor = 'var(--accent-danger)';
|
884 |
+
iconColor = 'var(--accent-danger)';
|
885 |
+
break;
|
886 |
+
}
|
887 |
+
|
888 |
notification.style.cssText = `
|
889 |
position: fixed;
|
890 |
top: 20px;
|
891 |
right: 20px;
|
892 |
background: var(--bg-primary);
|
893 |
+
border: 1px solid ${borderColor};
|
894 |
border-radius: 8px;
|
895 |
padding: 16px;
|
896 |
box-shadow: var(--shadow-lg);
|
897 |
z-index: 1000;
|
898 |
+
max-width: 350px;
|
899 |
color: var(--text-primary);
|
900 |
+
animation: slideInRight 0.3s ease;
|
901 |
`;
|
902 |
|
|
|
|
|
903 |
notification.innerHTML = `
|
904 |
+
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
905 |
+
<i class="${iconClass}" style="color: ${iconColor}; font-size: 18px; margin-top: 2px; flex-shrink: 0;"></i>
|
906 |
+
<div style="flex: 1; min-width: 0;">
|
907 |
+
${title ? `<div style="font-weight: 600; margin-bottom: 4px; color: var(--text-primary);">${title}</div>` : ''}
|
908 |
+
${message ? `<div style="font-size: 14px; color: var(--text-secondary); line-height: 1.4;">${message}</div>` : ''}
|
909 |
+
</div>
|
910 |
</div>
|
|
|
|
|
|
|
911 |
`;
|
912 |
|
913 |
+
// Add CSS animation
|
914 |
+
const style = document.createElement('style');
|
915 |
+
style.textContent = `
|
916 |
+
@keyframes slideInRight {
|
917 |
+
from {
|
918 |
+
transform: translateX(100%);
|
919 |
+
opacity: 0;
|
920 |
+
}
|
921 |
+
to {
|
922 |
+
transform: translateX(0);
|
923 |
+
opacity: 1;
|
924 |
+
}
|
925 |
+
}
|
926 |
+
`;
|
927 |
+
document.head.appendChild(style);
|
928 |
+
|
929 |
document.body.appendChild(notification);
|
930 |
|
931 |
+
// Remove notification after duration
|
932 |
+
if (duration > 0) {
|
933 |
+
setTimeout(() => {
|
934 |
+
if (notification.parentNode) {
|
935 |
+
notification.style.animation = 'slideInRight 0.3s ease reverse';
|
936 |
+
setTimeout(() => {
|
937 |
+
if (notification.parentNode) {
|
938 |
+
notification.parentNode.removeChild(notification);
|
939 |
+
}
|
940 |
+
}, 300);
|
941 |
+
}
|
942 |
+
}, duration);
|
943 |
+
}
|
944 |
+
|
945 |
+
return notification;
|
946 |
+
}
|
947 |
+
|
948 |
+
// Convenience methods for different notification types
|
949 |
+
showDateLimitNotification(message) {
|
950 |
+
this.showNotification({
|
951 |
+
type: 'warning',
|
952 |
+
title: 'Date Limit',
|
953 |
+
message: message,
|
954 |
+
icon: 'fas fa-calendar-times'
|
955 |
+
});
|
956 |
+
}
|
957 |
+
|
958 |
+
showNoPapersNotification(date) {
|
959 |
+
this.showNotification({
|
960 |
+
type: 'info',
|
961 |
+
title: 'No Papers Found',
|
962 |
+
message: `No papers available for ${date}. Try a different date.`,
|
963 |
+
icon: 'fas fa-search'
|
964 |
+
});
|
965 |
+
}
|
966 |
+
|
967 |
+
showRedirectNotification(requestedDate, actualDate) {
|
968 |
+
this.showNotification({
|
969 |
+
type: 'info',
|
970 |
+
title: 'Date Redirected',
|
971 |
+
message: `Papers for ${requestedDate} not available. Showing papers for ${actualDate}.`,
|
972 |
+
icon: 'fas fa-arrow-right'
|
973 |
+
});
|
974 |
+
}
|
975 |
+
|
976 |
+
showCacheNotification(cachedAt) {
|
977 |
+
const cacheTime = new Date(cachedAt).toLocaleTimeString();
|
978 |
+
this.showNotification({
|
979 |
+
type: 'info',
|
980 |
+
title: 'Cached Data',
|
981 |
+
message: `Showing cached data from ${cacheTime}`,
|
982 |
+
icon: 'fas fa-database',
|
983 |
+
duration: 3000
|
984 |
+
});
|
985 |
}
|
986 |
}
|
987 |
|
frontend/paper.html
CHANGED
@@ -81,7 +81,7 @@
|
|
81 |
</div>
|
82 |
</main>
|
83 |
|
84 |
-
<script src="/paper.js"></script>
|
85 |
</body>
|
86 |
</html>
|
87 |
|
|
|
81 |
</div>
|
82 |
</main>
|
83 |
|
84 |
+
<script src="/paper.js?v=2"></script>
|
85 |
</body>
|
86 |
</html>
|
87 |
|
frontend/paper.js
CHANGED
@@ -77,7 +77,7 @@ class PaperEvaluationRenderer {
|
|
77 |
}
|
78 |
}
|
79 |
|
80 |
-
renderMetaGrid(meta) {
|
81 |
const metaGrid = document.getElementById('metaGrid');
|
82 |
if (!metaGrid) return;
|
83 |
|
@@ -85,6 +85,7 @@ class PaperEvaluationRenderer {
|
|
85 |
{ label: 'Assessed At', value: meta.assessed_at || '-', icon: 'fas fa-calendar' },
|
86 |
{ label: 'Model', value: meta.model || '-', icon: 'fas fa-robot' },
|
87 |
{ label: 'Version', value: meta.version || '-', icon: 'fas fa-tag' },
|
|
|
88 |
{ label: 'Paper Path', value: meta.paper_path || '-', icon: 'fas fa-file-pdf', isLink: true }
|
89 |
];
|
90 |
|
@@ -136,7 +137,7 @@ class PaperEvaluationRenderer {
|
|
136 |
`;
|
137 |
}
|
138 |
|
139 |
-
renderContent(json) {
|
140 |
const contentEl = document.getElementById('content');
|
141 |
const titleEl = document.getElementById('title');
|
142 |
if (!contentEl || !titleEl) return;
|
@@ -144,11 +145,30 @@ class PaperEvaluationRenderer {
|
|
144 |
const meta = json.metadata || {};
|
145 |
const paperId = getParam('id');
|
146 |
|
147 |
-
//
|
148 |
-
|
|
|
|
|
149 |
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
|
153 |
// Executive Summary - styled like Hugging Face abstract
|
154 |
const execSummary = json.executive_summary ? `
|
@@ -494,8 +514,8 @@ class PaperEvaluationRenderer {
|
|
494 |
}
|
495 |
}
|
496 |
|
497 |
-
render(json) {
|
498 |
-
this.renderContent(json);
|
499 |
this.updateRadarChart(json);
|
500 |
}
|
501 |
}
|
@@ -548,7 +568,7 @@ class PaperEvaluationApp {
|
|
548 |
}
|
549 |
|
550 |
console.log('Rendering evaluation...');
|
551 |
-
this.renderer.render(json);
|
552 |
console.log('Evaluation rendered successfully');
|
553 |
|
554 |
} catch (error) {
|
|
|
77 |
}
|
78 |
}
|
79 |
|
80 |
+
renderMetaGrid(meta, paperAuthors = '') {
|
81 |
const metaGrid = document.getElementById('metaGrid');
|
82 |
if (!metaGrid) return;
|
83 |
|
|
|
85 |
{ label: 'Assessed At', value: meta.assessed_at || '-', icon: 'fas fa-calendar' },
|
86 |
{ label: 'Model', value: meta.model || '-', icon: 'fas fa-robot' },
|
87 |
{ label: 'Version', value: meta.version || '-', icon: 'fas fa-tag' },
|
88 |
+
{ label: 'Authors', value: paperAuthors || '-', icon: 'fas fa-users' },
|
89 |
{ label: 'Paper Path', value: meta.paper_path || '-', icon: 'fas fa-file-pdf', isLink: true }
|
90 |
];
|
91 |
|
|
|
137 |
`;
|
138 |
}
|
139 |
|
140 |
+
async renderContent(json) {
|
141 |
const contentEl = document.getElementById('content');
|
142 |
const titleEl = document.getElementById('title');
|
143 |
if (!contentEl || !titleEl) return;
|
|
|
145 |
const meta = json.metadata || {};
|
146 |
const paperId = getParam('id');
|
147 |
|
148 |
+
// Fetch paper details from database
|
149 |
+
let paperTitle = `Paper Evaluation - ${paperId}`;
|
150 |
+
let paperAuthors = '';
|
151 |
+
let paperAbstract = '';
|
152 |
|
153 |
+
try {
|
154 |
+
const response = await fetch(`/api/paper/${encodeURIComponent(paperId)}`);
|
155 |
+
if (response.ok) {
|
156 |
+
const paperData = await response.json();
|
157 |
+
if (paperData.title) {
|
158 |
+
paperTitle = paperData.title;
|
159 |
+
paperAuthors = paperData.authors || '';
|
160 |
+
paperAbstract = paperData.abstract || '';
|
161 |
+
}
|
162 |
+
}
|
163 |
+
} catch (error) {
|
164 |
+
console.error('Error fetching paper details:', error);
|
165 |
+
}
|
166 |
+
|
167 |
+
// Update title with actual paper title
|
168 |
+
titleEl.textContent = paperTitle;
|
169 |
+
|
170 |
+
// Render meta grid with paper info
|
171 |
+
this.renderMetaGrid(meta, paperAuthors);
|
172 |
|
173 |
// Executive Summary - styled like Hugging Face abstract
|
174 |
const execSummary = json.executive_summary ? `
|
|
|
514 |
}
|
515 |
}
|
516 |
|
517 |
+
async render(json) {
|
518 |
+
await this.renderContent(json);
|
519 |
this.updateRadarChart(json);
|
520 |
}
|
521 |
}
|
|
|
568 |
}
|
569 |
|
570 |
console.log('Rendering evaluation...');
|
571 |
+
await this.renderer.render(json);
|
572 |
console.log('Evaluation rendered successfully');
|
573 |
|
574 |
} catch (error) {
|
frontend/styles.css
CHANGED
@@ -14,6 +14,7 @@
|
|
14 |
--accent-success: #10b981;
|
15 |
--accent-warning: #f59e0b;
|
16 |
--accent-danger: #ef4444;
|
|
|
17 |
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
18 |
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
19 |
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
@@ -35,6 +36,7 @@
|
|
35 |
--accent-success: #34d399;
|
36 |
--accent-warning: #fbbf24;
|
37 |
--accent-danger: #f87171;
|
|
|
38 |
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
39 |
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
40 |
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
@@ -328,6 +330,7 @@ body {
|
|
328 |
display: grid;
|
329 |
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
330 |
gap: 24px;
|
|
|
331 |
}
|
332 |
|
333 |
/* Hugging Face Style Paper Cards */
|
@@ -340,6 +343,7 @@ body {
|
|
340 |
border: 1px solid var(--border-light);
|
341 |
background-color: var(--bg-primary);
|
342 |
transition: all 0.2s ease;
|
|
|
343 |
}
|
344 |
|
345 |
.hf-paper-card:hover {
|
@@ -398,6 +402,46 @@ body {
|
|
398 |
color: var(--text-secondary);
|
399 |
}
|
400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
401 |
.submitter-avatar-img {
|
402 |
width: 10px;
|
403 |
height: 10px;
|
@@ -429,6 +473,7 @@ body {
|
|
429 |
display: flex;
|
430 |
padding: 32px 24px 24px 24px;
|
431 |
gap: 24px;
|
|
|
432 |
}
|
433 |
|
434 |
/* Upvote Section */
|
@@ -489,6 +534,9 @@ body {
|
|
489 |
/* Paper Info */
|
490 |
.paper-info {
|
491 |
width: 100%;
|
|
|
|
|
|
|
492 |
}
|
493 |
|
494 |
.paper-title {
|
@@ -519,6 +567,7 @@ body {
|
|
519 |
align-items: center;
|
520 |
justify-content: space-between;
|
521 |
gap: 8px;
|
|
|
522 |
}
|
523 |
|
524 |
.authors-section {
|
@@ -615,6 +664,7 @@ body {
|
|
615 |
padding: 0 24px 24px 24px;
|
616 |
display: flex;
|
617 |
gap: 8px;
|
|
|
618 |
}
|
619 |
|
620 |
.eval-button {
|
@@ -631,6 +681,8 @@ body {
|
|
631 |
text-decoration: none;
|
632 |
cursor: pointer;
|
633 |
transition: all 0.2s ease;
|
|
|
|
|
634 |
}
|
635 |
|
636 |
.eval-button:hover {
|
@@ -639,6 +691,148 @@ body {
|
|
639 |
border-color: var(--border-medium);
|
640 |
}
|
641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
642 |
.badge {
|
643 |
display: inline-flex;
|
644 |
align-items: center;
|
@@ -652,6 +846,62 @@ body {
|
|
652 |
font-weight: 500;
|
653 |
}
|
654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
655 |
/* Responsive Design */
|
656 |
@media (max-width: 1024px) {
|
657 |
.header-container {
|
@@ -665,6 +915,7 @@ body {
|
|
665 |
|
666 |
.cards-grid {
|
667 |
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
|
668 |
}
|
669 |
}
|
670 |
|
@@ -687,6 +938,7 @@ body {
|
|
687 |
|
688 |
.cards-grid {
|
689 |
grid-template-columns: 1fr;
|
|
|
690 |
}
|
691 |
|
692 |
.paper-card {
|
@@ -763,54 +1015,132 @@ body {
|
|
763 |
}
|
764 |
|
765 |
.paper-header {
|
766 |
-
background
|
|
|
|
|
|
|
767 |
border: 1px solid var(--border-light);
|
768 |
-
border-radius:
|
769 |
-
padding:
|
770 |
-
margin-bottom:
|
771 |
-
box-shadow: var(--shadow-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
772 |
}
|
773 |
|
774 |
.paper-meta h1 {
|
775 |
-
font-size:
|
776 |
-
font-weight:
|
777 |
-
|
778 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
779 |
}
|
780 |
|
781 |
.meta-grid {
|
782 |
display: grid;
|
783 |
-
grid-template-columns: repeat(auto-fit, minmax(
|
784 |
-
gap:
|
|
|
|
|
785 |
}
|
786 |
|
787 |
.meta-item {
|
|
|
|
|
|
|
|
|
|
|
788 |
display: flex;
|
789 |
flex-direction: column;
|
790 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
791 |
}
|
792 |
|
793 |
.meta-label {
|
794 |
-
font-size:
|
795 |
-
font-weight:
|
796 |
color: var(--text-muted);
|
797 |
text-transform: uppercase;
|
798 |
-
letter-spacing: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
799 |
}
|
800 |
|
801 |
.meta-value {
|
802 |
-
font-size:
|
803 |
color: var(--text-primary);
|
804 |
-
font-weight:
|
|
|
805 |
}
|
806 |
|
807 |
.meta-value a {
|
808 |
color: var(--accent-primary);
|
809 |
text-decoration: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
810 |
}
|
811 |
|
812 |
.meta-value a:hover {
|
813 |
-
|
|
|
|
|
814 |
}
|
815 |
|
816 |
.content-layout {
|
@@ -1372,4 +1702,55 @@ body {
|
|
1372 |
}
|
1373 |
}
|
1374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1375 |
|
|
|
14 |
--accent-success: #10b981;
|
15 |
--accent-warning: #f59e0b;
|
16 |
--accent-danger: #ef4444;
|
17 |
+
--accent-info: #3b82f6;
|
18 |
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
19 |
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
20 |
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
|
36 |
--accent-success: #34d399;
|
37 |
--accent-warning: #fbbf24;
|
38 |
--accent-danger: #f87171;
|
39 |
+
--accent-info: #60a5fa;
|
40 |
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
41 |
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
42 |
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
|
330 |
display: grid;
|
331 |
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
332 |
gap: 24px;
|
333 |
+
align-items: stretch;
|
334 |
}
|
335 |
|
336 |
/* Hugging Face Style Paper Cards */
|
|
|
343 |
border: 1px solid var(--border-light);
|
344 |
background-color: var(--bg-primary);
|
345 |
transition: all 0.2s ease;
|
346 |
+
height: 100%;
|
347 |
}
|
348 |
|
349 |
.hf-paper-card:hover {
|
|
|
402 |
color: var(--text-secondary);
|
403 |
}
|
404 |
|
405 |
+
/* Score badge */
|
406 |
+
.score-badge {
|
407 |
+
position: absolute;
|
408 |
+
right: 16px;
|
409 |
+
bottom: 16px;
|
410 |
+
display: flex;
|
411 |
+
flex-direction: column;
|
412 |
+
align-items: center;
|
413 |
+
justify-content: center;
|
414 |
+
border-radius: 12px;
|
415 |
+
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
416 |
+
color: white;
|
417 |
+
padding: 8px 12px;
|
418 |
+
min-width: 60px;
|
419 |
+
box-shadow: var(--shadow-lg);
|
420 |
+
z-index: 20;
|
421 |
+
transition: all 0.2s ease;
|
422 |
+
cursor: pointer;
|
423 |
+
}
|
424 |
+
|
425 |
+
.score-badge:hover {
|
426 |
+
transform: translateY(-2px);
|
427 |
+
box-shadow: var(--shadow-xl);
|
428 |
+
}
|
429 |
+
|
430 |
+
.score-badge .score-number {
|
431 |
+
font-size: 20px;
|
432 |
+
font-weight: 800;
|
433 |
+
line-height: 1;
|
434 |
+
margin-bottom: 2px;
|
435 |
+
}
|
436 |
+
|
437 |
+
.score-badge .score-label {
|
438 |
+
font-size: 10px;
|
439 |
+
font-weight: 600;
|
440 |
+
opacity: 0.9;
|
441 |
+
text-transform: uppercase;
|
442 |
+
letter-spacing: 0.5px;
|
443 |
+
}
|
444 |
+
|
445 |
.submitter-avatar-img {
|
446 |
width: 10px;
|
447 |
height: 10px;
|
|
|
473 |
display: flex;
|
474 |
padding: 32px 24px 24px 24px;
|
475 |
gap: 24px;
|
476 |
+
flex: 1;
|
477 |
}
|
478 |
|
479 |
/* Upvote Section */
|
|
|
534 |
/* Paper Info */
|
535 |
.paper-info {
|
536 |
width: 100%;
|
537 |
+
display: flex;
|
538 |
+
flex-direction: column;
|
539 |
+
flex: 1;
|
540 |
}
|
541 |
|
542 |
.paper-title {
|
|
|
567 |
align-items: center;
|
568 |
justify-content: space-between;
|
569 |
gap: 8px;
|
570 |
+
margin-top: auto;
|
571 |
}
|
572 |
|
573 |
.authors-section {
|
|
|
664 |
padding: 0 24px 24px 24px;
|
665 |
display: flex;
|
666 |
gap: 8px;
|
667 |
+
margin-top: auto;
|
668 |
}
|
669 |
|
670 |
.eval-button {
|
|
|
681 |
text-decoration: none;
|
682 |
cursor: pointer;
|
683 |
transition: all 0.2s ease;
|
684 |
+
min-width: 100px;
|
685 |
+
justify-content: center;
|
686 |
}
|
687 |
|
688 |
.eval-button:hover {
|
|
|
691 |
border-color: var(--border-medium);
|
692 |
}
|
693 |
|
694 |
+
.eval-button:disabled {
|
695 |
+
opacity: 0.6;
|
696 |
+
cursor: not-allowed;
|
697 |
+
}
|
698 |
+
|
699 |
+
/* Evaluate state (green) */
|
700 |
+
.eval-button.evaluate-state {
|
701 |
+
color: var(--accent-success);
|
702 |
+
border-color: var(--accent-success);
|
703 |
+
}
|
704 |
+
|
705 |
+
.eval-button.evaluate-state:hover {
|
706 |
+
background-color: var(--accent-success);
|
707 |
+
color: white;
|
708 |
+
}
|
709 |
+
|
710 |
+
/* Evaluation state (blue) */
|
711 |
+
.eval-button.evaluation-state {
|
712 |
+
color: var(--accent-primary);
|
713 |
+
border-color: var(--accent-primary);
|
714 |
+
}
|
715 |
+
|
716 |
+
.eval-button.evaluation-state:hover {
|
717 |
+
background-color: var(--accent-primary);
|
718 |
+
color: white;
|
719 |
+
}
|
720 |
+
|
721 |
+
/* Evaluating state (orange) */
|
722 |
+
.eval-button.evaluating-state {
|
723 |
+
color: var(--accent-warning);
|
724 |
+
border-color: var(--accent-warning);
|
725 |
+
position: relative;
|
726 |
+
}
|
727 |
+
|
728 |
+
.eval-button.evaluating-state::after {
|
729 |
+
content: '';
|
730 |
+
position: absolute;
|
731 |
+
top: 50%;
|
732 |
+
right: 8px;
|
733 |
+
width: 12px;
|
734 |
+
height: 12px;
|
735 |
+
border: 2px solid transparent;
|
736 |
+
border-top: 2px solid var(--accent-warning);
|
737 |
+
border-radius: 50%;
|
738 |
+
transform: translateY(-50%);
|
739 |
+
animation: spin 1s linear infinite;
|
740 |
+
}
|
741 |
+
|
742 |
+
@keyframes spin {
|
743 |
+
0% { transform: translateY(-50%) rotate(0deg); }
|
744 |
+
100% { transform: translateY(-50%) rotate(360deg); }
|
745 |
+
}
|
746 |
+
|
747 |
+
/* Started state (green) */
|
748 |
+
.eval-button.started-state {
|
749 |
+
color: var(--accent-success);
|
750 |
+
border-color: var(--accent-success);
|
751 |
+
position: relative;
|
752 |
+
}
|
753 |
+
|
754 |
+
.eval-button.started-state::after {
|
755 |
+
content: '';
|
756 |
+
position: absolute;
|
757 |
+
top: 50%;
|
758 |
+
right: 8px;
|
759 |
+
width: 8px;
|
760 |
+
height: 8px;
|
761 |
+
background-color: var(--accent-success);
|
762 |
+
border-radius: 50%;
|
763 |
+
transform: translateY(-50%);
|
764 |
+
animation: pulse 1.5s ease-in-out infinite;
|
765 |
+
}
|
766 |
+
|
767 |
+
@keyframes pulse {
|
768 |
+
0% {
|
769 |
+
opacity: 1;
|
770 |
+
transform: translateY(-50%) scale(1);
|
771 |
+
}
|
772 |
+
50% {
|
773 |
+
opacity: 0.5;
|
774 |
+
transform: translateY(-50%) scale(1.2);
|
775 |
+
}
|
776 |
+
100% {
|
777 |
+
opacity: 1;
|
778 |
+
transform: translateY(-50%) scale(1);
|
779 |
+
}
|
780 |
+
}
|
781 |
+
|
782 |
+
/* Processing state (blue) */
|
783 |
+
.eval-button.processing-state {
|
784 |
+
color: var(--accent-primary);
|
785 |
+
border-color: var(--accent-primary);
|
786 |
+
position: relative;
|
787 |
+
}
|
788 |
+
|
789 |
+
.eval-button.processing-state::after {
|
790 |
+
content: '';
|
791 |
+
position: absolute;
|
792 |
+
top: 50%;
|
793 |
+
right: 8px;
|
794 |
+
width: 10px;
|
795 |
+
height: 10px;
|
796 |
+
background-color: var(--accent-primary);
|
797 |
+
border-radius: 50%;
|
798 |
+
transform: translateY(-50%);
|
799 |
+
animation: bounce 1s ease-in-out infinite;
|
800 |
+
}
|
801 |
+
|
802 |
+
@keyframes bounce {
|
803 |
+
0%, 20%, 50%, 80%, 100% {
|
804 |
+
transform: translateY(-50%) scale(1);
|
805 |
+
}
|
806 |
+
40% {
|
807 |
+
transform: translateY(-50%) scale(1.1);
|
808 |
+
}
|
809 |
+
60% {
|
810 |
+
transform: translateY(-50%) scale(0.9);
|
811 |
+
}
|
812 |
+
}
|
813 |
+
|
814 |
+
/* Error state (red) */
|
815 |
+
.eval-button.error-state {
|
816 |
+
color: var(--accent-danger);
|
817 |
+
border-color: var(--accent-danger);
|
818 |
+
}
|
819 |
+
|
820 |
+
/* Timeout state (gray) */
|
821 |
+
.eval-button.timeout-state {
|
822 |
+
color: var(--text-muted);
|
823 |
+
border-color: var(--text-muted);
|
824 |
+
}
|
825 |
+
|
826 |
+
/* Spinner animation */
|
827 |
+
.eval-button .fa-spinner {
|
828 |
+
animation: spin 1s linear infinite;
|
829 |
+
}
|
830 |
+
|
831 |
+
@keyframes spin {
|
832 |
+
from { transform: rotate(0deg); }
|
833 |
+
to { transform: rotate(360deg); }
|
834 |
+
}
|
835 |
+
|
836 |
.badge {
|
837 |
display: inline-flex;
|
838 |
align-items: center;
|
|
|
846 |
font-weight: 500;
|
847 |
}
|
848 |
|
849 |
+
/* Loading Animation */
|
850 |
+
.loading-overlay {
|
851 |
+
position: fixed;
|
852 |
+
top: 0;
|
853 |
+
left: 0;
|
854 |
+
width: 100%;
|
855 |
+
height: 100%;
|
856 |
+
background-color: rgba(255, 255, 255, 0.8);
|
857 |
+
backdrop-filter: blur(4px);
|
858 |
+
display: flex;
|
859 |
+
align-items: center;
|
860 |
+
justify-content: center;
|
861 |
+
z-index: 9999;
|
862 |
+
opacity: 0;
|
863 |
+
visibility: hidden;
|
864 |
+
transition: all 0.3s ease;
|
865 |
+
}
|
866 |
+
|
867 |
+
[data-theme="dark"] .loading-overlay {
|
868 |
+
background-color: rgba(15, 23, 42, 0.8);
|
869 |
+
}
|
870 |
+
|
871 |
+
.loading-overlay.show {
|
872 |
+
opacity: 1;
|
873 |
+
visibility: visible;
|
874 |
+
}
|
875 |
+
|
876 |
+
.loading-spinner {
|
877 |
+
display: flex;
|
878 |
+
flex-direction: column;
|
879 |
+
align-items: center;
|
880 |
+
gap: 16px;
|
881 |
+
}
|
882 |
+
|
883 |
+
.spinner {
|
884 |
+
width: 48px;
|
885 |
+
height: 48px;
|
886 |
+
border: 4px solid var(--border-light);
|
887 |
+
border-top: 4px solid var(--accent-primary);
|
888 |
+
border-radius: 50%;
|
889 |
+
animation: spin 1s linear infinite;
|
890 |
+
}
|
891 |
+
|
892 |
+
.loading-text {
|
893 |
+
font-size: 16px;
|
894 |
+
font-weight: 500;
|
895 |
+
color: var(--text-primary);
|
896 |
+
text-align: center;
|
897 |
+
}
|
898 |
+
|
899 |
+
.loading-subtext {
|
900 |
+
font-size: 14px;
|
901 |
+
color: var(--text-secondary);
|
902 |
+
text-align: center;
|
903 |
+
}
|
904 |
+
|
905 |
/* Responsive Design */
|
906 |
@media (max-width: 1024px) {
|
907 |
.header-container {
|
|
|
915 |
|
916 |
.cards-grid {
|
917 |
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
918 |
+
align-items: stretch;
|
919 |
}
|
920 |
}
|
921 |
|
|
|
938 |
|
939 |
.cards-grid {
|
940 |
grid-template-columns: 1fr;
|
941 |
+
align-items: stretch;
|
942 |
}
|
943 |
|
944 |
.paper-card {
|
|
|
1015 |
}
|
1016 |
|
1017 |
.paper-header {
|
1018 |
+
background: linear-gradient(135deg,
|
1019 |
+
var(--bg-primary) 0%,
|
1020 |
+
var(--bg-secondary) 50%,
|
1021 |
+
var(--bg-tertiary) 100%);
|
1022 |
border: 1px solid var(--border-light);
|
1023 |
+
border-radius: 20px;
|
1024 |
+
padding: 40px;
|
1025 |
+
margin-bottom: 32px;
|
1026 |
+
box-shadow: var(--shadow-lg);
|
1027 |
+
position: relative;
|
1028 |
+
overflow: hidden;
|
1029 |
+
}
|
1030 |
+
|
1031 |
+
.paper-header::before {
|
1032 |
+
content: '';
|
1033 |
+
position: absolute;
|
1034 |
+
top: 0;
|
1035 |
+
left: 0;
|
1036 |
+
right: 0;
|
1037 |
+
height: 4px;
|
1038 |
+
background: linear-gradient(90deg,
|
1039 |
+
var(--accent-primary) 0%,
|
1040 |
+
var(--accent-secondary) 50%,
|
1041 |
+
var(--accent-success) 100%);
|
1042 |
+
}
|
1043 |
+
|
1044 |
+
.paper-header::after {
|
1045 |
+
content: '';
|
1046 |
+
position: absolute;
|
1047 |
+
top: -50%;
|
1048 |
+
right: -50%;
|
1049 |
+
width: 200%;
|
1050 |
+
height: 200%;
|
1051 |
+
background: radial-gradient(circle,
|
1052 |
+
rgba(59, 130, 246, 0.03) 0%,
|
1053 |
+
rgba(6, 182, 212, 0.02) 50%,
|
1054 |
+
transparent 100%);
|
1055 |
+
pointer-events: none;
|
1056 |
}
|
1057 |
|
1058 |
.paper-meta h1 {
|
1059 |
+
font-size: 32px;
|
1060 |
+
font-weight: 900;
|
1061 |
+
background: linear-gradient(135deg,
|
1062 |
+
var(--text-primary) 0%,
|
1063 |
+
var(--accent-primary) 50%,
|
1064 |
+
var(--accent-secondary) 100%);
|
1065 |
+
-webkit-background-clip: text;
|
1066 |
+
-webkit-text-fill-color: transparent;
|
1067 |
+
background-clip: text;
|
1068 |
+
margin-bottom: 32px;
|
1069 |
+
position: relative;
|
1070 |
+
z-index: 1;
|
1071 |
+
letter-spacing: -0.5px;
|
1072 |
}
|
1073 |
|
1074 |
.meta-grid {
|
1075 |
display: grid;
|
1076 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
1077 |
+
gap: 20px;
|
1078 |
+
position: relative;
|
1079 |
+
z-index: 1;
|
1080 |
}
|
1081 |
|
1082 |
.meta-item {
|
1083 |
+
background: rgba(255, 255, 255, 0.7);
|
1084 |
+
backdrop-filter: blur(10px);
|
1085 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
1086 |
+
border-radius: 12px;
|
1087 |
+
padding: 16px;
|
1088 |
display: flex;
|
1089 |
flex-direction: column;
|
1090 |
+
gap: 8px;
|
1091 |
+
transition: all 0.3s ease;
|
1092 |
+
box-shadow: var(--shadow-sm);
|
1093 |
+
}
|
1094 |
+
|
1095 |
+
[data-theme="dark"] .meta-item {
|
1096 |
+
background: rgba(30, 41, 59, 0.7);
|
1097 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1098 |
+
}
|
1099 |
+
|
1100 |
+
.meta-item:hover {
|
1101 |
+
transform: translateY(-2px);
|
1102 |
+
box-shadow: var(--shadow-md);
|
1103 |
+
border-color: var(--accent-primary);
|
1104 |
}
|
1105 |
|
1106 |
.meta-label {
|
1107 |
+
font-size: 11px;
|
1108 |
+
font-weight: 700;
|
1109 |
color: var(--text-muted);
|
1110 |
text-transform: uppercase;
|
1111 |
+
letter-spacing: 0.8px;
|
1112 |
+
display: flex;
|
1113 |
+
align-items: center;
|
1114 |
+
gap: 6px;
|
1115 |
+
}
|
1116 |
+
|
1117 |
+
.meta-label i {
|
1118 |
+
color: var(--accent-primary);
|
1119 |
+
font-size: 12px;
|
1120 |
}
|
1121 |
|
1122 |
.meta-value {
|
1123 |
+
font-size: 15px;
|
1124 |
color: var(--text-primary);
|
1125 |
+
font-weight: 600;
|
1126 |
+
line-height: 1.4;
|
1127 |
}
|
1128 |
|
1129 |
.meta-value a {
|
1130 |
color: var(--accent-primary);
|
1131 |
text-decoration: none;
|
1132 |
+
font-weight: 600;
|
1133 |
+
transition: all 0.3s ease;
|
1134 |
+
padding: 4px 8px;
|
1135 |
+
border-radius: 6px;
|
1136 |
+
background: rgba(59, 130, 246, 0.1);
|
1137 |
+
display: inline-block;
|
1138 |
}
|
1139 |
|
1140 |
.meta-value a:hover {
|
1141 |
+
background: rgba(59, 130, 246, 0.2);
|
1142 |
+
transform: translateY(-1px);
|
1143 |
+
box-shadow: var(--shadow-sm);
|
1144 |
}
|
1145 |
|
1146 |
.content-layout {
|
|
|
1702 |
}
|
1703 |
}
|
1704 |
|
1705 |
+
/* Responsive Design for Paper Header */
|
1706 |
+
@media (max-width: 768px) {
|
1707 |
+
.paper-header {
|
1708 |
+
padding: 24px;
|
1709 |
+
margin-bottom: 24px;
|
1710 |
+
border-radius: 16px;
|
1711 |
+
}
|
1712 |
+
|
1713 |
+
.paper-meta h1 {
|
1714 |
+
font-size: 24px;
|
1715 |
+
margin-bottom: 24px;
|
1716 |
+
}
|
1717 |
+
|
1718 |
+
.meta-grid {
|
1719 |
+
grid-template-columns: 1fr;
|
1720 |
+
gap: 16px;
|
1721 |
+
}
|
1722 |
+
|
1723 |
+
.meta-item {
|
1724 |
+
padding: 12px;
|
1725 |
+
}
|
1726 |
+
|
1727 |
+
.meta-value {
|
1728 |
+
font-size: 14px;
|
1729 |
+
}
|
1730 |
+
}
|
1731 |
+
|
1732 |
+
@media (max-width: 480px) {
|
1733 |
+
.paper-header {
|
1734 |
+
padding: 20px;
|
1735 |
+
margin-bottom: 20px;
|
1736 |
+
}
|
1737 |
+
|
1738 |
+
.paper-meta h1 {
|
1739 |
+
font-size: 20px;
|
1740 |
+
margin-bottom: 20px;
|
1741 |
+
}
|
1742 |
+
|
1743 |
+
.meta-item {
|
1744 |
+
padding: 10px;
|
1745 |
+
}
|
1746 |
+
|
1747 |
+
.meta-label {
|
1748 |
+
font-size: 10px;
|
1749 |
+
}
|
1750 |
+
|
1751 |
+
.meta-value {
|
1752 |
+
font-size: 13px;
|
1753 |
+
}
|
1754 |
+
}
|
1755 |
+
|
1756 |
|
server.py
DELETED
@@ -1,731 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import re
|
3 |
-
import glob
|
4 |
-
import json
|
5 |
-
import sqlite3
|
6 |
-
from datetime import date, datetime, timedelta
|
7 |
-
from typing import Any, Dict, List, Optional
|
8 |
-
from contextlib import contextmanager
|
9 |
-
|
10 |
-
from fastapi import FastAPI, HTTPException
|
11 |
-
from fastapi.middleware.cors import CORSMiddleware
|
12 |
-
from fastapi.responses import FileResponse
|
13 |
-
from fastapi.staticfiles import StaticFiles
|
14 |
-
from dotenv import load_dotenv
|
15 |
-
import httpx
|
16 |
-
from bs4 import BeautifulSoup
|
17 |
-
|
18 |
-
|
19 |
-
# Load environment variables
|
20 |
-
load_dotenv()
|
21 |
-
|
22 |
-
# Get API key from HF Spaces secrets
|
23 |
-
def get_anthropic_api_key() -> Optional[str]:
|
24 |
-
"""Get Anthropic API key, prioritize HF Spaces secrets"""
|
25 |
-
# First try to get from HF Spaces secrets
|
26 |
-
hf_secret = os.getenv("HF_SECRET_ANTHROPIC_API_KEY")
|
27 |
-
if hf_secret:
|
28 |
-
return hf_secret
|
29 |
-
|
30 |
-
# Then try to get from environment variables
|
31 |
-
env_key = os.getenv("ANTHROPIC_API_KEY")
|
32 |
-
if env_key:
|
33 |
-
return env_key
|
34 |
-
|
35 |
-
return None
|
36 |
-
|
37 |
-
|
38 |
-
def get_project_root() -> str:
|
39 |
-
return os.path.dirname(os.path.abspath(__file__))
|
40 |
-
|
41 |
-
|
42 |
-
PROJECT_ROOT = get_project_root()
|
43 |
-
DEFAULT_WORKDIR = os.getenv("WORKDIR", os.path.join(PROJECT_ROOT, "workdir"))
|
44 |
-
DB_PATH = os.path.join(PROJECT_ROOT, "papers_cache.db")
|
45 |
-
|
46 |
-
# Database management
|
47 |
-
class PapersDatabase:
|
48 |
-
def __init__(self, db_path: str):
|
49 |
-
self.db_path = db_path
|
50 |
-
self.init_database()
|
51 |
-
|
52 |
-
def init_database(self):
|
53 |
-
"""Initialize the database with required tables"""
|
54 |
-
with self.get_connection() as conn:
|
55 |
-
cursor = conn.cursor()
|
56 |
-
|
57 |
-
# Create papers cache table
|
58 |
-
cursor.execute('''
|
59 |
-
CREATE TABLE IF NOT EXISTS papers_cache (
|
60 |
-
date_str TEXT PRIMARY KEY,
|
61 |
-
html_content TEXT NOT NULL,
|
62 |
-
parsed_cards TEXT NOT NULL,
|
63 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
64 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
65 |
-
)
|
66 |
-
''')
|
67 |
-
|
68 |
-
# Create latest_date table to track the most recent available date
|
69 |
-
cursor.execute('''
|
70 |
-
CREATE TABLE IF NOT EXISTS latest_date (
|
71 |
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
72 |
-
date_str TEXT NOT NULL,
|
73 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
74 |
-
)
|
75 |
-
''')
|
76 |
-
|
77 |
-
# Insert default latest_date record if it doesn't exist
|
78 |
-
cursor.execute('''
|
79 |
-
INSERT OR IGNORE INTO latest_date (id, date_str)
|
80 |
-
VALUES (1, ?)
|
81 |
-
''', (date.today().isoformat(),))
|
82 |
-
|
83 |
-
conn.commit()
|
84 |
-
|
85 |
-
@contextmanager
|
86 |
-
def get_connection(self):
|
87 |
-
"""Context manager for database connections"""
|
88 |
-
conn = sqlite3.connect(self.db_path)
|
89 |
-
conn.row_factory = sqlite3.Row # Enable dict-like access
|
90 |
-
try:
|
91 |
-
yield conn
|
92 |
-
finally:
|
93 |
-
conn.close()
|
94 |
-
|
95 |
-
def get_cached_papers(self, date_str: str) -> Optional[Dict[str, Any]]:
|
96 |
-
"""Get cached papers for a specific date"""
|
97 |
-
with self.get_connection() as conn:
|
98 |
-
cursor = conn.cursor()
|
99 |
-
cursor.execute('''
|
100 |
-
SELECT parsed_cards, created_at
|
101 |
-
FROM papers_cache
|
102 |
-
WHERE date_str = ?
|
103 |
-
''', (date_str,))
|
104 |
-
|
105 |
-
row = cursor.fetchone()
|
106 |
-
if row:
|
107 |
-
return {
|
108 |
-
'cards': json.loads(row['parsed_cards']),
|
109 |
-
'cached_at': row['created_at']
|
110 |
-
}
|
111 |
-
return None
|
112 |
-
|
113 |
-
def cache_papers(self, date_str: str, html_content: str, parsed_cards: List[Dict[str, Any]]):
|
114 |
-
"""Cache papers for a specific date"""
|
115 |
-
with self.get_connection() as conn:
|
116 |
-
cursor = conn.cursor()
|
117 |
-
cursor.execute('''
|
118 |
-
INSERT OR REPLACE INTO papers_cache
|
119 |
-
(date_str, html_content, parsed_cards, updated_at)
|
120 |
-
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
121 |
-
''', (date_str, html_content, json.dumps(parsed_cards)))
|
122 |
-
conn.commit()
|
123 |
-
|
124 |
-
def get_latest_cached_date(self) -> Optional[str]:
|
125 |
-
"""Get the latest cached date"""
|
126 |
-
with self.get_connection() as conn:
|
127 |
-
cursor = conn.cursor()
|
128 |
-
cursor.execute('SELECT date_str FROM latest_date WHERE id = 1')
|
129 |
-
row = cursor.fetchone()
|
130 |
-
return row['date_str'] if row else None
|
131 |
-
|
132 |
-
def update_latest_date(self, date_str: str):
|
133 |
-
"""Update the latest available date"""
|
134 |
-
with self.get_connection() as conn:
|
135 |
-
cursor = conn.cursor()
|
136 |
-
cursor.execute('''
|
137 |
-
UPDATE latest_date
|
138 |
-
SET date_str = ?, updated_at = CURRENT_TIMESTAMP
|
139 |
-
WHERE id = 1
|
140 |
-
''', (date_str,))
|
141 |
-
conn.commit()
|
142 |
-
|
143 |
-
def is_cache_fresh(self, date_str: str, max_age_hours: int = 24) -> bool:
|
144 |
-
"""Check if cache is fresh (within max_age_hours)"""
|
145 |
-
with self.get_connection() as conn:
|
146 |
-
cursor = conn.cursor()
|
147 |
-
cursor.execute('''
|
148 |
-
SELECT updated_at
|
149 |
-
FROM papers_cache
|
150 |
-
WHERE date_str = ?
|
151 |
-
''', (date_str,))
|
152 |
-
|
153 |
-
row = cursor.fetchone()
|
154 |
-
if not row:
|
155 |
-
return False
|
156 |
-
|
157 |
-
cached_time = datetime.fromisoformat(row['updated_at'].replace('Z', '+00:00'))
|
158 |
-
age = datetime.now(cached_time.tzinfo) - cached_time
|
159 |
-
return age.total_seconds() < max_age_hours * 3600
|
160 |
-
|
161 |
-
def cleanup_old_cache(self, days_to_keep: int = 7):
|
162 |
-
"""Clean up old cache entries"""
|
163 |
-
cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).isoformat()
|
164 |
-
with self.get_connection() as conn:
|
165 |
-
cursor = conn.cursor()
|
166 |
-
cursor.execute('''
|
167 |
-
DELETE FROM papers_cache
|
168 |
-
WHERE updated_at < ?
|
169 |
-
''', (cutoff_date,))
|
170 |
-
conn.commit()
|
171 |
-
|
172 |
-
|
173 |
-
# Initialize database
|
174 |
-
db = PapersDatabase(DB_PATH)
|
175 |
-
|
176 |
-
|
177 |
-
app = FastAPI(title="PaperIndex Web")
|
178 |
-
|
179 |
-
# Local development: allow same-origin and localhost
|
180 |
-
app.add_middleware(
|
181 |
-
CORSMiddleware,
|
182 |
-
allow_origins=["*"],
|
183 |
-
allow_credentials=True,
|
184 |
-
allow_methods=["*"],
|
185 |
-
allow_headers=["*"],
|
186 |
-
)
|
187 |
-
|
188 |
-
|
189 |
-
# --- Utility functions ---
|
190 |
-
|
191 |
-
def ensure_workdir() -> str:
|
192 |
-
os.makedirs(DEFAULT_WORKDIR, exist_ok=True)
|
193 |
-
return DEFAULT_WORKDIR
|
194 |
-
|
195 |
-
|
196 |
-
def extract_arxiv_id(url: str) -> Optional[str]:
|
197 |
-
if not url:
|
198 |
-
return None
|
199 |
-
# Matches /abs/2508.05629, /pdf/2508.05629.pdf
|
200 |
-
m = re.search(r"arxiv\.org/(abs|pdf)/([0-9]{4}\.\d{4,5})(?:\.pdf)?", url)
|
201 |
-
if m:
|
202 |
-
return m.group(2)
|
203 |
-
return None
|
204 |
-
|
205 |
-
def extract_json_data(html: str) -> Dict[str, Any]:
|
206 |
-
"""Extract JSON data from the HTML page to get GitHub stars and other metadata."""
|
207 |
-
try:
|
208 |
-
soup = BeautifulSoup(html, "lxml")
|
209 |
-
|
210 |
-
# Look for GitHub stars in the HTML structure
|
211 |
-
# Based on the user's description, GitHub stars are displayed with SVG icons
|
212 |
-
# Look for SVG elements that might represent GitHub stars
|
213 |
-
svg_elements = soup.find_all("svg")
|
214 |
-
|
215 |
-
github_stars_map = {}
|
216 |
-
|
217 |
-
for svg in svg_elements:
|
218 |
-
# Look for GitHub-related SVG (usually has specific viewBox or path)
|
219 |
-
svg_html = str(svg)
|
220 |
-
if "github" in svg_html.lower() or "256 250" in svg_html: # GitHub icon viewBox
|
221 |
-
# Look for the star count near this SVG
|
222 |
-
parent = svg.parent
|
223 |
-
if parent:
|
224 |
-
# Look for numbers that might be star counts
|
225 |
-
text_content = parent.get_text()
|
226 |
-
numbers = re.findall(r'\b(\d+)\b', text_content)
|
227 |
-
if numbers:
|
228 |
-
# The number near a GitHub SVG is likely the star count
|
229 |
-
star_count = int(numbers[0])
|
230 |
-
# Try to find the paper title or ID to associate with
|
231 |
-
# Look for the closest article or card container
|
232 |
-
article = svg.find_parent("article")
|
233 |
-
if article:
|
234 |
-
title_elem = article.find("h3")
|
235 |
-
if title_elem:
|
236 |
-
paper_title = title_elem.get_text(strip=True)
|
237 |
-
github_stars_map[paper_title] = star_count
|
238 |
-
|
239 |
-
# Also look for any elements with GitHub-related text
|
240 |
-
github_text_elements = soup.find_all(string=lambda text: text and "github" in text.lower())
|
241 |
-
for text_elem in github_text_elements:
|
242 |
-
parent = text_elem.parent
|
243 |
-
if parent:
|
244 |
-
text_content = parent.get_text()
|
245 |
-
numbers = re.findall(r'\b(\d+)\b', text_content)
|
246 |
-
if numbers:
|
247 |
-
star_count = int(numbers[0])
|
248 |
-
# Try to find the paper title
|
249 |
-
article = parent.find_parent("article")
|
250 |
-
if article:
|
251 |
-
title_elem = article.find("h3")
|
252 |
-
if title_elem:
|
253 |
-
paper_title = title_elem.get_text(strip=True)
|
254 |
-
if paper_title not in github_stars_map:
|
255 |
-
github_stars_map[paper_title] = star_count
|
256 |
-
|
257 |
-
return {"github_stars_map": github_stars_map}
|
258 |
-
|
259 |
-
except Exception as e:
|
260 |
-
print(f"Error extracting JSON data: {e}")
|
261 |
-
|
262 |
-
return {"github_stars_map": {}}
|
263 |
-
|
264 |
-
|
265 |
-
def find_eval_file_for_id(arxiv_id: str) -> Optional[str]:
|
266 |
-
workdir = ensure_workdir()
|
267 |
-
# Look for any json containing the id substring
|
268 |
-
candidates = glob.glob(os.path.join(workdir, f"**/*{arxiv_id}*.json"), recursive=True)
|
269 |
-
return candidates[0] if candidates else None
|
270 |
-
|
271 |
-
|
272 |
-
async def fetch_daily_html(target_date: str) -> tuple[str, str]:
|
273 |
-
"""Fetch daily papers HTML, with fallback to find the latest available date"""
|
274 |
-
async with httpx.AsyncClient(timeout=20, follow_redirects=False) as client:
|
275 |
-
# First try the requested date
|
276 |
-
url = f"https://huggingface.co/papers/date/{target_date}"
|
277 |
-
try:
|
278 |
-
r = await client.get(url)
|
279 |
-
|
280 |
-
# Check if we got redirected
|
281 |
-
if r.status_code in [301, 302, 303, 307, 308]:
|
282 |
-
# We got redirected, extract the actual date from the redirect location
|
283 |
-
location = r.headers.get('location', '')
|
284 |
-
print(f"Got redirect to: {location}")
|
285 |
-
|
286 |
-
# Extract date from redirect URL (e.g., /papers/date/2025-08-08)
|
287 |
-
import re
|
288 |
-
date_match = re.search(r'/papers/date/(\d{4}-\d{2}-\d{2})', location)
|
289 |
-
if date_match:
|
290 |
-
actual_date = date_match.group(1)
|
291 |
-
print(f"Redirected from {target_date} to {actual_date}")
|
292 |
-
|
293 |
-
# Fetch the actual page
|
294 |
-
actual_url = f"https://huggingface.co{location}"
|
295 |
-
r = await client.get(actual_url)
|
296 |
-
if r.status_code == 200:
|
297 |
-
return actual_date, r.text
|
298 |
-
else:
|
299 |
-
raise Exception(f"Failed to fetch redirected page: {r.status_code}")
|
300 |
-
else:
|
301 |
-
# Couldn't extract date from redirect, use fallback
|
302 |
-
raise Exception("Could not extract date from redirect")
|
303 |
-
|
304 |
-
elif r.status_code == 200:
|
305 |
-
# Direct success, check if the page actually contains the requested date
|
306 |
-
if target_date in r.text or "Daily Papers" in r.text:
|
307 |
-
return target_date, r.text
|
308 |
-
else:
|
309 |
-
# Page exists but doesn't contain expected content
|
310 |
-
raise Exception("Page exists but doesn't contain expected content")
|
311 |
-
else:
|
312 |
-
# Other error status
|
313 |
-
raise Exception(f"Status code {r.status_code}")
|
314 |
-
|
315 |
-
except Exception as e:
|
316 |
-
print(f"Failed to fetch {target_date}: {e}")
|
317 |
-
# If the requested date fails, try to find the latest available date
|
318 |
-
actual_date, html = await find_latest_available_date(client)
|
319 |
-
return actual_date, html
|
320 |
-
|
321 |
-
async def find_latest_available_date(client: httpx.AsyncClient) -> tuple[str, str]:
|
322 |
-
"""Find the latest available date by checking recent dates"""
|
323 |
-
|
324 |
-
# Start from today and go backwards up to 30 days
|
325 |
-
today = datetime.now()
|
326 |
-
for i in range(30):
|
327 |
-
check_date = today - timedelta(days=i)
|
328 |
-
date_str = check_date.strftime("%Y-%m-%d")
|
329 |
-
url = f"https://huggingface.co/papers/date/{date_str}"
|
330 |
-
|
331 |
-
try:
|
332 |
-
r = await client.get(url)
|
333 |
-
if r.status_code == 200:
|
334 |
-
# Check if the page actually has content (not just a 404 or empty page)
|
335 |
-
if "Daily Papers" in r.text and len(r.text) > 1000:
|
336 |
-
print(f"Found latest available date: {date_str}")
|
337 |
-
return date_str, r.text
|
338 |
-
except Exception:
|
339 |
-
continue
|
340 |
-
|
341 |
-
# If no date found, return a default page or raise an error
|
342 |
-
raise Exception("No available daily papers found in the last 30 days")
|
343 |
-
|
344 |
-
|
345 |
-
def parse_daily_cards(html: str) -> List[Dict[str, Any]]:
|
346 |
-
soup = BeautifulSoup(html, "lxml")
|
347 |
-
|
348 |
-
# First, extract JSON data from the page to get GitHub stars
|
349 |
-
json_data = extract_json_data(html)
|
350 |
-
|
351 |
-
# Find all article elements that contain paper cards
|
352 |
-
cards: List[Dict[str, Any]] = []
|
353 |
-
|
354 |
-
# Look for article elements with the specific class structure from Hugging Face
|
355 |
-
for article in soup.select("article.relative.flex.flex-col.overflow-hidden.rounded-xl.border"):
|
356 |
-
try:
|
357 |
-
card_data = {}
|
358 |
-
|
359 |
-
# Extract title and link
|
360 |
-
title_link = article.select_one("h3 a")
|
361 |
-
if title_link:
|
362 |
-
card_data["title"] = title_link.get_text(strip=True)
|
363 |
-
href = title_link.get("href")
|
364 |
-
if href:
|
365 |
-
if href.startswith("http"):
|
366 |
-
card_data["huggingface_url"] = href
|
367 |
-
else:
|
368 |
-
card_data["huggingface_url"] = f"https://huggingface.co{href}"
|
369 |
-
|
370 |
-
# Extract upvote count
|
371 |
-
upvote_div = article.select_one("div.shadow-alternate div.leading-none")
|
372 |
-
if upvote_div:
|
373 |
-
upvote_text = upvote_div.get_text(strip=True)
|
374 |
-
try:
|
375 |
-
card_data["upvotes"] = int(upvote_text)
|
376 |
-
except ValueError:
|
377 |
-
card_data["upvotes"] = 0
|
378 |
-
|
379 |
-
# Extract author count - look for the author count text
|
380 |
-
author_count_div = article.select_one("div.flex.truncate.text-sm")
|
381 |
-
if author_count_div:
|
382 |
-
author_text = author_count_div.get_text(strip=True)
|
383 |
-
# Extract number from "· 10 authors"
|
384 |
-
author_match = re.search(r'(\d+)\s*authors?', author_text)
|
385 |
-
if author_match:
|
386 |
-
card_data["author_count"] = int(author_match.group(1))
|
387 |
-
else:
|
388 |
-
card_data["author_count"] = 0
|
389 |
-
|
390 |
-
# Extract GitHub stars from JSON data in the page
|
391 |
-
# This will be handled later when we parse the JSON data
|
392 |
-
card_data["github_stars"] = 0 # Default value
|
393 |
-
|
394 |
-
# Extract comments count - look for comment icon and number
|
395 |
-
comment_links = article.select("a[href*='#community']")
|
396 |
-
for comment_link in comment_links:
|
397 |
-
comment_text = comment_link.get_text(strip=True)
|
398 |
-
try:
|
399 |
-
card_data["comments"] = int(comment_text)
|
400 |
-
break
|
401 |
-
except ValueError:
|
402 |
-
continue
|
403 |
-
|
404 |
-
# Extract submitter information
|
405 |
-
submitted_div = article.select_one("div.shadow-xs")
|
406 |
-
if submitted_div:
|
407 |
-
submitter_text = submitted_div.get_text(strip=True)
|
408 |
-
# Extract submitter name from "Submitted byLiang0223" (no space)
|
409 |
-
submitter_match = re.search(r'Submitted by(\S+)', submitter_text)
|
410 |
-
if submitter_match:
|
411 |
-
card_data["submitter"] = submitter_match.group(1)
|
412 |
-
|
413 |
-
# Extract arXiv ID from the URL
|
414 |
-
if card_data.get("huggingface_url"):
|
415 |
-
arxiv_id = extract_arxiv_id(card_data["huggingface_url"])
|
416 |
-
if arxiv_id:
|
417 |
-
card_data["arxiv_id"] = arxiv_id
|
418 |
-
|
419 |
-
# Try to get GitHub stars from the extracted data
|
420 |
-
# Look for GitHub stars by matching paper title
|
421 |
-
paper_title = card_data.get("title", "")
|
422 |
-
if paper_title in json_data.get("github_stars_map", {}):
|
423 |
-
card_data["github_stars"] = json_data["github_stars_map"][paper_title]
|
424 |
-
|
425 |
-
# Only add cards that have at least a title
|
426 |
-
if card_data.get("title"):
|
427 |
-
cards.append(card_data)
|
428 |
-
|
429 |
-
except Exception as e:
|
430 |
-
print(f"Error parsing card: {e}")
|
431 |
-
continue
|
432 |
-
|
433 |
-
# If the above method didn't work, fall back to the old method
|
434 |
-
if not cards:
|
435 |
-
print("Falling back to old parsing method")
|
436 |
-
for h3 in soup.select("h3"):
|
437 |
-
# Title and Hugging Face paper link (if present)
|
438 |
-
a = h3.find("a")
|
439 |
-
title = h3.get_text(strip=True)
|
440 |
-
hf_link = None
|
441 |
-
if a and a.get("href"):
|
442 |
-
href = a.get("href")
|
443 |
-
# Absolute URL to huggingface
|
444 |
-
if href.startswith("http"):
|
445 |
-
hf_link = href
|
446 |
-
else:
|
447 |
-
hf_link = f"https://huggingface.co{href}"
|
448 |
-
|
449 |
-
# Try to capture sibling info (authors, votes, etc.) as a small snippet
|
450 |
-
meta_text = None
|
451 |
-
parent = h3.parent
|
452 |
-
if parent:
|
453 |
-
# Join immediate text content following h3
|
454 |
-
collected: List[str] = []
|
455 |
-
for sib in parent.find_all(text=True, recursive=False):
|
456 |
-
t = (sib or "").strip()
|
457 |
-
if t:
|
458 |
-
collected.append(t)
|
459 |
-
if collected:
|
460 |
-
meta_text = " ".join(collected)
|
461 |
-
|
462 |
-
# Try to discover any arXiv link inside nearby anchors
|
463 |
-
arxiv_id: Optional[str] = None
|
464 |
-
container = parent if parent else h3
|
465 |
-
for link in container.find_all("a", href=True):
|
466 |
-
possible = extract_arxiv_id(link["href"])
|
467 |
-
if possible:
|
468 |
-
arxiv_id = possible
|
469 |
-
break
|
470 |
-
|
471 |
-
cards.append(
|
472 |
-
{
|
473 |
-
"title": title,
|
474 |
-
"huggingface_url": hf_link,
|
475 |
-
"meta": meta_text,
|
476 |
-
"arxiv_id": arxiv_id,
|
477 |
-
}
|
478 |
-
)
|
479 |
-
|
480 |
-
# Deduplicate by title
|
481 |
-
seen = set()
|
482 |
-
unique_cards: List[Dict[str, Any]] = []
|
483 |
-
for c in cards:
|
484 |
-
key = c.get("title") or ""
|
485 |
-
if key and key not in seen:
|
486 |
-
seen.add(key)
|
487 |
-
unique_cards.append(c)
|
488 |
-
|
489 |
-
print(f"Parsed {len(unique_cards)} cards")
|
490 |
-
return unique_cards
|
491 |
-
|
492 |
-
|
493 |
-
# --- API Routes ---
|
494 |
-
|
495 |
-
@app.get("/api/daily")
|
496 |
-
async def get_daily(date_str: Optional[str] = None) -> Dict[str, Any]:
|
497 |
-
target_date = date_str or date.today().isoformat()
|
498 |
-
|
499 |
-
# First, check if we have fresh cache for the requested date
|
500 |
-
cached_data = db.get_cached_papers(target_date)
|
501 |
-
if cached_data and db.is_cache_fresh(target_date):
|
502 |
-
print(f"Using cached data for {target_date}")
|
503 |
-
return {
|
504 |
-
"date": target_date,
|
505 |
-
"requested_date": target_date,
|
506 |
-
"cards": cached_data['cards'],
|
507 |
-
"fallback_used": False,
|
508 |
-
"cached": True,
|
509 |
-
"cached_at": cached_data['cached_at']
|
510 |
-
}
|
511 |
-
|
512 |
-
# If no cache or stale cache, try to fetch fresh data
|
513 |
-
try:
|
514 |
-
actual_date, html = await fetch_daily_html(target_date)
|
515 |
-
print(f"Fetched fresh data for {actual_date} (requested {target_date})")
|
516 |
-
|
517 |
-
# Check if we got redirected to a different date, and if that date has fresh cache
|
518 |
-
if actual_date != target_date:
|
519 |
-
cached_data = db.get_cached_papers(actual_date)
|
520 |
-
if cached_data and db.is_cache_fresh(actual_date):
|
521 |
-
print(f"Using cached data for redirected date {actual_date}")
|
522 |
-
return {
|
523 |
-
"date": actual_date,
|
524 |
-
"requested_date": target_date,
|
525 |
-
"cards": cached_data['cards'],
|
526 |
-
"fallback_used": True,
|
527 |
-
"cached": True,
|
528 |
-
"cached_at": cached_data['cached_at']
|
529 |
-
}
|
530 |
-
|
531 |
-
except Exception as e:
|
532 |
-
print(f"Failed to fetch {target_date}, trying to find latest available date")
|
533 |
-
# If the requested date fails, try to find the latest available date
|
534 |
-
async with httpx.AsyncClient(timeout=20) as client:
|
535 |
-
try:
|
536 |
-
actual_date, html = await find_latest_available_date(client)
|
537 |
-
print(f"Using fallback date: {actual_date}")
|
538 |
-
|
539 |
-
# Check if the fallback date has fresh cache
|
540 |
-
cached_data = db.get_cached_papers(actual_date)
|
541 |
-
if cached_data and db.is_cache_fresh(actual_date):
|
542 |
-
print(f"Using cached data for fallback date {actual_date}")
|
543 |
-
return {
|
544 |
-
"date": actual_date,
|
545 |
-
"requested_date": target_date,
|
546 |
-
"cards": cached_data['cards'],
|
547 |
-
"fallback_used": True,
|
548 |
-
"cached": True,
|
549 |
-
"cached_at": cached_data['cached_at']
|
550 |
-
}
|
551 |
-
|
552 |
-
except Exception as fallback_error:
|
553 |
-
print(f"Fallback also failed: {fallback_error}")
|
554 |
-
# If everything fails, return cached data if available
|
555 |
-
cached_data = db.get_cached_papers(target_date)
|
556 |
-
if cached_data:
|
557 |
-
return {
|
558 |
-
"date": target_date,
|
559 |
-
"requested_date": target_date,
|
560 |
-
"cards": cached_data['cards'],
|
561 |
-
"fallback_used": False,
|
562 |
-
"cached": True,
|
563 |
-
"cached_at": cached_data['cached_at']
|
564 |
-
}
|
565 |
-
# If no cache available, return error
|
566 |
-
raise HTTPException(status_code=503, detail="Unable to fetch papers and no cache available")
|
567 |
-
|
568 |
-
# Parse the HTML and process cards
|
569 |
-
cards = parse_daily_cards(html)
|
570 |
-
|
571 |
-
# Attempt to resolve missing arXiv ids by scraping the HF paper page
|
572 |
-
async with httpx.AsyncClient(timeout=15) as client:
|
573 |
-
async def resolve_card(card: Dict[str, Any]) -> None:
|
574 |
-
if card.get("arxiv_id") or not card.get("huggingface_url"):
|
575 |
-
return
|
576 |
-
try:
|
577 |
-
r = await client.get(card["huggingface_url"])
|
578 |
-
if r.status_code == 200:
|
579 |
-
soup = BeautifulSoup(r.text, "lxml")
|
580 |
-
for a in soup.select("a[href]"):
|
581 |
-
aid = extract_arxiv_id(a.get("href") or "")
|
582 |
-
if aid:
|
583 |
-
card["arxiv_id"] = aid
|
584 |
-
break
|
585 |
-
except Exception:
|
586 |
-
pass
|
587 |
-
|
588 |
-
# Resolve sequentially for compatibility/simplicity
|
589 |
-
for c in cards:
|
590 |
-
await resolve_card(c)
|
591 |
-
|
592 |
-
# Fallback HF link to the daily page when missing
|
593 |
-
for c in cards:
|
594 |
-
if not c.get("huggingface_url"):
|
595 |
-
c["huggingface_url"] = f"https://huggingface.co/papers/date/{actual_date}"
|
596 |
-
|
597 |
-
# Attach has_eval flag
|
598 |
-
for c in cards:
|
599 |
-
arxiv_id = c.get("arxiv_id")
|
600 |
-
c["has_eval"] = bool(arxiv_id and find_eval_file_for_id(arxiv_id))
|
601 |
-
|
602 |
-
# Cache the results
|
603 |
-
db.cache_papers(actual_date, html, cards)
|
604 |
-
db.update_latest_date(actual_date)
|
605 |
-
|
606 |
-
# Clean up old cache entries (run occasionally)
|
607 |
-
if datetime.now().hour == 2: # Run cleanup at 2 AM
|
608 |
-
db.cleanup_old_cache()
|
609 |
-
|
610 |
-
return {
|
611 |
-
"date": actual_date,
|
612 |
-
"requested_date": target_date,
|
613 |
-
"cards": cards,
|
614 |
-
"fallback_used": actual_date != target_date,
|
615 |
-
"cached": False
|
616 |
-
}
|
617 |
-
|
618 |
-
|
619 |
-
@app.get("/api/evals")
|
620 |
-
def list_evals() -> Dict[str, Any]:
|
621 |
-
workdir = ensure_workdir()
|
622 |
-
files = sorted(glob.glob(os.path.join(workdir, "**/*.json"), recursive=True))
|
623 |
-
items: List[Dict[str, Any]] = []
|
624 |
-
for f in files:
|
625 |
-
base = os.path.basename(f)
|
626 |
-
# Extract arxiv-like id from filename if present
|
627 |
-
m = re.search(r"([0-9]{4}\.\d{4,5})", base)
|
628 |
-
arxiv_id = m.group(1) if m else None
|
629 |
-
items.append({"file": f, "name": base, "arxiv_id": arxiv_id})
|
630 |
-
return {"count": len(items), "items": items}
|
631 |
-
|
632 |
-
|
633 |
-
@app.get("/api/has-eval/{paper_id}")
|
634 |
-
def has_eval(paper_id: str) -> Dict[str, bool]:
|
635 |
-
exists = find_eval_file_for_id(paper_id) is not None
|
636 |
-
return {"exists": exists}
|
637 |
-
|
638 |
-
|
639 |
-
@app.get("/api/eval/{paper_id}")
|
640 |
-
def get_eval(paper_id: str) -> Any:
|
641 |
-
path = find_eval_file_for_id(paper_id)
|
642 |
-
if not path:
|
643 |
-
raise HTTPException(status_code=404, detail="Evaluation not found")
|
644 |
-
return FileResponse(path, media_type="application/json")
|
645 |
-
|
646 |
-
|
647 |
-
@app.get("/api/cache/status")
|
648 |
-
def get_cache_status() -> Dict[str, Any]:
|
649 |
-
"""Get cache status and statistics"""
|
650 |
-
with db.get_connection() as conn:
|
651 |
-
cursor = conn.cursor()
|
652 |
-
|
653 |
-
# Get total cached dates
|
654 |
-
cursor.execute('SELECT COUNT(*) as count FROM papers_cache')
|
655 |
-
total_cached = cursor.fetchone()['count']
|
656 |
-
|
657 |
-
# Get latest cached date
|
658 |
-
cursor.execute('SELECT date_str, updated_at FROM latest_date WHERE id = 1')
|
659 |
-
latest_info = cursor.fetchone()
|
660 |
-
|
661 |
-
# Get cache age distribution
|
662 |
-
cursor.execute('''
|
663 |
-
SELECT
|
664 |
-
CASE
|
665 |
-
WHEN updated_at > datetime('now', '-1 hour') THEN '1 hour'
|
666 |
-
WHEN updated_at > datetime('now', '-24 hours') THEN '24 hours'
|
667 |
-
WHEN updated_at > datetime('now', '-7 days') THEN '7 days'
|
668 |
-
ELSE 'older'
|
669 |
-
END as age_group,
|
670 |
-
COUNT(*) as count
|
671 |
-
FROM papers_cache
|
672 |
-
GROUP BY age_group
|
673 |
-
''')
|
674 |
-
age_distribution = {row['age_group']: row['count'] for row in cursor.fetchall()}
|
675 |
-
|
676 |
-
return {
|
677 |
-
"total_cached_dates": total_cached,
|
678 |
-
"latest_cached_date": latest_info['date_str'] if latest_info else None,
|
679 |
-
"latest_updated": latest_info['updated_at'] if latest_info else None,
|
680 |
-
"age_distribution": age_distribution
|
681 |
-
}
|
682 |
-
|
683 |
-
|
684 |
-
@app.post("/api/cache/clear")
|
685 |
-
def clear_cache() -> Dict[str, str]:
|
686 |
-
"""Clear all cached data"""
|
687 |
-
with db.get_connection() as conn:
|
688 |
-
cursor = conn.cursor()
|
689 |
-
cursor.execute('DELETE FROM papers_cache')
|
690 |
-
conn.commit()
|
691 |
-
return {"message": "Cache cleared successfully"}
|
692 |
-
|
693 |
-
|
694 |
-
@app.post("/api/cache/refresh/{date_str}")
|
695 |
-
async def refresh_cache(date_str: str) -> Dict[str, Any]:
|
696 |
-
"""Force refresh cache for a specific date"""
|
697 |
-
try:
|
698 |
-
# Force fetch fresh data
|
699 |
-
html = await fetch_daily_html(date_str)
|
700 |
-
cards = parse_daily_cards(html)
|
701 |
-
|
702 |
-
# Cache the results
|
703 |
-
db.cache_papers(date_str, html, cards)
|
704 |
-
|
705 |
-
return {
|
706 |
-
"message": f"Cache refreshed for {date_str}",
|
707 |
-
"cards_count": len(cards)
|
708 |
-
}
|
709 |
-
except Exception as e:
|
710 |
-
raise HTTPException(status_code=500, detail=f"Failed to refresh cache: {str(e)}")
|
711 |
-
|
712 |
-
|
713 |
-
@app.get("/styles.css")
|
714 |
-
async def get_styles():
|
715 |
-
"""Serve CSS with no-cache headers to prevent caching issues during development"""
|
716 |
-
response = FileResponse("frontend/styles.css", media_type="text/css")
|
717 |
-
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
718 |
-
response.headers["Pragma"] = "no-cache"
|
719 |
-
response.headers["Expires"] = "0"
|
720 |
-
return response
|
721 |
-
|
722 |
-
# --- Static Frontend ---
|
723 |
-
|
724 |
-
FRONTEND_DIR = os.path.join(PROJECT_ROOT, "frontend")
|
725 |
-
os.makedirs(FRONTEND_DIR, exist_ok=True)
|
726 |
-
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="static")
|
727 |
-
|
728 |
-
# Initialize database
|
729 |
-
db = PapersDatabase(DB_PATH)
|
730 |
-
|
731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# PaperIndex - A beautiful web application for browsing and evaluating daily papers
|
src/agents/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# AI agents for paper evaluation
|
2 |
+
|
3 |
+
|
{agents → src/agents}/evaluator.py
RENAMED
@@ -1,5 +1,6 @@
|
|
1 |
from __future__ import annotations
|
2 |
-
|
|
|
3 |
import base64
|
4 |
import os
|
5 |
import json
|
@@ -9,14 +10,15 @@ from pathlib import Path
|
|
9 |
from datetime import datetime
|
10 |
|
11 |
from anthropic import Anthropic
|
|
|
12 |
from langgraph.graph import END, StateGraph
|
13 |
from pydantic import BaseModel, Field
|
14 |
-
from agents.prompt import REVIEWER_SYSTEM_PROMPT, EVALUATION_PROMPT_TEMPLATE, TOOLS, TOOL_CHOICE
|
15 |
|
16 |
-
|
17 |
-
import
|
18 |
-
|
19 |
-
from
|
|
|
20 |
|
21 |
|
22 |
class ConversationState(BaseModel):
|
@@ -24,6 +26,9 @@ class ConversationState(BaseModel):
|
|
24 |
messages: List[Dict[str, Any]] = Field(default_factory=list)
|
25 |
response_text: str = ""
|
26 |
tool_result: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
|
27 |
|
28 |
|
29 |
def _load_pdf_as_content(pdf_path: str) -> Dict[str, Any]:
|
@@ -51,7 +56,7 @@ def _load_pdf_as_content(pdf_path: str) -> Dict[str, Any]:
|
|
51 |
|
52 |
class Evaluator:
|
53 |
def __init__(self, api_key: Optional[str] = None):
|
54 |
-
api_key = api_key or
|
55 |
if not api_key:
|
56 |
raise ValueError("Anthropic API key is required. Please set HF_SECRET_ANTHROPIC_API_KEY in Hugging Face Spaces secrets or ANTHROPIC_API_KEY environment variable.")
|
57 |
self.client = Anthropic(api_key=api_key)
|
@@ -61,9 +66,24 @@ class Evaluator:
|
|
61 |
async def __call__(self, state: ConversationState) -> ConversationState:
|
62 |
"""Evaluate the paper using the conversation state"""
|
63 |
# Prepare messages for the API call
|
64 |
-
messages = [
|
65 |
messages.extend(state.messages)
|
66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
# Add the evaluation prompt
|
68 |
messages.append({
|
69 |
"role": "user",
|
@@ -72,32 +92,49 @@ class Evaluator:
|
|
72 |
|
73 |
try:
|
74 |
# Call Anthropic API with tools
|
75 |
-
response =
|
76 |
-
model=
|
77 |
max_tokens=4000,
|
|
|
78 |
messages=messages,
|
79 |
tools=TOOLS,
|
80 |
tool_choice=TOOL_CHOICE
|
81 |
)
|
82 |
|
83 |
# Process the response
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
# Extract tool result if present
|
90 |
-
tool_result = None
|
91 |
-
if response.content and hasattr(response.content[0], 'tool_use'):
|
92 |
-
tool_use = response.content[0].tool_use
|
93 |
if tool_use:
|
94 |
-
tool_result =
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
else:
|
100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
except Exception as e:
|
103 |
state.response_text = f"Error during evaluation: {str(e)}"
|
@@ -106,29 +143,85 @@ class Evaluator:
|
|
106 |
|
107 |
|
108 |
async def save_node(state: ConversationState) -> ConversationState:
|
109 |
-
"""Save the evaluation result to
|
110 |
try:
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
|
115 |
-
#
|
116 |
-
|
117 |
-
|
118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
|
120 |
-
# Save
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
|
|
127 |
|
128 |
-
state.response_text += f"\n\nEvaluation saved to: {
|
129 |
|
130 |
except Exception as e:
|
131 |
-
state.response_text += f"\n\nError saving evaluation: {str(e)}"
|
132 |
|
133 |
return state
|
134 |
|
@@ -148,11 +241,11 @@ def build_graph(api_key: Optional[str] = None):
|
|
148 |
return graph.compile()
|
149 |
|
150 |
|
151 |
-
def run_evaluation(pdf_path: str, output_file: Optional[str] = None, api_key: Optional[str] = None) -> str:
|
152 |
app = build_graph(api_key=api_key)
|
153 |
-
initial = ConversationState(pdf_path=pdf_path, output_file=output_file)
|
154 |
# Ensure compatibility with LangGraph's dict-based state
|
155 |
-
final_state = app.
|
156 |
if isinstance(final_state, dict):
|
157 |
return str(final_state.get("response_text", ""))
|
158 |
if isinstance(final_state, ConversationState):
|
|
|
1 |
from __future__ import annotations
|
2 |
+
import os
|
3 |
+
import sys
|
4 |
import base64
|
5 |
import os
|
6 |
import json
|
|
|
10 |
from datetime import datetime
|
11 |
|
12 |
from anthropic import Anthropic
|
13 |
+
from anthropic.types import ToolUseBlock
|
14 |
from langgraph.graph import END, StateGraph
|
15 |
from pydantic import BaseModel, Field
|
|
|
16 |
|
17 |
+
|
18 |
+
from src.agents.prompt import REVIEWER_SYSTEM_PROMPT, EVALUATION_PROMPT_TEMPLATE, TOOLS, TOOL_CHOICE
|
19 |
+
from src.database import db
|
20 |
+
from src.config import config
|
21 |
+
from src.logger import logger
|
22 |
|
23 |
|
24 |
class ConversationState(BaseModel):
|
|
|
26 |
messages: List[Dict[str, Any]] = Field(default_factory=list)
|
27 |
response_text: str = ""
|
28 |
tool_result: Optional[Dict[str, Any]] = None
|
29 |
+
arxiv_id: Optional[str] = None
|
30 |
+
pdf_path: Optional[str] = None
|
31 |
+
output_file: Optional[str] = None
|
32 |
|
33 |
|
34 |
def _load_pdf_as_content(pdf_path: str) -> Dict[str, Any]:
|
|
|
56 |
|
57 |
class Evaluator:
|
58 |
def __init__(self, api_key: Optional[str] = None):
|
59 |
+
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
60 |
if not api_key:
|
61 |
raise ValueError("Anthropic API key is required. Please set HF_SECRET_ANTHROPIC_API_KEY in Hugging Face Spaces secrets or ANTHROPIC_API_KEY environment variable.")
|
62 |
self.client = Anthropic(api_key=api_key)
|
|
|
66 |
async def __call__(self, state: ConversationState) -> ConversationState:
|
67 |
"""Evaluate the paper using the conversation state"""
|
68 |
# Prepare messages for the API call
|
69 |
+
messages = []
|
70 |
messages.extend(state.messages)
|
71 |
|
72 |
+
# Load PDF content if pdf_path is provided
|
73 |
+
if state.pdf_path:
|
74 |
+
try:
|
75 |
+
pdf_content = _load_pdf_as_content(state.pdf_path)
|
76 |
+
messages.append({
|
77 |
+
"role": "user",
|
78 |
+
"content": [
|
79 |
+
{"type": "text", "text": "Please evaluate this academic paper:"},
|
80 |
+
pdf_content
|
81 |
+
]
|
82 |
+
})
|
83 |
+
except Exception as e:
|
84 |
+
state.response_text = f"Error loading PDF: {str(e)}"
|
85 |
+
return state
|
86 |
+
|
87 |
# Add the evaluation prompt
|
88 |
messages.append({
|
89 |
"role": "user",
|
|
|
92 |
|
93 |
try:
|
94 |
# Call Anthropic API with tools
|
95 |
+
response = self.client.messages.create(
|
96 |
+
model=config.model_id,
|
97 |
max_tokens=4000,
|
98 |
+
system=self.system_prompt,
|
99 |
messages=messages,
|
100 |
tools=TOOLS,
|
101 |
tool_choice=TOOL_CHOICE
|
102 |
)
|
103 |
|
104 |
# Process the response
|
105 |
+
# Check if response is a tool use or text
|
106 |
+
if response.content and isinstance(response.content[0], ToolUseBlock):
|
107 |
+
# This is a tool use response
|
108 |
+
tool_use = response.content[0]
|
|
|
|
|
|
|
|
|
|
|
109 |
if tool_use:
|
110 |
+
tool_result = tool_use.input
|
111 |
+
|
112 |
+
# set metadata
|
113 |
+
tool_result['metadata'] = {
|
114 |
+
'assessed_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
115 |
+
'model': config.model_id,
|
116 |
+
'version': config.version,
|
117 |
+
'paper_path': state.pdf_path
|
118 |
+
}
|
119 |
+
|
120 |
+
state.tool_result = tool_result
|
121 |
+
state.response_text = json.dumps(tool_result, ensure_ascii=False, indent=4)
|
122 |
+
|
123 |
+
# Add tool use to messages
|
124 |
+
state.messages.append({
|
125 |
+
"role": "assistant",
|
126 |
+
"content": f"Tool use: {tool_use.name}"
|
127 |
+
})
|
128 |
+
else:
|
129 |
+
state.response_text = "Error: Tool use response but no tool_use found"
|
130 |
else:
|
131 |
+
# This is a text response
|
132 |
+
text_content = response.content[0].text if response.content else ""
|
133 |
+
state.messages.append({
|
134 |
+
"role": "assistant",
|
135 |
+
"content": text_content
|
136 |
+
})
|
137 |
+
state.response_text = text_content
|
138 |
|
139 |
except Exception as e:
|
140 |
state.response_text = f"Error during evaluation: {str(e)}"
|
|
|
143 |
|
144 |
|
145 |
async def save_node(state: ConversationState) -> ConversationState:
|
146 |
+
"""Save the evaluation result to database"""
|
147 |
try:
|
148 |
+
if not state.arxiv_id:
|
149 |
+
state.response_text += f"\n\nError: No arxiv_id provided for database save"
|
150 |
+
return state
|
151 |
+
|
152 |
+
# Parse the evaluation result
|
153 |
+
evaluation_content = state.response_text
|
154 |
+
evaluation_score = None
|
155 |
+
overall_score = None
|
156 |
+
evaluation_tags = None
|
157 |
|
158 |
+
# Try to extract score and tags from tool_result if available
|
159 |
+
if state.tool_result:
|
160 |
+
try:
|
161 |
+
# Extract overall automatability score from scorecard
|
162 |
+
if 'scorecard' in state.tool_result and 'overall_automatability' in state.tool_result['scorecard']:
|
163 |
+
evaluation_score = state.tool_result['scorecard']['overall_automatability']
|
164 |
+
|
165 |
+
# Extract overall score from scorecard
|
166 |
+
if 'scorecard' in state.tool_result and 'overall_automatability' in state.tool_result['scorecard']:
|
167 |
+
overall_score = state.tool_result['scorecard']['overall_automatability']
|
168 |
+
|
169 |
+
# Create tags from key dimensions in scorecard
|
170 |
+
tags = []
|
171 |
+
if 'scorecard' in state.tool_result:
|
172 |
+
scorecard = state.tool_result['scorecard']
|
173 |
+
if 'three_year_feasibility_pct' in scorecard:
|
174 |
+
tags.append(f"3yr_feasibility:{scorecard['three_year_feasibility_pct']}%")
|
175 |
+
if 'task_formalization' in scorecard:
|
176 |
+
tags.append(f"task_formalization:{scorecard['task_formalization']}/4")
|
177 |
+
if 'data_resource_availability' in scorecard:
|
178 |
+
tags.append(f"data_availability:{scorecard['data_resource_availability']}/4")
|
179 |
+
|
180 |
+
evaluation_tags = ",".join(tags) if tags else None
|
181 |
+
|
182 |
+
except Exception as e:
|
183 |
+
logger.warning(f"Warning: Could not extract structured data from tool_result: {e}")
|
184 |
+
else:
|
185 |
+
# Try to parse evaluation_content as JSON to extract structured data
|
186 |
+
try:
|
187 |
+
evaluation_json = json.loads(evaluation_content)
|
188 |
+
# Extract overall automatability score from scorecard
|
189 |
+
if 'scorecard' in evaluation_json and 'overall_automatability' in evaluation_json['scorecard']:
|
190 |
+
evaluation_score = evaluation_json['scorecard']['overall_automatability']
|
191 |
+
|
192 |
+
# Extract overall score from scorecard
|
193 |
+
if 'scorecard' in evaluation_json and 'overall_automatability' in evaluation_json['scorecard']:
|
194 |
+
overall_score = evaluation_json['scorecard']['overall_automatability']
|
195 |
+
|
196 |
+
# Create tags from key dimensions in scorecard
|
197 |
+
tags = []
|
198 |
+
if 'scorecard' in evaluation_json:
|
199 |
+
scorecard = evaluation_json['scorecard']
|
200 |
+
if 'three_year_feasibility_pct' in scorecard:
|
201 |
+
tags.append(f"3yr_feasibility:{scorecard['three_year_feasibility_pct']}%")
|
202 |
+
if 'task_formalization' in scorecard:
|
203 |
+
tags.append(f"task_formalization:{scorecard['task_formalization']}/4")
|
204 |
+
if 'data_resource_availability' in scorecard:
|
205 |
+
tags.append(f"data_availability:{scorecard['data_resource_availability']}/4")
|
206 |
+
|
207 |
+
evaluation_tags = ",".join(tags) if tags else None
|
208 |
+
|
209 |
+
except Exception as e:
|
210 |
+
logger.warning(f"Warning: Could not parse evaluation_content as JSON: {e}")
|
211 |
|
212 |
+
# Save to database
|
213 |
+
db.update_paper_evaluation(
|
214 |
+
arxiv_id=state.arxiv_id,
|
215 |
+
evaluation_content=evaluation_content,
|
216 |
+
evaluation_score=evaluation_score,
|
217 |
+
overall_score=overall_score,
|
218 |
+
evaluation_tags=evaluation_tags
|
219 |
+
)
|
220 |
|
221 |
+
state.response_text += f"\n\nEvaluation saved to database for paper: {state.arxiv_id}"
|
222 |
|
223 |
except Exception as e:
|
224 |
+
state.response_text += f"\n\nError saving evaluation to database: {str(e)}"
|
225 |
|
226 |
return state
|
227 |
|
|
|
241 |
return graph.compile()
|
242 |
|
243 |
|
244 |
+
async def run_evaluation(pdf_path: str, arxiv_id: Optional[str] = None, output_file: Optional[str] = None, api_key: Optional[str] = None) -> str:
|
245 |
app = build_graph(api_key=api_key)
|
246 |
+
initial = ConversationState(pdf_path=pdf_path, arxiv_id=arxiv_id, output_file=output_file)
|
247 |
# Ensure compatibility with LangGraph's dict-based state
|
248 |
+
final_state = await app.ainvoke(initial.model_dump())
|
249 |
if isinstance(final_state, dict):
|
250 |
return str(final_state.get("response_text", ""))
|
251 |
if isinstance(final_state, ConversationState):
|
{agents → src/agents}/prompt.py
RENAMED
@@ -248,16 +248,6 @@ TOOLS = [
|
|
248 |
"input_schema": {
|
249 |
"type": "object",
|
250 |
"properties": {
|
251 |
-
"metadata": {
|
252 |
-
"type": "object",
|
253 |
-
"properties": {
|
254 |
-
"assessed_at": {"type": "string"},
|
255 |
-
"model": {"type": "string"},
|
256 |
-
"version": {"type": "string"},
|
257 |
-
"paper_path": {"type": "string"},
|
258 |
-
},
|
259 |
-
"required": ["assessed_at", "model", "version", "paper_path"],
|
260 |
-
},
|
261 |
"executive_summary": {"type": "string"},
|
262 |
"dimensions": {
|
263 |
"type": "object",
|
@@ -382,7 +372,6 @@ TOOLS = [
|
|
382 |
"limitations_uncertainties": {"type": "array", "items": {"type": "string"}},
|
383 |
},
|
384 |
"required": [
|
385 |
-
"metadata",
|
386 |
"executive_summary",
|
387 |
"dimensions",
|
388 |
"scorecard",
|
|
|
248 |
"input_schema": {
|
249 |
"type": "object",
|
250 |
"properties": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
"executive_summary": {"type": "string"},
|
252 |
"dimensions": {
|
253 |
"type": "object",
|
|
|
372 |
"limitations_uncertainties": {"type": "array", "items": {"type": "string"}},
|
373 |
},
|
374 |
"required": [
|
|
|
375 |
"executive_summary",
|
376 |
"dimensions",
|
377 |
"scorecard",
|
src/cli/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# Command line interface tools
|
src/cli/cli.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import sys
|
4 |
+
import asyncio
|
5 |
+
from typing import Optional
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
from rich.console import Console
|
10 |
+
from rich.panel import Panel
|
11 |
+
|
12 |
+
import os
|
13 |
+
import sys
|
14 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
15 |
+
from agents.evaluator import run_evaluation
|
16 |
+
|
17 |
+
|
18 |
+
console = Console()
|
19 |
+
|
20 |
+
|
21 |
+
def build_parser() -> argparse.ArgumentParser:
|
22 |
+
parser = argparse.ArgumentParser(
|
23 |
+
description="AI Automation Evaluator (LangGraph) — evaluate a paper PDF or arXiv URL",
|
24 |
+
epilog="Example: python cli.py https://arxiv.org/pdf/2507.14683 --arxiv-id 2507.14683 -o /abs/path/save_dir/eval_2507_14683",
|
25 |
+
)
|
26 |
+
parser.add_argument("pdf", help="Local PDF absolute path or URL (e.g., https://arxiv.org/pdf/xxxx)")
|
27 |
+
parser.add_argument(
|
28 |
+
"--arxiv-id",
|
29 |
+
dest="arxiv_id",
|
30 |
+
help="arXiv ID for the paper (e.g., 2507.14683)",
|
31 |
+
)
|
32 |
+
parser.add_argument(
|
33 |
+
"-o",
|
34 |
+
"--output-prefix",
|
35 |
+
dest="output_prefix",
|
36 |
+
help="Output file prefix (if provided, will save as <prefix>_YYYYMMDD_HHMMSS.md)",
|
37 |
+
)
|
38 |
+
parser.add_argument(
|
39 |
+
"--api-key",
|
40 |
+
dest="api_key",
|
41 |
+
default=os.getenv("ANTHROPIC_API_KEY"),
|
42 |
+
help="Anthropic API key (overrides ANTHROPIC_API_KEY env)",
|
43 |
+
)
|
44 |
+
return parser
|
45 |
+
|
46 |
+
|
47 |
+
async def main(argv: Optional[list[str]] = None):
|
48 |
+
parser = build_parser()
|
49 |
+
args = parser.parse_args(argv)
|
50 |
+
|
51 |
+
pdf_path: str = args.pdf
|
52 |
+
arxiv_id: Optional[str] = args.arxiv_id
|
53 |
+
output_prefix: Optional[str] = args.output_prefix
|
54 |
+
api_key: Optional[str] = args.api_key or os.getenv("ANTHROPIC_API_KEY")
|
55 |
+
|
56 |
+
if not api_key:
|
57 |
+
console.print("[yellow]Warning:[/yellow] ANTHROPIC_API_KEY not set and --api-key not provided.", highlight=False)
|
58 |
+
|
59 |
+
console.print(Panel.fit(f"Evaluating: {pdf_path}"))
|
60 |
+
if arxiv_id:
|
61 |
+
console.print(f"arXiv ID: {arxiv_id}")
|
62 |
+
|
63 |
+
try:
|
64 |
+
result = await run_evaluation(pdf_path=pdf_path, arxiv_id=arxiv_id, output_file=output_prefix, api_key=api_key)
|
65 |
+
console.print("\n[bold green]Done.[/bold green]\n")
|
66 |
+
if output_prefix:
|
67 |
+
console.print(f"Saved to prefix: {output_prefix}_<timestamp>.md")
|
68 |
+
elif arxiv_id:
|
69 |
+
console.print(f"Evaluation saved to database for paper: {arxiv_id}")
|
70 |
+
else:
|
71 |
+
console.print(result)
|
72 |
+
except Exception as e:
|
73 |
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
74 |
+
sys.exit(2)
|
75 |
+
|
76 |
+
|
77 |
+
if __name__ == "__main__":
|
78 |
+
asyncio.run(main())
|
79 |
+
|
80 |
+
|
src/config/config.py
CHANGED
@@ -2,45 +2,19 @@ import os
|
|
2 |
from mmengine import Config as MMConfig
|
3 |
from argparse import Namespace
|
4 |
|
5 |
-
from
|
6 |
-
load_dotenv(verbose=True)
|
7 |
-
|
8 |
-
from finworld.utils import assemble_project_path, get_tag_name, Singleton, set_seed
|
9 |
-
|
10 |
-
def check_level(level: str) -> bool:
|
11 |
-
"""
|
12 |
-
Check if the level is valid.
|
13 |
-
"""
|
14 |
-
valid_levels = ['1day', '1min', '5min', '15min', '30min', '1hour', '4hour']
|
15 |
-
if level not in valid_levels:
|
16 |
-
return False
|
17 |
-
return True
|
18 |
|
19 |
def process_general(config: MMConfig) -> MMConfig:
|
20 |
|
21 |
config.exp_path = assemble_project_path(os.path.join(config.workdir, config.tag))
|
22 |
os.makedirs(config.exp_path, exist_ok=True)
|
23 |
|
24 |
-
config.log_path = os.path.join(config.exp_path, getattr(config, 'log_path', '
|
25 |
-
|
26 |
-
|
27 |
-
config.checkpoint_path = os.path.join(config.exp_path, getattr(config, 'checkpoint_path', 'checkpoint'))
|
28 |
-
os.makedirs(config.checkpoint_path, exist_ok=True)
|
29 |
-
|
30 |
-
if "plot_path" in config:
|
31 |
-
config.plot_path = os.path.join(config.exp_path, getattr(config, 'plot_path', 'plot'))
|
32 |
-
os.makedirs(config.plot_path, exist_ok=True)
|
33 |
-
|
34 |
-
if "tracker" in config:
|
35 |
-
for key, value in config.tracker.items():
|
36 |
-
config.tracker[key]['logging_dir'] = os.path.join(config.exp_path, value['logging_dir'])
|
37 |
-
|
38 |
-
if "seed" in config:
|
39 |
-
set_seed(config.seed)
|
40 |
|
41 |
return config
|
42 |
|
43 |
-
|
44 |
class Config(MMConfig, metaclass=Singleton):
|
45 |
def __init__(self):
|
46 |
super(Config, self).__init__()
|
@@ -57,30 +31,9 @@ class Config(MMConfig, metaclass=Singleton):
|
|
57 |
cfg_options[item] = args.__dict__[item]
|
58 |
mmconfig.merge_from_dict(cfg_options)
|
59 |
|
60 |
-
tag = get_tag_name(
|
61 |
-
tag=getattr(mmconfig, 'tag', None),
|
62 |
-
assets_name=getattr(mmconfig, 'assets_name', None),
|
63 |
-
source=getattr(mmconfig, 'source', None),
|
64 |
-
data_type= getattr(mmconfig, 'data_type', None),
|
65 |
-
level= getattr(mmconfig, 'level', None),
|
66 |
-
)
|
67 |
-
mmconfig.tag = tag
|
68 |
-
|
69 |
# Process general configuration
|
70 |
mmconfig = process_general(mmconfig)
|
71 |
|
72 |
-
# Initialize the price downloader configuration
|
73 |
-
if 'downloader' in mmconfig:
|
74 |
-
if "assets_path" in mmconfig.downloader:
|
75 |
-
mmconfig.downloader.assets_path = assemble_project_path(mmconfig.downloader.assets_path)
|
76 |
-
assert check_level(mmconfig.downloader.level), f"Invalid level: {mmconfig.downloader.level}. Valid levels are: ['1day', '1min', '5min', '15min', '30min', '1hour', '4hour']"
|
77 |
-
|
78 |
-
if 'processor' in mmconfig:
|
79 |
-
if "assets_path" in mmconfig.processor:
|
80 |
-
mmconfig.processor.assets_path = assemble_project_path(mmconfig.processor.assets_path)
|
81 |
-
mmconfig.processor.repo_id = f"{os.getenv('HF_REPO_NAME')}/{mmconfig.processor.repo_id}"
|
82 |
-
mmconfig.processor.repo_type = mmconfig.processor.repo_type if 'repo_type' in mmconfig.processor else 'dataset'
|
83 |
-
|
84 |
self.__dict__.update(mmconfig.__dict__)
|
85 |
|
86 |
config = Config()
|
|
|
2 |
from mmengine import Config as MMConfig
|
3 |
from argparse import Namespace
|
4 |
|
5 |
+
from src.utils import assemble_project_path, Singleton
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
def process_general(config: MMConfig) -> MMConfig:
|
8 |
|
9 |
config.exp_path = assemble_project_path(os.path.join(config.workdir, config.tag))
|
10 |
os.makedirs(config.exp_path, exist_ok=True)
|
11 |
|
12 |
+
config.log_path = os.path.join(config.exp_path, getattr(config, 'log_path', 'paper_agent.log'))
|
13 |
+
config.db_path = os.path.join(config.exp_path, getattr(config, 'db_path', 'papers_cache.db'))
|
14 |
+
config.frontend_path = assemble_project_path(getattr(config, 'frontend_path', 'frontend'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
return config
|
17 |
|
|
|
18 |
class Config(MMConfig, metaclass=Singleton):
|
19 |
def __init__(self):
|
20 |
super(Config, self).__init__()
|
|
|
31 |
cfg_options[item] = args.__dict__[item]
|
32 |
mmconfig.merge_from_dict(cfg_options)
|
33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
# Process general configuration
|
35 |
mmconfig = process_general(mmconfig)
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
self.__dict__.update(mmconfig.__dict__)
|
38 |
|
39 |
config = Config()
|
src/crawl/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Crawl module for web scraping and data extraction
|
2 |
+
|
3 |
+
from .huggingface_daily import HuggingFaceDailyPapers
|
4 |
+
|
5 |
+
__all__ = ['HuggingFaceDailyPapers']
|
src/crawl/huggingface_daily.py
ADDED
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Dict, Any, Optional
|
2 |
+
import re
|
3 |
+
import httpx
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
from bs4 import BeautifulSoup
|
6 |
+
|
7 |
+
from src.logger import logger
|
8 |
+
|
9 |
+
|
10 |
+
class HuggingFaceDailyPapers:
|
11 |
+
"""Class for crawling and parsing Hugging Face daily papers"""
|
12 |
+
|
13 |
+
def __init__(self):
|
14 |
+
self.base_url = "https://huggingface.co/papers/date"
|
15 |
+
self.timeout = 20
|
16 |
+
|
17 |
+
def extract_arxiv_id(self, url: str) -> Optional[str]:
|
18 |
+
"""Extract arXiv ID from a URL"""
|
19 |
+
if not url:
|
20 |
+
return None
|
21 |
+
# Matches /abs/2508.05629, /pdf/2508.05629.pdf
|
22 |
+
m = re.search(r"arxiv\.org/(abs|pdf)/([0-9]{4}\.\d{4,5})(?:\.pdf)?", url)
|
23 |
+
if m:
|
24 |
+
return m.group(2)
|
25 |
+
return None
|
26 |
+
|
27 |
+
def extract_json_data(self, html: str) -> Dict[str, Any]:
|
28 |
+
"""Extract JSON data from the HTML page to get GitHub stars and other metadata."""
|
29 |
+
try:
|
30 |
+
soup = BeautifulSoup(html, "lxml")
|
31 |
+
|
32 |
+
# Look for GitHub stars in the HTML structure
|
33 |
+
# Based on the user's description, GitHub stars are displayed with SVG icons
|
34 |
+
# Look for SVG elements that might represent GitHub stars
|
35 |
+
svg_elements = soup.find_all("svg")
|
36 |
+
|
37 |
+
github_stars_map = {}
|
38 |
+
|
39 |
+
for svg in svg_elements:
|
40 |
+
# Look for GitHub-related SVG (usually has specific viewBox or path)
|
41 |
+
svg_html = str(svg)
|
42 |
+
if "github" in svg_html.lower() or "256 250" in svg_html: # GitHub icon viewBox
|
43 |
+
# Look for the star count near this SVG
|
44 |
+
parent = svg.parent
|
45 |
+
if parent:
|
46 |
+
# Look for numbers that might be star counts
|
47 |
+
text_content = parent.get_text()
|
48 |
+
numbers = re.findall(r'\b(\d+)\b', text_content)
|
49 |
+
if numbers:
|
50 |
+
# The number near a GitHub SVG is likely the star count
|
51 |
+
star_count = int(numbers[0])
|
52 |
+
# Try to find the paper title or ID to associate with
|
53 |
+
# Look for the closest article or card container
|
54 |
+
article = svg.find_parent("article")
|
55 |
+
if article:
|
56 |
+
title_elem = article.find("h3")
|
57 |
+
if title_elem:
|
58 |
+
paper_title = title_elem.get_text(strip=True)
|
59 |
+
github_stars_map[paper_title] = star_count
|
60 |
+
|
61 |
+
# Also look for any elements with GitHub-related text
|
62 |
+
github_text_elements = soup.find_all(string=lambda text: text and "github" in text.lower())
|
63 |
+
for text_elem in github_text_elements:
|
64 |
+
parent = text_elem.parent
|
65 |
+
if parent:
|
66 |
+
text_content = parent.get_text()
|
67 |
+
numbers = re.findall(r'\b(\d+)\b', text_content)
|
68 |
+
if numbers:
|
69 |
+
star_count = int(numbers[0])
|
70 |
+
# Try to find the paper title
|
71 |
+
article = parent.find_parent("article")
|
72 |
+
if article:
|
73 |
+
title_elem = article.find("h3")
|
74 |
+
if title_elem:
|
75 |
+
paper_title = title_elem.get_text(strip=True)
|
76 |
+
if paper_title not in github_stars_map:
|
77 |
+
github_stars_map[paper_title] = star_count
|
78 |
+
|
79 |
+
return {"github_stars_map": github_stars_map}
|
80 |
+
|
81 |
+
except Exception as e:
|
82 |
+
logger.error(f"Error extracting JSON data: {e}")
|
83 |
+
|
84 |
+
return {"github_stars_map": {}}
|
85 |
+
|
86 |
+
async def fetch_daily_html(self, target_date: str) -> tuple[str, str]:
|
87 |
+
"""Fetch daily papers HTML, with fallback to find the latest available date"""
|
88 |
+
async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=False) as client:
|
89 |
+
# First try the requested date
|
90 |
+
url = f"{self.base_url}/{target_date}"
|
91 |
+
try:
|
92 |
+
r = await client.get(url)
|
93 |
+
|
94 |
+
# Check if we got redirected
|
95 |
+
if r.status_code in [301, 302, 303, 307, 308]:
|
96 |
+
# We got redirected, extract the actual date from the redirect location
|
97 |
+
location = r.headers.get('location', '')
|
98 |
+
logger.info(f"Got redirect to: {location}")
|
99 |
+
|
100 |
+
# Extract date from redirect URL (e.g., /papers/date/2025-08-08)
|
101 |
+
date_match = re.search(r'/papers/date/(\d{4}-\d{2}-\d{2})', location)
|
102 |
+
if date_match:
|
103 |
+
actual_date = date_match.group(1)
|
104 |
+
logger.info(f"Redirected from {target_date} to {actual_date}")
|
105 |
+
|
106 |
+
# Fetch the actual page
|
107 |
+
actual_url = f"https://huggingface.co{location}"
|
108 |
+
r = await client.get(actual_url)
|
109 |
+
if r.status_code == 200:
|
110 |
+
return actual_date, r.text
|
111 |
+
else:
|
112 |
+
raise Exception(f"Failed to fetch redirected page: {r.status_code}")
|
113 |
+
else:
|
114 |
+
# Couldn't extract date from redirect, use fallback
|
115 |
+
raise Exception("Could not extract date from redirect")
|
116 |
+
|
117 |
+
elif r.status_code == 200:
|
118 |
+
# Direct success, check if the page actually contains the requested date
|
119 |
+
if target_date in r.text or "Daily Papers" in r.text:
|
120 |
+
return target_date, r.text
|
121 |
+
else:
|
122 |
+
# Page exists but doesn't contain expected content
|
123 |
+
raise Exception("Page exists but doesn't contain expected content")
|
124 |
+
else:
|
125 |
+
# Other error status
|
126 |
+
raise Exception(f"Status code {r.status_code}")
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
logger.error(f"Failed to fetch {target_date}: {e}")
|
130 |
+
# If the requested date fails, try to find the latest available date
|
131 |
+
actual_date, html = await self.find_latest_available_date(client)
|
132 |
+
return actual_date, html
|
133 |
+
|
134 |
+
async def find_latest_available_date(self, client: httpx.AsyncClient) -> tuple[str, str]:
|
135 |
+
"""Find the latest available date by checking recent dates"""
|
136 |
+
|
137 |
+
# Start from today and go backwards up to 30 days
|
138 |
+
today = datetime.now()
|
139 |
+
for i in range(30):
|
140 |
+
check_date = today - timedelta(days=i)
|
141 |
+
date_str = check_date.strftime("%Y-%m-%d")
|
142 |
+
url = f"{self.base_url}/{date_str}"
|
143 |
+
|
144 |
+
try:
|
145 |
+
r = await client.get(url)
|
146 |
+
if r.status_code == 200:
|
147 |
+
# Check if the page actually has content (not just a 404 or empty page)
|
148 |
+
if "Daily Papers" in r.text and len(r.text) > 1000:
|
149 |
+
logger.info(f"Found latest available date: {date_str}")
|
150 |
+
return date_str, r.text
|
151 |
+
except Exception:
|
152 |
+
continue
|
153 |
+
|
154 |
+
# If no date found, return a default page or raise an error
|
155 |
+
raise Exception("No available daily papers found in the last 30 days")
|
156 |
+
|
157 |
+
def parse_daily_cards(self, html: str) -> List[Dict[str, Any]]:
|
158 |
+
"""Parse daily papers HTML and extract paper cards"""
|
159 |
+
soup = BeautifulSoup(html, "lxml")
|
160 |
+
|
161 |
+
# First, extract JSON data from the page to get GitHub stars
|
162 |
+
json_data = self.extract_json_data(html)
|
163 |
+
|
164 |
+
# Find all article elements that contain paper cards
|
165 |
+
cards: List[Dict[str, Any]] = []
|
166 |
+
|
167 |
+
# Look for article elements with the specific class structure from Hugging Face
|
168 |
+
for article in soup.select("article.relative.flex.flex-col.overflow-hidden.rounded-xl.border"):
|
169 |
+
try:
|
170 |
+
card_data = {}
|
171 |
+
|
172 |
+
# Extract title and link
|
173 |
+
title_link = article.select_one("h3 a")
|
174 |
+
if title_link:
|
175 |
+
card_data["title"] = title_link.get_text(strip=True)
|
176 |
+
href = title_link.get("href")
|
177 |
+
if href:
|
178 |
+
if href.startswith("http"):
|
179 |
+
card_data["huggingface_url"] = href
|
180 |
+
else:
|
181 |
+
card_data["huggingface_url"] = f"https://huggingface.co{href}"
|
182 |
+
|
183 |
+
# Extract upvote count
|
184 |
+
upvote_div = article.select_one("div.shadow-alternate div.leading-none")
|
185 |
+
if upvote_div:
|
186 |
+
upvote_text = upvote_div.get_text(strip=True)
|
187 |
+
try:
|
188 |
+
card_data["upvotes"] = int(upvote_text)
|
189 |
+
except ValueError:
|
190 |
+
card_data["upvotes"] = 0
|
191 |
+
|
192 |
+
# Extract author count - look for the author count text
|
193 |
+
author_count_div = article.select_one("div.flex.truncate.text-sm")
|
194 |
+
if author_count_div:
|
195 |
+
author_text = author_count_div.get_text(strip=True)
|
196 |
+
# Extract number from "· 10 authors"
|
197 |
+
author_match = re.search(r'(\d+)\s*authors?', author_text)
|
198 |
+
if author_match:
|
199 |
+
card_data["author_count"] = int(author_match.group(1))
|
200 |
+
else:
|
201 |
+
card_data["author_count"] = 0
|
202 |
+
|
203 |
+
# Extract GitHub stars from JSON data in the page
|
204 |
+
# This will be handled later when we parse the JSON data
|
205 |
+
card_data["github_stars"] = 0 # Default value
|
206 |
+
|
207 |
+
# Extract comments count - look for comment icon and number
|
208 |
+
comment_links = article.select("a[href*='#community']")
|
209 |
+
for comment_link in comment_links:
|
210 |
+
comment_text = comment_link.get_text(strip=True)
|
211 |
+
try:
|
212 |
+
card_data["comments"] = int(comment_text)
|
213 |
+
break
|
214 |
+
except ValueError:
|
215 |
+
continue
|
216 |
+
|
217 |
+
# Extract submitter information
|
218 |
+
submitted_div = article.select_one("div.shadow-xs")
|
219 |
+
if submitted_div:
|
220 |
+
submitter_text = submitted_div.get_text(strip=True)
|
221 |
+
# Extract submitter name from "Submitted byLiang0223" (no space)
|
222 |
+
submitter_match = re.search(r'Submitted by(\S+)', submitter_text)
|
223 |
+
if submitter_match:
|
224 |
+
card_data["submitter"] = submitter_match.group(1)
|
225 |
+
|
226 |
+
# Extract arXiv ID from the URL
|
227 |
+
if card_data.get("huggingface_url"):
|
228 |
+
arxiv_id = self.extract_arxiv_id(card_data["huggingface_url"])
|
229 |
+
if arxiv_id:
|
230 |
+
card_data["arxiv_id"] = arxiv_id
|
231 |
+
|
232 |
+
# Try to get GitHub stars from the extracted data
|
233 |
+
# Look for GitHub stars by matching paper title
|
234 |
+
paper_title = card_data.get("title", "")
|
235 |
+
if paper_title in json_data.get("github_stars_map", {}):
|
236 |
+
card_data["github_stars"] = json_data["github_stars_map"][paper_title]
|
237 |
+
|
238 |
+
# Only add cards that have at least a title
|
239 |
+
if card_data.get("title"):
|
240 |
+
cards.append(card_data)
|
241 |
+
|
242 |
+
except Exception as e:
|
243 |
+
logger.error(f"Error parsing card: {e}")
|
244 |
+
continue
|
245 |
+
|
246 |
+
# If the above method didn't work, fall back to the old method
|
247 |
+
if not cards:
|
248 |
+
logger.info("Falling back to old parsing method")
|
249 |
+
for h3 in soup.select("h3"):
|
250 |
+
# Title and Hugging Face paper link (if present)
|
251 |
+
a = h3.find("a")
|
252 |
+
title = h3.get_text(strip=True)
|
253 |
+
hf_link = None
|
254 |
+
if a and a.get("href"):
|
255 |
+
href = a.get("href")
|
256 |
+
# Absolute URL to huggingface
|
257 |
+
if href.startswith("http"):
|
258 |
+
hf_link = href
|
259 |
+
else:
|
260 |
+
hf_link = f"https://huggingface.co{href}"
|
261 |
+
|
262 |
+
# Try to capture sibling info (authors, votes, etc.) as a small snippet
|
263 |
+
meta_text = None
|
264 |
+
parent = h3.parent
|
265 |
+
if parent:
|
266 |
+
# Join immediate text content following h3
|
267 |
+
collected: List[str] = []
|
268 |
+
for sib in parent.find_all(text=True, recursive=False):
|
269 |
+
t = (sib or "").strip()
|
270 |
+
if t:
|
271 |
+
collected.append(t)
|
272 |
+
if collected:
|
273 |
+
meta_text = " ".join(collected)
|
274 |
+
|
275 |
+
# Try to discover any arXiv link inside nearby anchors
|
276 |
+
arxiv_id: Optional[str] = None
|
277 |
+
container = parent if parent else h3
|
278 |
+
for link in container.find_all("a", href=True):
|
279 |
+
possible = self.extract_arxiv_id(link["href"])
|
280 |
+
if possible:
|
281 |
+
arxiv_id = possible
|
282 |
+
break
|
283 |
+
|
284 |
+
cards.append(
|
285 |
+
{
|
286 |
+
"title": title,
|
287 |
+
"huggingface_url": hf_link,
|
288 |
+
"meta": meta_text,
|
289 |
+
"arxiv_id": arxiv_id,
|
290 |
+
}
|
291 |
+
)
|
292 |
+
|
293 |
+
# Deduplicate by title
|
294 |
+
seen = set()
|
295 |
+
unique_cards: List[Dict[str, Any]] = []
|
296 |
+
for c in cards:
|
297 |
+
key = c.get("title") or ""
|
298 |
+
if key and key not in seen:
|
299 |
+
seen.add(key)
|
300 |
+
unique_cards.append(c)
|
301 |
+
|
302 |
+
logger.info(f"Parsed {len(unique_cards)} cards")
|
303 |
+
return unique_cards
|
304 |
+
|
305 |
+
async def get_daily_papers(self, target_date: str) -> tuple[str, List[Dict[str, Any]]]:
|
306 |
+
"""Get daily papers for a specific date"""
|
307 |
+
date_str, html = await self.fetch_daily_html(target_date)
|
308 |
+
cards = self.parse_daily_cards(html)
|
309 |
+
return date_str, cards
|
src/database/db.py
CHANGED
@@ -30,6 +30,27 @@ class PapersDatabase():
|
|
30 |
)
|
31 |
''')
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
# Create latest_date table to track the most recent available date
|
34 |
cursor.execute('''
|
35 |
CREATE TABLE IF NOT EXISTS latest_date (
|
@@ -59,7 +80,7 @@ class PapersDatabase():
|
|
59 |
|
60 |
def get_cached_papers(self, date_str: str) -> Optional[Dict[str, Any]]:
|
61 |
"""Get cached papers for a specific date"""
|
62 |
-
with self.get_connection(
|
63 |
cursor = conn.cursor()
|
64 |
cursor.execute('''
|
65 |
SELECT parsed_cards, created_at
|
@@ -134,6 +155,122 @@ class PapersDatabase():
|
|
134 |
''', (cutoff_date,))
|
135 |
conn.commit()
|
136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
def __str__(self):
|
138 |
return f"PapersDatabase(db_path={self.db_path})"
|
139 |
|
|
|
30 |
)
|
31 |
''')
|
32 |
|
33 |
+
# Create papers table for individual arXiv papers
|
34 |
+
cursor.execute('''
|
35 |
+
CREATE TABLE IF NOT EXISTS papers (
|
36 |
+
arxiv_id TEXT PRIMARY KEY,
|
37 |
+
title TEXT NOT NULL,
|
38 |
+
authors TEXT NOT NULL,
|
39 |
+
abstract TEXT,
|
40 |
+
categories TEXT,
|
41 |
+
published_date TEXT,
|
42 |
+
evaluation_content TEXT,
|
43 |
+
evaluation_score REAL,
|
44 |
+
overall_score REAL,
|
45 |
+
evaluation_tags TEXT,
|
46 |
+
evaluation_status TEXT DEFAULT 'not_started',
|
47 |
+
is_evaluated BOOLEAN DEFAULT FALSE,
|
48 |
+
evaluation_date TIMESTAMP,
|
49 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
50 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
51 |
+
)
|
52 |
+
''')
|
53 |
+
|
54 |
# Create latest_date table to track the most recent available date
|
55 |
cursor.execute('''
|
56 |
CREATE TABLE IF NOT EXISTS latest_date (
|
|
|
80 |
|
81 |
def get_cached_papers(self, date_str: str) -> Optional[Dict[str, Any]]:
|
82 |
"""Get cached papers for a specific date"""
|
83 |
+
with self.get_connection() as conn:
|
84 |
cursor = conn.cursor()
|
85 |
cursor.execute('''
|
86 |
SELECT parsed_cards, created_at
|
|
|
155 |
''', (cutoff_date,))
|
156 |
conn.commit()
|
157 |
|
158 |
+
# Papers table methods
|
159 |
+
def insert_paper(self, arxiv_id: str, title: str, authors: str, abstract: str = None,
|
160 |
+
categories: str = None, published_date: str = None):
|
161 |
+
"""Insert a new paper into the papers table"""
|
162 |
+
with self.get_connection() as conn:
|
163 |
+
cursor = conn.cursor()
|
164 |
+
cursor.execute('''
|
165 |
+
INSERT OR REPLACE INTO papers
|
166 |
+
(arxiv_id, title, authors, abstract, categories, published_date, updated_at)
|
167 |
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
168 |
+
''', (arxiv_id, title, authors, abstract, categories, published_date))
|
169 |
+
conn.commit()
|
170 |
+
|
171 |
+
def get_paper(self, arxiv_id: str) -> Optional[Dict[str, Any]]:
|
172 |
+
"""Get a paper by arxiv_id"""
|
173 |
+
with self.get_connection() as conn:
|
174 |
+
cursor = conn.cursor()
|
175 |
+
cursor.execute('''
|
176 |
+
SELECT * FROM papers WHERE arxiv_id = ?
|
177 |
+
''', (arxiv_id,))
|
178 |
+
|
179 |
+
row = cursor.fetchone()
|
180 |
+
if row:
|
181 |
+
return dict(row)
|
182 |
+
return None
|
183 |
+
|
184 |
+
def get_papers_by_evaluation_status(self, is_evaluated: bool = None) -> List[Dict[str, Any]]:
|
185 |
+
"""Get papers by evaluation status"""
|
186 |
+
with self.get_connection() as conn:
|
187 |
+
cursor = conn.cursor()
|
188 |
+
if is_evaluated is None:
|
189 |
+
cursor.execute('SELECT * FROM papers ORDER BY created_at DESC')
|
190 |
+
else:
|
191 |
+
cursor.execute('''
|
192 |
+
SELECT * FROM papers
|
193 |
+
WHERE is_evaluated = ?
|
194 |
+
ORDER BY created_at DESC
|
195 |
+
''', (is_evaluated,))
|
196 |
+
|
197 |
+
return [dict(row) for row in cursor.fetchall()]
|
198 |
+
|
199 |
+
def update_paper_evaluation(self, arxiv_id: str, evaluation_content: str,
|
200 |
+
evaluation_score: float = None, overall_score: float = None, evaluation_tags: str = None):
|
201 |
+
"""Update paper with evaluation content"""
|
202 |
+
with self.get_connection() as conn:
|
203 |
+
cursor = conn.cursor()
|
204 |
+
cursor.execute('''
|
205 |
+
UPDATE papers
|
206 |
+
SET evaluation_content = ?,
|
207 |
+
evaluation_score = ?,
|
208 |
+
overall_score = ?,
|
209 |
+
evaluation_tags = ?,
|
210 |
+
is_evaluated = TRUE,
|
211 |
+
evaluation_status = 'completed',
|
212 |
+
evaluation_date = CURRENT_TIMESTAMP,
|
213 |
+
updated_at = CURRENT_TIMESTAMP
|
214 |
+
WHERE arxiv_id = ?
|
215 |
+
''', (evaluation_content, evaluation_score, overall_score, evaluation_tags, arxiv_id))
|
216 |
+
conn.commit()
|
217 |
+
|
218 |
+
def update_paper_status(self, arxiv_id: str, status: str):
|
219 |
+
"""Update paper evaluation status"""
|
220 |
+
with self.get_connection() as conn:
|
221 |
+
cursor = conn.cursor()
|
222 |
+
cursor.execute('''
|
223 |
+
UPDATE papers
|
224 |
+
SET evaluation_status = ?,
|
225 |
+
updated_at = CURRENT_TIMESTAMP
|
226 |
+
WHERE arxiv_id = ?
|
227 |
+
''', (status, arxiv_id))
|
228 |
+
conn.commit()
|
229 |
+
|
230 |
+
def get_unevaluated_papers(self) -> List[Dict[str, Any]]:
|
231 |
+
"""Get all papers that haven't been evaluated yet"""
|
232 |
+
return self.get_papers_by_evaluation_status(is_evaluated=False)
|
233 |
+
|
234 |
+
def get_evaluated_papers(self) -> List[Dict[str, Any]]:
|
235 |
+
"""Get all papers that have been evaluated"""
|
236 |
+
return self.get_papers_by_evaluation_status(is_evaluated=True)
|
237 |
+
|
238 |
+
def search_papers(self, query: str) -> List[Dict[str, Any]]:
|
239 |
+
"""Search papers by title, authors, or abstract"""
|
240 |
+
with self.get_connection() as conn:
|
241 |
+
cursor = conn.cursor()
|
242 |
+
search_pattern = f'%{query}%'
|
243 |
+
cursor.execute('''
|
244 |
+
SELECT * FROM papers
|
245 |
+
WHERE title LIKE ? OR authors LIKE ? OR abstract LIKE ?
|
246 |
+
ORDER BY created_at DESC
|
247 |
+
''', (search_pattern, search_pattern, search_pattern))
|
248 |
+
|
249 |
+
return [dict(row) for row in cursor.fetchall()]
|
250 |
+
|
251 |
+
def delete_paper(self, arxiv_id: str):
|
252 |
+
"""Delete a paper from the database"""
|
253 |
+
with self.get_connection() as conn:
|
254 |
+
cursor = conn.cursor()
|
255 |
+
cursor.execute('DELETE FROM papers WHERE arxiv_id = ?', (arxiv_id,))
|
256 |
+
conn.commit()
|
257 |
+
|
258 |
+
def get_papers_count(self) -> Dict[str, int]:
|
259 |
+
"""Get count of papers by evaluation status"""
|
260 |
+
with self.get_connection() as conn:
|
261 |
+
cursor = conn.cursor()
|
262 |
+
cursor.execute('SELECT COUNT(*) as total FROM papers')
|
263 |
+
total = cursor.fetchone()['total']
|
264 |
+
|
265 |
+
cursor.execute('SELECT COUNT(*) as evaluated FROM papers WHERE is_evaluated = TRUE')
|
266 |
+
evaluated = cursor.fetchone()['evaluated']
|
267 |
+
|
268 |
+
return {
|
269 |
+
'total': total,
|
270 |
+
'evaluated': evaluated,
|
271 |
+
'unevaluated': total - evaluated
|
272 |
+
}
|
273 |
+
|
274 |
def __str__(self):
|
275 |
return f"PapersDatabase(db_path={self.db_path})"
|
276 |
|
src/logger/__init__.py
CHANGED
@@ -1,10 +1,7 @@
|
|
1 |
-
from .
|
2 |
-
from .monitor import Monitor, Timing, TokenUsage
|
3 |
|
4 |
__all__ = ["logger",
|
5 |
"LogLevel",
|
6 |
-
"
|
7 |
-
"Monitor",
|
8 |
"YELLOW_HEX",
|
9 |
-
|
10 |
-
"TokenUsage"]
|
|
|
1 |
+
from .log import logger, LogLevel, Logger, YELLOW_HEX
|
|
|
2 |
|
3 |
__all__ = ["logger",
|
4 |
"LogLevel",
|
5 |
+
"Logger",
|
|
|
6 |
"YELLOW_HEX",
|
7 |
+
]
|
|
src/logger/log.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from enum import IntEnum
|
3 |
+
from typing import Any, Optional
|
4 |
+
|
5 |
+
from rich.console import Console, Group
|
6 |
+
from rich.panel import Panel
|
7 |
+
from rich.rule import Rule
|
8 |
+
from rich.syntax import Syntax
|
9 |
+
from rich.table import Table
|
10 |
+
from rich.tree import Tree
|
11 |
+
from rich.logging import RichHandler
|
12 |
+
|
13 |
+
from src.utils import Singleton
|
14 |
+
|
15 |
+
YELLOW_HEX = "#d4b702"
|
16 |
+
|
17 |
+
class LogLevel(IntEnum):
|
18 |
+
CRITICAL = logging.CRITICAL
|
19 |
+
FATAL = logging.FATAL
|
20 |
+
ERROR = logging.ERROR
|
21 |
+
WARNING = logging.WARNING
|
22 |
+
WARN = logging.WARN
|
23 |
+
INFO = logging.INFO
|
24 |
+
DEBUG = logging.DEBUG
|
25 |
+
|
26 |
+
class Logger(logging.Logger, metaclass=Singleton):
|
27 |
+
def __init__(self, name="logger", level=logging.INFO):
|
28 |
+
# Initialize the parent class
|
29 |
+
super().__init__(name, level)
|
30 |
+
|
31 |
+
# Define a formatter for log messages
|
32 |
+
self.formatter = logging.Formatter(
|
33 |
+
fmt="%(asctime)s - %(name)s:%(levelname)s - %(filename)s:%(lineno)s - %(message)s",
|
34 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
35 |
+
)
|
36 |
+
|
37 |
+
def init_logger(self, config, level: int = LogLevel.INFO):
|
38 |
+
"""
|
39 |
+
Initialize the logger with a file path and optional main process check.
|
40 |
+
|
41 |
+
Args:
|
42 |
+
log_path (str): The log file path.
|
43 |
+
level (int, optional): The logging level. Defaults to logging.INFO.
|
44 |
+
accelerator (Accelerator, optional): Accelerator instance to determine the main process.
|
45 |
+
"""
|
46 |
+
|
47 |
+
log_path = config.log_path
|
48 |
+
|
49 |
+
self.handlers.clear()
|
50 |
+
|
51 |
+
self.console = Console(
|
52 |
+
width=None,
|
53 |
+
markup=True,
|
54 |
+
color_system="truecolor",
|
55 |
+
force_terminal=True
|
56 |
+
)
|
57 |
+
rich_handler = RichHandler(
|
58 |
+
console=self.console,
|
59 |
+
rich_tracebacks=True,
|
60 |
+
show_time=False,
|
61 |
+
show_level=False,
|
62 |
+
show_path=False,
|
63 |
+
markup=True,
|
64 |
+
omit_repeated_times=False
|
65 |
+
)
|
66 |
+
rich_handler.setLevel(level)
|
67 |
+
rich_handler.setFormatter(self.formatter)
|
68 |
+
self.addHandler(rich_handler)
|
69 |
+
|
70 |
+
self.file_console = Console(
|
71 |
+
width=None,
|
72 |
+
markup=True,
|
73 |
+
color_system="truecolor",
|
74 |
+
force_terminal=True,
|
75 |
+
file=open(log_path, "a", encoding="utf-8")
|
76 |
+
)
|
77 |
+
rich_file_handler = RichHandler(
|
78 |
+
console=self.file_console,
|
79 |
+
rich_tracebacks=True,
|
80 |
+
show_time=False,
|
81 |
+
show_level=False,
|
82 |
+
show_path=False,
|
83 |
+
markup=True,
|
84 |
+
omit_repeated_times=False,
|
85 |
+
)
|
86 |
+
rich_file_handler.setLevel(level)
|
87 |
+
rich_file_handler.setFormatter(self.formatter)
|
88 |
+
self.addHandler(rich_file_handler)
|
89 |
+
|
90 |
+
self.propagate = False
|
91 |
+
|
92 |
+
def info(self, msg, *args, **kwargs):
|
93 |
+
"""
|
94 |
+
Only for string messages, not for rich objects.
|
95 |
+
"""
|
96 |
+
kwargs.setdefault("stacklevel", 2)
|
97 |
+
|
98 |
+
if "style" in kwargs:
|
99 |
+
kwargs.pop("style")
|
100 |
+
if "level" in kwargs:
|
101 |
+
kwargs.pop("level")
|
102 |
+
super().info(msg, *args, **kwargs)
|
103 |
+
|
104 |
+
def warning(self, msg, *args, **kwargs):
|
105 |
+
"""
|
106 |
+
Only for string messages, not for rich objects.
|
107 |
+
"""
|
108 |
+
kwargs.setdefault("stacklevel", 2)
|
109 |
+
super().warning(msg, *args, **kwargs)
|
110 |
+
|
111 |
+
def error(self, msg, *args, **kwargs):
|
112 |
+
kwargs.setdefault("stacklevel", 2)
|
113 |
+
super().error(msg, *args, **kwargs)
|
114 |
+
|
115 |
+
def critical(self, msg, *args, **kwargs):
|
116 |
+
kwargs.setdefault("stacklevel", 2)
|
117 |
+
super().critical(msg, *args, **kwargs)
|
118 |
+
|
119 |
+
def debug(self, msg, *args, **kwargs):
|
120 |
+
kwargs.setdefault("stacklevel", 2)
|
121 |
+
super().debug(msg, *args, **kwargs)
|
122 |
+
|
123 |
+
def log(self,
|
124 |
+
msg: Optional[Any] = None,
|
125 |
+
level: LogLevel = LogLevel.INFO,
|
126 |
+
**kwargs):
|
127 |
+
"""
|
128 |
+
Log a rich object or a string message to both console and file.
|
129 |
+
"""
|
130 |
+
if isinstance(msg, str):
|
131 |
+
self.info(msg, **kwargs)
|
132 |
+
elif isinstance(msg, (Group, Panel, Rule, Syntax, Table, Tree)):
|
133 |
+
self.console.print(msg, **kwargs)
|
134 |
+
self.file_console.print(msg, **kwargs)
|
135 |
+
|
136 |
+
logger = Logger()
|
src/logger/logger.py
DELETED
@@ -1,229 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
import json
|
3 |
-
from enum import IntEnum
|
4 |
-
|
5 |
-
from rich import box
|
6 |
-
from rich.console import Console, Group
|
7 |
-
from rich.panel import Panel
|
8 |
-
from rich.rule import Rule
|
9 |
-
from rich.syntax import Syntax
|
10 |
-
from rich.table import Table
|
11 |
-
from rich.tree import Tree
|
12 |
-
|
13 |
-
from src.utils import (
|
14 |
-
escape_code_brackets,
|
15 |
-
Singleton
|
16 |
-
)
|
17 |
-
|
18 |
-
YELLOW_HEX = "#d4b702"
|
19 |
-
|
20 |
-
class LogLevel(IntEnum):
|
21 |
-
OFF = -1 # No output
|
22 |
-
ERROR = 0 # Only errors
|
23 |
-
INFO = 1 # Normal output (default)
|
24 |
-
DEBUG = 2 # Detailed output
|
25 |
-
|
26 |
-
class AgentLogger(logging.Logger, metaclass=Singleton):
|
27 |
-
def __init__(self, name="logger", level=logging.INFO):
|
28 |
-
# Initialize the parent class
|
29 |
-
super().__init__(name, level)
|
30 |
-
|
31 |
-
# Define a formatter for log messages
|
32 |
-
self.formatter = logging.Formatter(
|
33 |
-
fmt="\033[92m%(asctime)s - %(name)s:%(levelname)s\033[0m: %(filename)s:%(lineno)s - %(message)s",
|
34 |
-
datefmt="%H:%M:%S",
|
35 |
-
)
|
36 |
-
|
37 |
-
def init_logger(self, log_path: str, level=logging.INFO):
|
38 |
-
"""
|
39 |
-
Initialize the logger with a file path and optional main process check.
|
40 |
-
|
41 |
-
Args:
|
42 |
-
log_path (str): The log file path.
|
43 |
-
level (int, optional): The logging level. Defaults to logging.INFO.
|
44 |
-
accelerator (Accelerator, optional): Accelerator instance to determine the main process.
|
45 |
-
"""
|
46 |
-
|
47 |
-
# Add a console handler for logging to the console
|
48 |
-
console_handler = logging.StreamHandler()
|
49 |
-
console_handler.setLevel(level)
|
50 |
-
console_handler.setFormatter(self.formatter)
|
51 |
-
self.addHandler(console_handler)
|
52 |
-
|
53 |
-
# Add a file handler for logging to the file
|
54 |
-
file_handler = logging.FileHandler(
|
55 |
-
log_path, mode="a"
|
56 |
-
) # 'a' mode appends to the file
|
57 |
-
file_handler.setLevel(level)
|
58 |
-
file_handler.setFormatter(self.formatter)
|
59 |
-
self.addHandler(file_handler)
|
60 |
-
|
61 |
-
self.console = Console(width=100)
|
62 |
-
self.file_console = Console(file=open(log_path, "a"), width=100)
|
63 |
-
|
64 |
-
# Prevent duplicate logs from propagating to the root logger
|
65 |
-
self.propagate = False
|
66 |
-
|
67 |
-
def log(self, *args, level: int | str | LogLevel = LogLevel.INFO, **kwargs) -> None:
|
68 |
-
"""Logs a message to the console.
|
69 |
-
|
70 |
-
Args:
|
71 |
-
level (LogLevel, optional): Defaults to LogLevel.INFO.
|
72 |
-
"""
|
73 |
-
if isinstance(level, str):
|
74 |
-
level = LogLevel[level.upper()]
|
75 |
-
if level <= self.level:
|
76 |
-
self.info(*args, **kwargs)
|
77 |
-
|
78 |
-
def info(self, msg, *args, **kwargs):
|
79 |
-
"""
|
80 |
-
Overridden info method with stacklevel adjustment for correct log location.
|
81 |
-
"""
|
82 |
-
if isinstance(msg, (Rule, Panel, Group, Tree, Table, Syntax)):
|
83 |
-
self.console.print(msg)
|
84 |
-
self.file_console.print(msg)
|
85 |
-
else:
|
86 |
-
kwargs.setdefault(
|
87 |
-
"stacklevel", 2
|
88 |
-
) # Adjust stack level to show the actual caller
|
89 |
-
if "style" in kwargs:
|
90 |
-
kwargs.pop("style")
|
91 |
-
if "level" in kwargs:
|
92 |
-
kwargs.pop("level")
|
93 |
-
super().info(msg, *args, **kwargs)
|
94 |
-
|
95 |
-
def warning(self, msg, *args, **kwargs):
|
96 |
-
kwargs.setdefault("stacklevel", 2)
|
97 |
-
super().warning(msg, *args, **kwargs)
|
98 |
-
|
99 |
-
def error(self, msg, *args, **kwargs):
|
100 |
-
kwargs.setdefault("stacklevel", 2)
|
101 |
-
super().error(msg, *args, **kwargs)
|
102 |
-
|
103 |
-
def critical(self, msg, *args, **kwargs):
|
104 |
-
kwargs.setdefault("stacklevel", 2)
|
105 |
-
super().critical(msg, *args, **kwargs)
|
106 |
-
|
107 |
-
def debug(self, msg, *args, **kwargs):
|
108 |
-
kwargs.setdefault("stacklevel", 2)
|
109 |
-
super().debug(msg, *args, **kwargs)
|
110 |
-
|
111 |
-
def log_error(self, error_message: str) -> None:
|
112 |
-
self.info(escape_code_brackets(error_message), style="bold red", level=LogLevel.ERROR)
|
113 |
-
|
114 |
-
def log_markdown(self, content: str, title: str | None = None, level=LogLevel.INFO, style=YELLOW_HEX) -> None:
|
115 |
-
markdown_content = Syntax(
|
116 |
-
content,
|
117 |
-
lexer="markdown",
|
118 |
-
theme="github-dark",
|
119 |
-
word_wrap=True,
|
120 |
-
)
|
121 |
-
if title:
|
122 |
-
self.info(
|
123 |
-
Group(
|
124 |
-
Rule(
|
125 |
-
"[bold italic]" + title,
|
126 |
-
align="left",
|
127 |
-
style=style,
|
128 |
-
),
|
129 |
-
markdown_content,
|
130 |
-
),
|
131 |
-
level=level,
|
132 |
-
)
|
133 |
-
else:
|
134 |
-
self.info(markdown_content, level=level)
|
135 |
-
|
136 |
-
def log_code(self, title: str, content: str, level: int = LogLevel.INFO) -> None:
|
137 |
-
self.info(
|
138 |
-
Panel(
|
139 |
-
Syntax(
|
140 |
-
content,
|
141 |
-
lexer="python",
|
142 |
-
theme="monokai",
|
143 |
-
word_wrap=True,
|
144 |
-
),
|
145 |
-
title="[bold]" + title,
|
146 |
-
title_align="left",
|
147 |
-
box=box.HORIZONTALS,
|
148 |
-
),
|
149 |
-
level=level,
|
150 |
-
)
|
151 |
-
|
152 |
-
def log_rule(self, title: str, level: int = LogLevel.INFO) -> None:
|
153 |
-
self.info(
|
154 |
-
Rule(
|
155 |
-
"[bold]" + title,
|
156 |
-
characters="━",
|
157 |
-
style=YELLOW_HEX,
|
158 |
-
),
|
159 |
-
level=LogLevel.INFO,
|
160 |
-
)
|
161 |
-
|
162 |
-
def log_task(self, content: str, subtitle: str, title: str | None = None, level: LogLevel = LogLevel.INFO) -> None:
|
163 |
-
self.info(
|
164 |
-
Panel(
|
165 |
-
f"\n[bold]{escape_code_brackets(content)}\n",
|
166 |
-
title="[bold]New run" + (f" - {title}" if title else ""),
|
167 |
-
subtitle=subtitle,
|
168 |
-
border_style=YELLOW_HEX,
|
169 |
-
subtitle_align="left",
|
170 |
-
),
|
171 |
-
level=level,
|
172 |
-
)
|
173 |
-
|
174 |
-
def log_messages(self, messages: list[dict], level: LogLevel = LogLevel.DEBUG) -> None:
|
175 |
-
messages_as_string = "\n".join([json.dumps(dict(message), indent=4, ensure_ascii=False) for message in messages])
|
176 |
-
self.info(
|
177 |
-
Syntax(
|
178 |
-
messages_as_string,
|
179 |
-
lexer="markdown",
|
180 |
-
theme="github-dark",
|
181 |
-
word_wrap=True,
|
182 |
-
),
|
183 |
-
level=level,
|
184 |
-
)
|
185 |
-
|
186 |
-
def visualize_agent_tree(self, agent):
|
187 |
-
def create_tools_section(tools_dict):
|
188 |
-
table = Table(show_header=True, header_style="bold")
|
189 |
-
table.add_column("Name", style="#1E90FF")
|
190 |
-
table.add_column("Description")
|
191 |
-
table.add_column("Arguments")
|
192 |
-
|
193 |
-
for name, tool in tools_dict.items():
|
194 |
-
args = [
|
195 |
-
f"{arg_name} (`{info.get('type', 'Any')}`{', optional' if info.get('optional') else ''}): {info.get('description', '')}"
|
196 |
-
for arg_name, info in getattr(tool, "inputs", {}).items()
|
197 |
-
]
|
198 |
-
table.add_row(name, getattr(tool, "description", str(tool)), "\n".join(args))
|
199 |
-
|
200 |
-
return Group("🛠️ [italic #1E90FF]Tools:[/italic #1E90FF]", table)
|
201 |
-
|
202 |
-
def get_agent_headline(agent, name: str | None = None):
|
203 |
-
name_headline = f"{name} | " if name else ""
|
204 |
-
return f"[bold {YELLOW_HEX}]{name_headline}{agent.__class__.__name__} | {agent.model.model_id}"
|
205 |
-
|
206 |
-
def build_agent_tree(parent_tree, agent_obj):
|
207 |
-
"""Recursively builds the agent tree."""
|
208 |
-
parent_tree.add(create_tools_section(agent_obj.tools))
|
209 |
-
|
210 |
-
if agent_obj.managed_agents:
|
211 |
-
agents_branch = parent_tree.add("🤖 [italic #1E90FF]Managed agents:")
|
212 |
-
for name, managed_agent in agent_obj.managed_agents.items():
|
213 |
-
agent_tree = agents_branch.add(get_agent_headline(managed_agent, name))
|
214 |
-
if managed_agent.__class__.__name__ == "CodeAgent":
|
215 |
-
agent_tree.add(
|
216 |
-
f"✅ [italic #1E90FF]Authorized imports:[/italic #1E90FF] {managed_agent.additional_authorized_imports}"
|
217 |
-
)
|
218 |
-
agent_tree.add(f"📝 [italic #1E90FF]Description:[/italic #1E90FF] {managed_agent.description}")
|
219 |
-
build_agent_tree(agent_tree, managed_agent)
|
220 |
-
|
221 |
-
main_tree = Tree(get_agent_headline(agent))
|
222 |
-
if agent.__class__.__name__ == "CodeAgent":
|
223 |
-
main_tree.add(
|
224 |
-
f"✅ [italic #1E90FF]Authorized imports:[/italic #1E90FF] {agent.additional_authorized_imports}"
|
225 |
-
)
|
226 |
-
build_agent_tree(main_tree, agent)
|
227 |
-
self.console.print(main_tree)
|
228 |
-
|
229 |
-
logger = AgentLogger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/__init__.py
CHANGED
@@ -4,5 +4,5 @@ from .singleton import Singleton
|
|
4 |
__all__ = [
|
5 |
"get_project_root",
|
6 |
"assemble_project_path",
|
7 |
-
"Singleton"
|
8 |
]
|
|
|
4 |
__all__ = [
|
5 |
"get_project_root",
|
6 |
"assemble_project_path",
|
7 |
+
"Singleton",
|
8 |
]
|
src/utils/hf_utils.py
DELETED
File without changes
|
test_evaluation.py
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script: Verify that the run_evaluation function works correctly
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
from pathlib import Path
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
import argparse
|
12 |
+
from mmengine import DictAction
|
13 |
+
|
14 |
+
# 加载环境变量
|
15 |
+
load_dotenv(verbose=True)
|
16 |
+
|
17 |
+
# 设置根目录路径
|
18 |
+
root = str(Path(__file__).parent)
|
19 |
+
sys.path.append(root)
|
20 |
+
|
21 |
+
from src.database import db
|
22 |
+
from src.logger import logger
|
23 |
+
from src.config import config
|
24 |
+
from src.agents.evaluator import run_evaluation
|
25 |
+
|
26 |
+
|
27 |
+
def parse_args():
|
28 |
+
"""Parse command line arguments"""
|
29 |
+
parser = argparse.ArgumentParser(description='main')
|
30 |
+
parser.add_argument("--config", default=os.path.join(root, "configs", "paper_agent.py"), help="config file path")
|
31 |
+
|
32 |
+
parser.add_argument(
|
33 |
+
'--cfg-options',
|
34 |
+
nargs='+',
|
35 |
+
action=DictAction,
|
36 |
+
help='override some settings in the used config, the key-value pair '
|
37 |
+
'in xxx=yyy format will be merged into config file. If the value to '
|
38 |
+
'be overwritten is a list, it should be like key="[a,b]" or key=a,b '
|
39 |
+
'It also allows nested list/tuple values, e.g. key="[(a,b),(c,d)]" '
|
40 |
+
'Note that the quotation marks are necessary and that no white space '
|
41 |
+
'is allowed.')
|
42 |
+
args = parser.parse_args()
|
43 |
+
return args
|
44 |
+
|
45 |
+
|
46 |
+
async def test_evaluation():
|
47 |
+
"""Test evaluation functionality"""
|
48 |
+
print("=== Starting Evaluation Test ===")
|
49 |
+
|
50 |
+
# Test parameters
|
51 |
+
test_arxiv_id = "2508.09889" # Use existing paper in database
|
52 |
+
test_pdf_url = f"https://arxiv.org/pdf/{test_arxiv_id}.pdf"
|
53 |
+
|
54 |
+
print(f"Test paper ID: {test_arxiv_id}")
|
55 |
+
print(f"PDF URL: {test_pdf_url}")
|
56 |
+
|
57 |
+
# Check API key
|
58 |
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
59 |
+
if not api_key:
|
60 |
+
print("❌ Error: ANTHROPIC_API_KEY environment variable not found")
|
61 |
+
return False
|
62 |
+
|
63 |
+
print(f"✅ API key is set: {api_key[:20]}...")
|
64 |
+
|
65 |
+
try:
|
66 |
+
# Check if paper exists in database
|
67 |
+
paper = db.get_paper(test_arxiv_id)
|
68 |
+
if paper:
|
69 |
+
print(f"✅ Paper found in database: {paper['title']}")
|
70 |
+
else:
|
71 |
+
print(f"⚠️ Paper not in database, creating new record")
|
72 |
+
# Insert test paper
|
73 |
+
db.insert_paper(
|
74 |
+
arxiv_id=test_arxiv_id,
|
75 |
+
title="Test Paper for Evaluation",
|
76 |
+
authors="Test Author",
|
77 |
+
abstract="This is a test paper for evaluation.",
|
78 |
+
categories="cs.AI",
|
79 |
+
published_date="2024-08-01"
|
80 |
+
)
|
81 |
+
print(f"✅ Test paper inserted into database")
|
82 |
+
|
83 |
+
print("\n=== Starting Evaluation ===")
|
84 |
+
|
85 |
+
# Run evaluation
|
86 |
+
result = await run_evaluation(
|
87 |
+
pdf_path=test_pdf_url,
|
88 |
+
arxiv_id=test_arxiv_id,
|
89 |
+
api_key=api_key
|
90 |
+
)
|
91 |
+
|
92 |
+
print(f"\n=== Evaluation Results ===")
|
93 |
+
print(f"Result length: {len(result)} characters")
|
94 |
+
print(f"First 500 characters: {result[:500]}...")
|
95 |
+
|
96 |
+
# Check if result contains expected content
|
97 |
+
if "AI Automation Assessment" in result or "Executive Summary" in result:
|
98 |
+
print("✅ Evaluation result contains expected content")
|
99 |
+
else:
|
100 |
+
print("⚠️ Evaluation result may be incomplete")
|
101 |
+
|
102 |
+
# Check evaluation status in database
|
103 |
+
updated_paper = db.get_paper(test_arxiv_id)
|
104 |
+
if updated_paper and updated_paper.get('is_evaluated'):
|
105 |
+
print("✅ Evaluation saved to database")
|
106 |
+
print(f"Evaluation score: {updated_paper.get('evaluation_score')}")
|
107 |
+
print(f"Evaluation tags: {updated_paper.get('evaluation_tags')}")
|
108 |
+
else:
|
109 |
+
print("❌ Evaluation not saved to database")
|
110 |
+
|
111 |
+
return True
|
112 |
+
|
113 |
+
except Exception as e:
|
114 |
+
print(f"❌ Error during evaluation: {str(e)}")
|
115 |
+
import traceback
|
116 |
+
traceback.print_exc()
|
117 |
+
return False
|
118 |
+
|
119 |
+
|
120 |
+
async def test_database_operations():
|
121 |
+
"""Test database operations"""
|
122 |
+
print("\n=== Testing Database Operations ===")
|
123 |
+
|
124 |
+
try:
|
125 |
+
# Test getting paper
|
126 |
+
paper = db.get_paper("2508.09889")
|
127 |
+
if paper:
|
128 |
+
print(f"✅ Database connection OK, found paper: {paper['title']}")
|
129 |
+
else:
|
130 |
+
print("⚠️ Test paper not found in database")
|
131 |
+
|
132 |
+
# Test getting paper statistics
|
133 |
+
stats = db.get_papers_count()
|
134 |
+
print(f"✅ Paper statistics: Total={stats['total']}, Evaluated={stats['evaluated']}, Unevaluated={stats['unevaluated']}")
|
135 |
+
|
136 |
+
return True
|
137 |
+
|
138 |
+
except Exception as e:
|
139 |
+
print(f"❌ Database operation error: {str(e)}")
|
140 |
+
return False
|
141 |
+
|
142 |
+
|
143 |
+
async def main():
|
144 |
+
"""Main test function"""
|
145 |
+
print("🚀 Starting Evaluation System Test")
|
146 |
+
|
147 |
+
# Parse command line arguments
|
148 |
+
args = parse_args()
|
149 |
+
|
150 |
+
# Initialize configuration
|
151 |
+
config.init_config(args.config, args)
|
152 |
+
|
153 |
+
# Initialize logger
|
154 |
+
logger.init_logger(config=config)
|
155 |
+
logger.info(f"| Logger initialized at: {config.log_path}")
|
156 |
+
logger.info(f"| Config:\n{config.pretty_text}")
|
157 |
+
|
158 |
+
# Initialize database
|
159 |
+
db.init_db(config=config)
|
160 |
+
logger.info(f"| Database initialized at: {config.db_path}")
|
161 |
+
|
162 |
+
print(f"✅ Database initialized: {config.db_path}")
|
163 |
+
|
164 |
+
# Test database operations
|
165 |
+
db_success = await test_database_operations()
|
166 |
+
|
167 |
+
# Test evaluation functionality
|
168 |
+
eval_success = await test_evaluation()
|
169 |
+
|
170 |
+
print("\n=== Test Summary ===")
|
171 |
+
print(f"Database operations: {'✅ Success' if db_success else '❌ Failed'}")
|
172 |
+
print(f"Evaluation functionality: {'✅ Success' if eval_success else '❌ Failed'}")
|
173 |
+
|
174 |
+
if db_success and eval_success:
|
175 |
+
print("🎉 All tests passed!")
|
176 |
+
else:
|
177 |
+
print("⚠️ Some tests failed, please check error messages")
|
178 |
+
|
179 |
+
|
180 |
+
if __name__ == "__main__":
|
181 |
+
asyncio.run(main())
|
workdir/2508.05629.json
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"dimensions": "{\n \"task_formalization\": {\n \"score\": 3,\n \"analysis\": \"The research task is highly formalized with clear mathematical objectives. The authors present a mathematical framework for analyzing Supervised Fine-Tuning (SFT) through the lens of Reinforcement Learning (RL). They provide precise mathematical formulations for both SFT and RL objectives, establish a formal equivalence between SFT gradients and policy gradients, and derive their proposed Dynamic Fine-Tuning (DFT) approach with well-defined equations. The paper includes rigorous mathematical proofs and derivations, particularly in Section 3 where they rewrite SFT gradients as policy gradients via importance sampling. While the mathematical formulation is comprehensive, there are some minor implementation details and hyperparameter considerations that leave room for case-by-case adjustments, preventing a perfect score.\"\n },\n \"data_resource_availability\": {\n \"score\": 3,\n \"analysis\": \"The research relies on publicly available datasets and models for experimentation. The authors use established benchmarks including NuminaMath, Math500, Minerva Math, Olympiad Bench, AIME 2024, and AMC 2023. They experiment with multiple open-source models including Qwen2.5-Math, LLaMA-3.1/3.2, and DeepSeekMath. The paper mentions that code will be made publicly available on GitHub. The implementation builds upon existing frameworks (verl, ms-swift) that are accessible. The experimental setup is well-documented, allowing for reproducibility. The primary limitation is that some of the most challenging mathematical benchmarks may have limited sample sizes, and the authors acknowledge not yet testing on a broader range of domains beyond mathematics or with larger models (13B+).\"\n },\n \"input_output_complexity\": {\n \"score\": 2,\n \"analysis\": \"The input-output complexity is moderate. The research deals with complex mathematical reasoning tasks that require processing detailed problem statements and generating multi-step solutions through chain-of-thought reasoning. These outputs can be lengthy and must follow specific mathematical reasoning patterns. However, the structure of the inputs and outputs is relatively well-defined within the domain of mathematical problem-solving. The paper focuses on a specific modification to the training process (adding one line of code) that applies across different model architectures and data types. The implementation requires understanding of token-level probabilities and loss functions, which adds some complexity but is manageable within standard language model frameworks. The method itself is designed to handle complex reasoning tasks, but its implementation is streamlined.\"\n },\n \"real_world_interaction\": {\n \"score\": 4,\n \"analysis\": \"The approach requires minimal real-world interaction. The entire process can be conducted offline with existing datasets and models. Both training and evaluation are fully computational processes that don't require human feedback loops or environmental interaction. The proposed DFT method specifically targets improvements in the standard SFT setting without requiring reward models, preference data, or verification signals that might necessitate additional human feedback. Even in the 'offline RL setting' experiment, the authors use automatically generated samples and verification rather than interactive feedback. The method is designed to work with static datasets and can be deployed in a fully offline manner without any ongoing human or environmental interaction.\"\n },\n \"existing_ai_coverage\": {\n \"score\": 3,\n \"coverage_pct_estimate\": 75,\n \"analysis\": \"A significant portion of the research task is already covered by existing AI tools and models. The core components include mathematical analysis of training objectives, implementation of fine-tuning techniques, experimental evaluation, and visualization of results. Current frameworks like PyTorch, Hugging Face Transformers, and specialized fine-tuning libraries (mentioned verl and ms-swift) provide comprehensive support for implementing various fine-tuning approaches. The mathematical derivation requires human insight, but computational validation of these derivations can be assisted by AI. Existing LLMs can help with code implementation, experimental design, and literature review. The most novel aspect - the insight that SFT can be reframed as RL with an implicit reward structure - required human originality, but once identified, the implementation of the proposed solution (DFT) is straightforward. Most of the experimental pipeline, from data processing to evaluation metrics calculation, can be handled by existing AI tools.\",\n \"tools_models\": [\"PyTorch\", \"Hugging Face Transformers\", \"verl framework\", \"ms-swift framework\", \"Mathematical computation libraries\", \"Data visualization tools\", \"LLMs for code generation\", \"Experimental analysis tools\"]\n },\n \"automation_barriers\": {\n \"analysis\": \"Several barriers limit full automation of this research:\\n\\n1. Theoretical insight: The core insight of connecting SFT and RL through mathematical analysis required creative human reasoning. Identifying the problematic inverse probability weighting in SFT was a novel insight that current AI systems would struggle to generate independently.\\n\\n2. Research direction determination: Choosing to focus on improving SFT rather than developing yet another hybrid SFT-RL method required understanding of research gaps and strategic thinking about valuable contributions to the field.\\n\\n3. Interpretation of results: The analysis of token probability distributions and what they reveal about the learning dynamics of different methods requires domain expertise and causal reasoning that remains challenging for AI.\\n\\n4. Experimental design decisions: Selecting appropriate benchmarks, models, and evaluation methods to comprehensively test the hypothesis required research experience and domain knowledge.\\n\\n5. Limitations analysis: Identifying the boundaries of the approach and potential future work directions demands critical thinking about when and why the approach might fail.\\n\\n6. Interdisciplinary connection: Bridging supervised learning and reinforcement learning perspectives requires deep understanding of both fields and the ability to see non-obvious connections between different learning paradigms.\"\n },\n \"human_originality\": {\n \"score\": 3,\n \"analysis\": \"The research demonstrates clear novelty in its core contribution. The key insight - reinterpreting SFT gradients as policy gradients with an implicit, problematic reward structure - represents an original theoretical connection between two well-established paradigms (SFT and RL). The authors' proposed solution (DFT) is elegantly simple but non-obvious, requiring just one line of code change that produces significant empirical improvements. The mathematical derivation that leads to this insight shows creative thinking in how the authors connect supervised learning to reinforcement learning through importance sampling. The paper also presents a compelling analysis of why this approach works through token probability distribution analysis. While building on established foundations in both supervised learning and reinforcement learning, the specific connection identified and the proposed solution represent a meaningful advance rather than an incremental improvement. The approach inverts conventional wisdom by showing that multiplying the loss by the token probability (opposite of focal loss) improves generalization, which is a novel insight in the era of large language models.\"\n },\n \"safety_ethics\": {\n \"score\": 3,\n \"analysis\": \"The safety and ethical considerations for this research are generally manageable. The proposed method aims to improve the generalization capabilities of language models in mathematical reasoning tasks, which has minimal direct negative implications. The approach does not introduce new safety risks beyond those already present in language model fine-tuning. Failure cases would primarily result in incorrect mathematical reasoning rather than harmful outputs. The research does not involve sensitive data or privacy concerns, as it uses publicly available mathematical benchmarks. The method actually improves robustness and reduces overfitting, potentially making models more reliable. The authors acknowledge limitations of their work and the need for further evaluation across different domains. There is limited discussion of broader societal impacts, though the focus on mathematical reasoning makes immediate misuse scenarios less likely than for general-purpose language models. The method does not significantly increase computational requirements, avoiding major environmental concerns associated with more compute-intensive approaches.\"\n },\n \"societal_economic_impact\": {\n \"analysis\": \"The societal and economic implications of this research are predominantly positive:\\n\\n1. Research efficiency: The proposed DFT method offers a more efficient alternative to complex RL approaches, potentially reducing computational resources needed for effective model fine-tuning. This could democratize access to high-quality fine-tuning techniques for researchers with limited computational budgets.\\n\\n2. Educational applications: Improved mathematical reasoning capabilities in language models could enhance educational tools, making AI tutoring more effective and accessible for mathematics education.\\n\\n3. Scientific advancement: Better generalization in mathematical reasoning could accelerate scientific research that relies on mathematical problem-solving, benefiting fields from physics to economics.\\n\\n4. Resource optimization: The method's improved sample efficiency could reduce the energy consumption and carbon footprint associated with training large language models, contributing to more sustainable AI development.\\n\\n5. Algorithmic insights: The theoretical connections established between SFT and RL could inform future developments in machine learning algorithms beyond the specific application presented.\\n\\n6. Economic effects: While the method could potentially reduce the need for some specialized ML engineers focused on complex RL implementations, it would likely create more value through broader adoption of effective fine-tuning techniques.\\n\\nPotential negative impacts are limited but could include further automation of mathematical reasoning tasks currently performed by humans, though such displacement effects would likely be gradual and limited to narrow domains initially.\"\n },\n \"technical_maturity_needed\": {\n \"score\": 3,\n \"analysis\": \"The proposed DFT method is relatively close to practical implementation, requiring only incremental advances rather than fundamental breakthroughs. The core implementation is extremely simple - just one line of code change to the standard SFT loss function. The mathematical foundation is well-established, drawing on existing concepts from both supervised learning and reinforcement learning. The authors have already demonstrated the approach working across multiple model architectures (Qwen, LLaMA, DeepSeekMath) and various sizes (1.5B to 8B parameters). The primary technical developments needed are: (1) testing on a broader range of tasks beyond mathematical reasoning, (2) scaling to larger models (13B+), (3) validating on multimodal tasks, and (4) further analysis of when and why the method might underperform. None of these require fundamental breakthroughs, but rather systematic experimentation and engineering refinements. The authors already promise to release their code, further reducing implementation barriers. The simplicity of the approach makes it immediately applicable for practitioners with standard ML expertise.\"\n },\n \"three_year_feasibility\": {\n \"probability_pct\": 90,\n \"analysis\": \"The probability of full automation of this research within three years is very high (90%). Several factors support this assessment:\\n\\n1. Implementation simplicity: The core DFT method requires just one line of code change to standard SFT, making technical implementation straightforward.\\n\\n2. Mathematical foundation: The theoretical analysis connecting SFT and RL is now established, providing a framework future AI systems can leverage.\\n\\n3. Experimental pipeline: The entire experimental workflow - from data preparation to model training to evaluation - uses standard components that are already well-supported by existing frameworks.\\n\\n4. Limited domain expertise: While mathematical reasoning was the focus of this paper, the method itself is domain-agnostic and could be applied to various tasks without specialized knowledge.\\n\\n5. Current AI capabilities: Today's most advanced AI systems can already perform many components of this research, including implementing training procedures, running experiments, and analyzing results.\\n\\n6. Rapid progress in AI for science: The pace of advancement in AI for scientific discovery is accelerating, with systems becoming increasingly capable of identifying patterns and relationships in scientific data.\\n\\nThe main limiting factors are the initial creative insight connecting SFT and RL in this specific way, and the identification of the inverse probability weighting issue. However, with this insight now published, future AI systems could automate similar investigations across other training paradigms. Within three years, it's highly likely that AI systems will be able to propose, implement, and evaluate novel training approaches comparable to DFT.\"\n },\n \"overall_automatability\": {\n \"score\": 3,\n \"analysis\": \"The overall automatability of this research is high, though not yet complete. The paper presents a clear case where most components could be automated with current or near-future AI systems. The experimental implementation, evaluation, and analysis portions follow standard practices in machine learning research that are increasingly being automated. The mathematical derivations, while requiring some sophistication, involve manipulations that advanced reasoning systems could potentially perform. Where human contribution remains most essential is in the initial framing of the research question - specifically, the insight to view SFT through the lens of RL and identify the problematic implicit reward structure. This creative connection between different learning paradigms represents the kind of cross-domain insight that remains challenging for current AI systems. Once this insight was established, the proposed solution (DFT) follows quite naturally and could likely be discovered through systematic exploration by an AI system. The paper's experimental design, implementation, and analysis of results could largely be automated with existing technologies. Given the rapid advances in AI for scientific discovery, particularly in mathematics and computer science, it's reasonable to expect that similar research contributions could be substantially automated within the next 2-3 years, though the most creative insights may still benefit from human intuition.\"\n }\n},",
|
3 |
-
"executive_summary": "This paper introduces Dynamic Fine-Tuning (DFT), a simple yet effective improvement to Supervised Fine-Tuning (SFT) for large language models that significantly enhances generalization capabilities. The authors provide a mathematical analysis revealing that standard SFT implicitly encodes a problematic reward structure inversely proportional to the model's confidence, leading to unstable optimization and poor generalization. Their solution—multiplying the SFT loss by the token probability—requires just one line of code change yet substantially outperforms standard SFT across multiple mathematical reasoning benchmarks and model architectures. The approach bridges supervised and reinforcement learning paradigms, offering the generalization benefits of RL without its complexity. This work represents a notable advance in fine-tuning methodology with immediate practical applications, combining theoretical insight with empirical validation. The research is highly automatable in most aspects, though the key theoretical insight connecting SFT and RL required human creativity that remains challenging for current AI systems.",
|
4 |
-
"limitations_uncertainties": [
|
5 |
-
"The evaluation is limited to mathematical reasoning tasks and hasn't been validated on other domains like code generation or general question answering",
|
6 |
-
"Experiments are limited to models up to 7B parameters, leaving questions about scalability to larger models (13B+)",
|
7 |
-
"The approach hasn't been tested on multimodal tasks to confirm its generality across different modalities",
|
8 |
-
"Limited analysis of potential negative cases where DFT might underperform compared to standard SFT",
|
9 |
-
"The research focuses on a specific modification to the training objective without exploring potential interactions with other training hyperparameters",
|
10 |
-
"The theoretical analysis assumes certain properties of the token distributions that may not hold universally across all domains",
|
11 |
-
"Limited discussion of computational efficiency implications for very large models",
|
12 |
-
"The assessment of existing AI coverage may underestimate the creative insights needed to formulate the theoretical connection between SFT and RL"
|
13 |
-
],
|
14 |
-
"metadata": {
|
15 |
-
"assessed_at": "2025-08-08",
|
16 |
-
"model": "claude-4-sonnet",
|
17 |
-
"version": "1.0",
|
18 |
-
"paper_path": "https://huggingface.co/papers/2508.05629"
|
19 |
-
},
|
20 |
-
"recommendations": {
|
21 |
-
"for_researchers": [
|
22 |
-
"Extend DFT evaluation to non-mathematical domains such as code generation, common sense reasoning, and general question-answering tasks",
|
23 |
-
"Test the approach with larger models (13B+ parameters) to verify scalability",
|
24 |
-
"Explore the application of DFT to multimodal tasks to confirm cross-modality effectiveness",
|
25 |
-
"Conduct ablation studies on the interaction between DFT and other training hyperparameters like learning rate schedules",
|
26 |
-
"Investigate potential hybrid approaches combining DFT with selective aspects of RL methods",
|
27 |
-
"Analyze the token distribution patterns across different domains to better understand when and why DFT provides advantages"
|
28 |
-
],
|
29 |
-
"for_institutions": [
|
30 |
-
"Invest in research that bridges theoretical understanding between different learning paradigms, as such connections can yield simple yet powerful improvements",
|
31 |
-
"Support comparative studies of fine-tuning approaches that consider both performance and computational efficiency",
|
32 |
-
"Prioritize funding for research that improves the efficiency of existing methods rather than focusing exclusively on novel architectures",
|
33 |
-
"Develop standardized benchmarks for evaluating generalization capabilities across diverse tasks beyond established domains",
|
34 |
-
"Encourage interdisciplinary collaboration between ML researchers with expertise in supervised learning and reinforcement learning"
|
35 |
-
],
|
36 |
-
"for_ai_development": [
|
37 |
-
"Implement DFT as a standard option in fine-tuning frameworks and libraries for large language models",
|
38 |
-
"Develop automated systems that can explore mathematical connections between different learning objectives",
|
39 |
-
"Create tools that visualize and analyze token probability distributions during training to better understand model learning dynamics",
|
40 |
-
"Focus on improving mathematical reasoning capabilities in foundation models to enable more sophisticated theoretical analysis",
|
41 |
-
"Invest in systems that can automatically identify potential efficiency improvements in existing training methodologies",
|
42 |
-
"Develop automated experimental pipelines that can systematically evaluate novel training approaches across diverse tasks and model architectures"
|
43 |
-
]
|
44 |
-
},
|
45 |
-
"scorecard": {
|
46 |
-
"task_formalization": 3,
|
47 |
-
"data_resource_availability": 3,
|
48 |
-
"input_output_complexity": 2,
|
49 |
-
"real_world_interaction": 4,
|
50 |
-
"existing_ai_coverage": 3,
|
51 |
-
"human_originality": 3,
|
52 |
-
"safety_ethics": 3,
|
53 |
-
"technical_maturity_needed": 3,
|
54 |
-
"three_year_feasibility_pct": 90,
|
55 |
-
"overall_automatability": 3
|
56 |
-
}
|
57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
papers_cache.db → workdir/paper_agent/papers_cache.db
RENAMED
@@ -1,3 +1,3 @@
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:
|
3 |
-
size
|
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:7c1fc0b499832f97bf8288fee40f5dcf5207b0fea8b5ae970958bcc7b2e109bf
|
3 |
+
size 3219456
|