From 224c406b8140f281cb07d1d9b80a627c6a4d7a06 Mon Sep 17 00:00:00 2001 From: Smilotte Date: Tue, 19 Aug 2025 16:35:59 +0800 Subject: [PATCH 1/4] update the check if existed method --- app/api/agent_review.py | 49 +++++++++++++++++++++++++++++++---------- app/crud.py | 16 +++++++------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/api/agent_review.py b/app/api/agent_review.py index 89499c0..9e0dafb 100644 --- a/app/api/agent_review.py +++ b/app/api/agent_review.py @@ -1,9 +1,10 @@ +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 @@ -45,15 +46,15 @@ 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" - # ) + # Save a place for check if the paper is existed + 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} and doc_type={review.doc_type} does not exist" + ) agent_type_val, doc_type_val = _resolve_agent_and_doc( reviewer=review.reviewer, @@ -74,10 +75,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" ) @@ -108,6 +123,16 @@ async def get_review( 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)}" diff --git a/app/crud.py b/app/crud.py index 7d505e8..2085b02 100644 --- a/app/crud.py +++ b/app/crud.py @@ -232,11 +232,11 @@ def get_reviews( return reviews_list # 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 From 08a9431bd9add60de6d9fc5c791ff8485842a87e Mon Sep 17 00:00:00 2001 From: Smilotte Date: Tue, 19 Aug 2025 16:52:53 +0800 Subject: [PATCH 2/4] update validate for version --- app/schemas.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/schemas.py b/app/schemas.py index 9fedd46..080e770 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -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 @@ -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] From da223f6217f0f237ffac0478d378019fd7989c71 Mon Sep 17 00:00:00 2001 From: Smilotte Date: Wed, 20 Aug 2025 18:01:21 +0800 Subject: [PATCH 3/4] update some ip limitation check --- ...9e623_add_userid_and_ip_to_paper_review.py | 32 +++++++++++++++++++ app/api/agent_review.py | 29 ++++++++++++++--- app/config.py | 2 ++ app/crud.py | 29 ++++++++--------- app/models.py | 2 ++ env.example | 4 ++- 6 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 alembic/versions/b8949439e623_add_userid_and_ip_to_paper_review.py diff --git a/alembic/versions/b8949439e623_add_userid_and_ip_to_paper_review.py b/alembic/versions/b8949439e623_add_userid_and_ip_to_paper_review.py new file mode 100644 index 0000000..f31d749 --- /dev/null +++ b/alembic/versions/b8949439e623_add_userid_and_ip_to_paper_review.py @@ -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 ### diff --git a/app/api/agent_review.py b/app/api/agent_review.py index 9e0dafb..86f7b74 100644 --- a/app/api/agent_review.py +++ b/app/api/agent_review.py @@ -7,10 +7,11 @@ 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__) @@ -46,14 +47,13 @@ async def submit_review( # Avoid failing the request due to logging issues pass - # Save a place for check if the paper is existed 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} and doc_type={review.doc_type} does not exist" + 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( @@ -62,11 +62,21 @@ async def submit_review( 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( @@ -118,8 +128,17 @@ 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: diff --git a/app/config.py b/app/config.py index 674aaf7..20a199f 100644 --- a/app/config.py +++ b/app/config.py @@ -24,6 +24,8 @@ 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") + ip_limit_window_size: int = os.getenv("IP_LIMIT_WINDOWSiZE", 1) + ip_limit_frequency: int = os.getenv("IP_LIMIT_FREQUENCY", 3) # CORS Configuration - handle both env var and default @property diff --git a/app/crud.py b/app/crud.py index 2085b02..66d8011 100644 --- a/app/crud.py +++ b/app/crud.py @@ -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() @@ -209,27 +211,24 @@ 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]: diff --git a/app/models.py b/app/models.py index f954bf2..e52f9c4 100644 --- a/app/models.py +++ b/app/models.py @@ -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"), diff --git a/env.example b/env.example index 491f47e..bc5d513 100644 --- a/env.example +++ b/env.example @@ -32,4 +32,6 @@ 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 \ No newline at end of file +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. \ No newline at end of file From 0060fd4a8457bdbf9aa054ee3a44a08086f9e7d4 Mon Sep 17 00:00:00 2001 From: Smilotte Date: Thu, 21 Aug 2025 17:55:32 +0800 Subject: [PATCH 4/4] update the switch of the limitation functions --- app/api/agent_review.py | 17 +++++++++-------- app/config.py | 4 +++- env.example | 3 ++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/api/agent_review.py b/app/api/agent_review.py index 86f7b74..48cbf9b 100644 --- a/app/api/agent_review.py +++ b/app/api/agent_review.py @@ -47,14 +47,15 @@ async def submit_review( # Avoid failing the request due to logging issues pass - 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" + 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, @@ -62,7 +63,7 @@ async def submit_review( token=review.token, ) - if settings.ip_limit_window_size>0: + 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: diff --git a/app/config.py b/app/config.py index 20a199f..383df46 100644 --- a/app/config.py +++ b/app/config.py @@ -24,8 +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") - ip_limit_window_size: int = os.getenv("IP_LIMIT_WINDOWSiZE", 1) + 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 diff --git a/env.example b/env.example index bc5d513..18fcca6 100644 --- a/env.example +++ b/env.example @@ -34,4 +34,5 @@ DEBUG=True ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 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. \ No newline at end of file +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 \ No newline at end of file