Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions alembic/versions/b8949439e623_add_userid_and_ip_to_paper_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add userid and ip to paper_review

Revision ID: b8949439e623
Revises: abc123456789
Create Date: 2025-08-20 16:57:40.605076

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'b8949439e623'
down_revision: Union[str, None] = 'abc123456789'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('paper_review', sa.Column('userid', sa.String(length=128), nullable=True))
op.add_column('paper_review', sa.Column('ip', sa.String(length=45), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('paper_review', 'ip')
op.drop_column('paper_review', 'userid')
# ### end Alembic commands ###
75 changes: 60 additions & 15 deletions app/api/agent_review.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import traceback
from datetime import datetime
from typing import Optional, List

from fastapi import APIRouter, Depends, HTTPException, Request

from app.crud import create_paper_review, get_reviews
from app.crud import create_paper_review, get_reviews, check_if_exist
from app.database import get_db
from app.schemas import SubmitReviewIn, Review, SubmitReviewOut, GetReviewOut, GetReviewIn
from app.constants import AgentType, DocType, ResponseCode
from app.constants import AgentType, DocType, ResponseCode, ReviewerConst
from sqlalchemy.orm import Session
from app.config import settings
import logging
from datetime import datetime, timedelta, timezone

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,27 +47,37 @@ async def submit_review(
# Avoid failing the request due to logging issues
pass

# Save a place for check if the paper is exist
# rec = check_if_exist(
# db=db, aixiv_id=review.aixiv_id, version=review.version, doc_type=review.doc_type
# )
# if rec is None:
# raise HTTPException(
# status_code=400,
# detail=f"Submission with aixiv_id={review.aixiv_id} and version={review.version} does not exist"
# )
if settings.paper_exist_check:
rec = check_if_exist(
db=db, aixiv_id=review.aixiv_id, version=review.version, doc_type=review.doc_type
)
if rec is None:
raise HTTPException(
status_code=400,
detail=f"Review submission with aixiv_id={review.aixiv_id} and version={review.version} and doc_type={review.doc_type} does not exist"
)

agent_type_val, doc_type_val = _resolve_agent_and_doc(
reviewer=review.reviewer,
doc_type=review.doc_type,
token=review.token,
)

if settings.ip_limit_window_size > 0:
start_time = datetime.now(timezone.utc) - timedelta(hours=settings.ip_limit_window_size)
rec = get_reviews(db, review.aixiv_id, start_time, datetime.now(timezone.utc), review.version, client_ip, doc_type_val)
if len(rec) > settings.ip_limit_frequency:
raise HTTPException(
status_code=429,
detail=f"Review submission with aixiv_id={review.aixiv_id} and version={review.version} and doc_type={review.doc_type} with ip={client_ip} has submitted too frequently, plz wait for {settings.ip_limit_window_size} hour to retry."
)

rec = create_paper_review(
db=db,
payload=review,
agent_type=agent_type_val,
doc_type=doc_type_val
doc_type=doc_type_val,
ip=client_ip
)

return SubmitReviewOut(
Expand All @@ -74,10 +86,24 @@ async def submit_review(
version=rec.version,
id=rec.id
)

except HTTPException:
raise

except Exception as e:
logger.error({
"event": "submit-review:error",
"aixiv_id": review.aixiv_id,
"version": review.version,
"doc_type": review.doc_type,
"reviewer": review.reviewer,
"error_message": str(e),
"traceback": traceback.format_exc(),
})

raise HTTPException(
status_code = ResponseCode.INTERNAL_ERROR,
detail=f"submit failed: {str(e)}"
status_code=ResponseCode.INTERNAL_ERROR,
detail="submit failed: internal server error"
)


Expand All @@ -103,11 +129,30 @@ async def get_review(
pass

reviews = get_reviews(db, query.aixiv_id, query.start_date, query.end_date, query.version)

reviews_list = [Review(
aixiv_id=r.aixiv_id,
version=r.version,
review_results=r.review_results,
create_time=r.create_time,
reviewer=ReviewerConst.REVIEWERS_TYPE_MAP.get(r.agent_type, ReviewerConst.UNKNOWN_REVIEWER),
) for r in reviews]

return GetReviewOut(
review_list=reviews,
review_list=reviews_list,
code=ResponseCode.SUCCESS
)
except Exception as e:
logger.info({
"event": "get-review:request",
"aixiv_id": query.aixiv_id,
"version": query.version,
"start_date": query.start_date.isoformat() if query.start_date else None,
"end_date": query.end_date.isoformat() if query.end_date else None,
"error_message": str(e),
"traceback": traceback.format_exc(),
})

raise HTTPException(
status_code = ResponseCode.INTERNAL_ERROR,
detail=f"query failed: {str(e)}"
Expand Down
4 changes: 4 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class Settings(BaseSettings):
secret_key: str = os.getenv("SECRET_KEY", "your-secret-key-here")
debug: bool = os.getenv("DEBUG", "True").lower() == "true"
auth_token: str = os.getenv("AUTH_TOKEN", "your-auth-token-here")
paper_exist_check: bool = os.getenv("PAPER_EXIST_CHECK", False)
ip_limit_window_size: int = os.getenv("IP_LIMIT_WINDOWSiZE", 0)
ip_limit_frequency: int = os.getenv("IP_LIMIT_FREQUENCY", 3)


# CORS Configuration - handle both env var and default
@property
Expand Down
45 changes: 22 additions & 23 deletions app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,16 @@ def create_paper_review(
db: Session,
payload: SubmitReviewIn,
agent_type: int = AgentType.agent.value,
doc_type: int = DocType.paper.value
doc_type: int = DocType.paper.value,
ip: Optional[str] = None
) -> PaperReview:
rec = PaperReview(
aixiv_id = payload.aixiv_id,
version = payload.version,
review_results = payload.review_results,
agent_type = agent_type,
doc_type = doc_type
doc_type = doc_type,
ip = ip
)
db.add(rec)
db.commit()
Expand All @@ -209,34 +211,31 @@ def get_reviews(
aixiv_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
version: Optional[str] = None
) -> list[Review]:
version: Optional[str] = None,
ip: Optional[str] = None,
doc_type: Optional[int] = None
) -> list[type[PaperReview]]:
query = db.query(PaperReview).filter(PaperReview.aixiv_id == aixiv_id)
if start_date:
query = query.filter(PaperReview.create_time >= start_date)
if end_date:
query = query.filter(PaperReview.create_time <= end_date)
if version is not None:
if version:
query = query.filter(PaperReview.version == version)
if ip:
query = query.filter(PaperReview.ip == ip)
if doc_type:
query = query.filter(PaperReview.doc_type == doc_type)

reviews = query.all()

reviews_list = [Review(
aixiv_id=r.aixiv_id,
version=r.version,
review_results=r.review_results,
create_time=r.create_time,
reviewer=ReviewerConst.REVIEWERS_TYPE_MAP.get(r.agent_type, ReviewerConst.UNKNOWN_REVIEWER),
) for r in reviews]

return reviews_list
return reviews

# Check if th paper is exitst
# def check_if_exist(db: Session, aixiv_id: str, version: str, doc_type: str) -> Optional[Submission]:
# record = (
# db.query(Submission)
# .filter(Submission.aixiv_id == aixiv_id, Submission.version == version, Submission.category == doc_type)
# .first()
# )
#
# return record
def check_if_exist(db: Session, aixiv_id: str, version: str, doc_type: str) -> Optional[Submission]:
record = (
db.query(Submission)
.filter(Submission.aixiv_id == aixiv_id, Submission.version == version, Submission.doc_type == doc_type)
.first()
)

return record
2 changes: 2 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class PaperReview(Base):
TIMESTAMP, nullable=False, server_default=func.now()
)
like_count = Column(Integer, nullable=False, server_default=text("0"))
userid = Column(String(128), nullable=True)
ip = Column(String(45), nullable=True)

__table_args__ = (
Index("idx_paper_review_aixiv_id_create_time", "aixiv_id", "create_time"),
Expand Down
12 changes: 12 additions & 0 deletions app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ def validate_aixiv_id(cls, v: str):

return v

@field_validator("version")
def validate_version(cls, v: str):
pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$"
if not re.match(pattern, v):
raise ValueError("version must be in the format 'X.Y' or 'X.Y.Z', e.g. 1.0, 2.1, 1.9.3")
return v

class SubmitReviewOut(BaseModel):
code: int
Expand Down Expand Up @@ -184,6 +190,12 @@ def lowercase_fields(cls, v):
return v.lower()
return v

@field_validator("version")
def validate_version(cls, v: str):
pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$"
if not re.match(pattern, v):
raise ValueError("version must be in the format 'X.Y' or 'X.Y.Z', e.g. 1.0, 2.1, 1.9.3")
return v

class GetReviewOut(BaseModel):
review_list: List[Review]
Expand Down
5 changes: 4 additions & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ AWS_S3_BUCKET=aixiv-papers
SECRET_KEY=your_secret_key_here
DEBUG=True
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
AUTH_TOKEN=your-auth-token-here
AUTH_TOKEN=your-auth-token-here
IP_LIMIT_WINDOW_SIZE=0 #for prevent IP frequently submit reviews, 0 for turn the lock off, 1 for 1 hour etc.
IP_LIMIT_FREQUENCY=0 #for prevent IP frequently submit reviews, means for each IP_LIMIT_WINDOWSiZE limit, accept IP_LIMIT_FREQUENCY reviews.
PAPER_EXIST_CHECK=True #for check the target paper is existed or not in the submissions table