From 2543b0c23f4d28770024b0fa624fb2cef71e58db Mon Sep 17 00:00:00 2001 From: yeokwanggoo-stack Date: Fri, 21 Aug 2026 00:25:24 +0900 Subject: [PATCH] feat: integrate Google Scholar alerts and improve paper filtering pipeline - Add Google Scholar Alert collection via Gmail IMAP - Add Gmail App Password based authentication - Add Google Scholar environment variables to NAS compose configuration - Enable Google Scholar Alert source in config.nas.yaml - Disable subject filtering that incorrectly excluded Scholar alert emails - Parse paper titles and links from Google Scholar alert HTML emails - Deduplicate collected Scholar papers by normalized title - Filter Scholar UI/control links such as update alert and unsubscribe entries - Filter bracket-only alert labels such as [automotive radar] - Add Google Scholar specific relevance threshold - Keep global min_relevance at 2 - Set Google Scholar minimum relevance to 3 - Reduce Scholar candidates from 229 collected / 207 merged to 28 accepted - Add Gemini API rate limiting - Enforce minimum 13 second interval between Gemini requests - Apply shared rate limiter to abstract and full-text enrichment - Prevent Gemini free-tier 5 RPM quota errors - Verify retry processing of previously failed AI enrichment jobs - Verify end-to-end NAS workflow - Google Scholar Alert collection successful - Gemini enrichment successful without HTTP 429 errors - Joplin report generation and WebDAV synchronization successful --- config.nas.yaml | 11 +- docker-compose.nas.yml | 4 +- paper-monitor-todo.md | 193 ++++++++++++++---- src/paper_monitor/ai_enrich.py | 20 ++ src/paper_monitor/collectors/scholar_gmail.py | 28 ++- src/paper_monitor/oa_resolver.py | 6 +- src/paper_monitor/runner.py | 15 +- state/provider_circuit_breaker_state.json | 7 + 8 files changed, 233 insertions(+), 51 deletions(-) diff --git a/config.nas.yaml b/config.nas.yaml index ce1ab3e..a21e38d 100644 --- a/config.nas.yaml +++ b/config.nas.yaml @@ -3,6 +3,7 @@ app: lookback_days: 14 max_papers_per_source_per_query: 20 min_relevance: 2 + google_scholar_min_relevance: 3 output_dir: ./data/outbox database_path: ./data/papers.db log_level: INFO @@ -55,18 +56,18 @@ sources: base_url: https://api.semanticscholar.org/graph/v1/paper/search crossref: - enabled: true + enabled: true mailto_env: CROSSREF_MAILTO base_url: https://api.crossref.org/works google_scholar_alert: - enabled: false - gmail_address_env: GMAIL_ADDRESS - gmail_app_password_env: GMAIL_APP_PASSWORD + enabled: true + gmail_address_env: GOOGLE_SCHOLAR_GMAIL_ADDRESS + gmail_app_password_env: GOOGLE_SCHOLAR_GMAIL_APP_PASSWORD imap_host: imap.gmail.com mailbox: INBOX sender_contains: scholaralerts-noreply - subject_contains: Google Scholar + subject_contains: "" ai: enabled: true diff --git a/docker-compose.nas.yml b/docker-compose.nas.yml index 166df34..3a46963 100644 --- a/docker-compose.nas.yml +++ b/docker-compose.nas.yml @@ -18,8 +18,8 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY} GROQ_API_KEY: ${GROQ_API_KEY} - GMAIL_ADDRESS: ${GMAIL_ADDRESS:-} - GMAIL_APP_PASSWORD: ${GMAIL_APP_PASSWORD:-} + GOOGLE_SCHOLAR_GMAIL_ADDRESS: ${GOOGLE_SCHOLAR_GMAIL_ADDRESS} + GOOGLE_SCHOLAR_GMAIL_APP_PASSWORD: ${GOOGLE_SCHOLAR_GMAIL_APP_PASSWORD} # Existing Joplin WebDAV sync target JOPLIN_WEBDAV_URL: ${JOPLIN_WEBDAV_URL} diff --git a/paper-monitor-todo.md b/paper-monitor-todo.md index a2ff617..f39ab4c 100644 --- a/paper-monitor-todo.md +++ b/paper-monitor-todo.md @@ -1,11 +1,43 @@ # Paper Monitor 프로젝트 - 남은 작업 및 검토 TODO -최종 업데이트: 2026-08-15 +최종 업데이트: 2026-08-21 이 문서는 지금까지 구축한 Synology NAS 기반 논문 자동 모니터링 시스템에서 **아직 미정이거나 수행하지 못한 항목**, 그리고 **운영 전에 변경 또는 추가 검토가 필요한 항목**을 정리한 TODO 목록이다. --- +## P0 - 다음 즉시 처리 + +### [ ] 최종 보고서 필터링 수정 +현재 run 결과 예시: +- collected: 229 +- merged: 207 +- accepted: 28 +- new: 0 +- report_count: 86 + +`report_count`에 Scholar 관련성 임계값 변경 이전에 삽입된 논문이 포함되어 있음. + +보고서 생성 시 현재 최종 관련성 기준을 반영하도록 수정 필요. + +### [ ] 최종 보고서 선택에 Gemini 관련성 적용 +현재 문제: +- rule-based 필터링이 Gemini 이전에 실행됨 +- rule-based에서 accepted된 논문이 이후 Gemini 관련성 1~2를 받아도 보고서에 잔류함 +- 자동차 레이더 보고서에 무관한 일반 MIMO/C-band/5G 안테나 논문이 포함됨 + +정책 정의 필요 (예): +- Gemini 분석 완료: AI 관련성 >= 3인 논문만 포함 +- AI 분석 미수행/불가: rule-based 관련성으로 fallback + +### [ ] Unpaywall HTTP 404 처리 수정 +- 현재 404 응답이 traceback 예외로 처리됨 +- `not_found`로 처리해야 함 (정상 케이스) +- `exc.response is not None` 조건으로 수정 +- 누락된 DOI 레코드에 대한 불필요한 traceback 없이 파이프라인 계속 실행 + +--- + ## 1. 즉시 처리 권장 ### [ ] Semantic Scholar 차단 해제 후 재활성화 @@ -101,10 +133,52 @@ sources: - 불필요한 중복 감소 - IEEE와 Semantic Scholar 차단 가능성 감소 +### [ ] Google Scholar 관련성 임계값 검토 +수회 스케줄 실행 후 확인 필요. + +현재 설정: +- Scholar 관련성 임계값: 3 +- 전역 임계값: 2 + +확인: +- 관련 자동차 레이더 논문이 임계값 3으로 인해 누락되는지 여부 +- 누락이 발견되면 임계값 2로 낮출지 검토 + +### [ ] Scholar 파서 개선 (필요 시) +현재 collector가 여전히 광범위한 알림 결과를 수집함. + +권장 접근: +- 알림/쿼리 설정 개선 및 하위 관련성 점수 개선을 우선 시도 +- 공격적인 제목 키워드 필터링보다 쿼리 수준에서 범위 조정 +- 새로운 비논문 항목이 나타나면 UI 링크 제외 목록 추가 + +### [ ] `classify_and_score()` 점수 규칙 검토 +다음 항목에 대해 자동차 레이더 관련성이 명확하지 않으면 낮은 관련성 점수 부여: +- 일반 MIMO +- 일반 안테나 +- 통신 전용 ISAC +- C/X/sub-6 GHz 논문 +- 비레이더 DOA 논문 + --- ## 4. Gemini AI 운영 설정 +### [x] Gemini 13초 요청 간격 추가 +무료 5 RPM 한도 보호를 위해 요청 간 13초 대기 적용 완료. + +### [x] Gemini 무료 5 RPM 한도에서 429 오류 없음 확인 +13초 간격 적용 후 실제 운영에서 429 발생 없음 확인. + +### [x] 이전 실패 Gemini 논문 재시도 구현 +`ai_status=failed`인 논문을 다음 run에서 자동 재시도하는 로직 구현 완료. + +### [ ] Gemini 429 RetryInfo/backoff 명시적 처리 +13초 간격이 주 보호 수단. +그러나 임시 quota/rate-limit 응답에 대한 방어 처리 추가 검토: +- `RetryInfo` 헤더를 파싱하여 대기 시간 준수 +- 429 발생 시 지수 백오프 fallback + ### [ ] Gemini 무료 quota 실제 사용량 모니터링 확인 항목: - full-text 요청 수 @@ -192,6 +266,16 @@ Groq는 인증 정상화 후 `pyproject.toml`에 추가. Semantic Scholar가 다시 활성화된 뒤 실제 논문에서 `pdf_url`이 정상 채워지는지 확인 필요. +### [ ] Unpaywall HTTP 404 처리 수정 +현재 상태: +- 404 응답이 traceback 예외로 처리됨 +- DOI가 없는 정상 케이스인데도 불필요한 오류 로그 발생 + +수정 사항: +- `exc.response is not None` 조건으로 404 여부 판별 +- 404는 `not_found`로 처리, 파이프라인 중단 없이 계속 진행 +- traceback 없이 조용히 처리 + ### [ ] Unpaywall 실제 신규 논문 경로 검증 standalone 테스트는 성공했지만 실제 신규 논문 run에서 다음 로그를 아직 충분히 확인하지 못함: @@ -363,28 +447,35 @@ OA PDF --- -## 10. Source 추가 +## 10. Source 추가 / Google Scholar 운영 -### [ ] Google Scholar Alert 연동 -아직 보류 중. +### [x] Google Scholar Alert Gmail IMAP 연동 구현 +구현 완료: +- Google Scholar Alert -> Gmail -> IMAP -> ScholarGmailCollector -> paper-monitor 파이프라인 +- Gmail App Password 인증 동작 확인 +- Scholar UI/alert-label 필터링 추가 -계획: -```text -Google Scholar Alert --> Gmail --> IMAP --> ScholarGmailCollector --> paper-monitor -``` +### [ ] Gmail IMAP 수집 최적화 +현재 구현이 최근 메시지를 가져온 후 필터링. -웹 scraping 대신 Google Scholar Alert 메일을 사용하는 방식 권장. +개선 검토: +- 서버 측 `SINCE` + `FROM` 필터링으로 IMAP 트래픽 및 실행 시간 감소 -필요: -- Google Scholar Alert 생성 -- Gmail App Password -- NAS `.env` -- Gmail collector 활성화 -- sender/subject filter 테스트 +### [ ] 알림 쿼리 품질 평가 +- 관련 없는 MIMO/DOA/안테나 논문을 과다 생성하는 Scholar 알림 쿼리 검토 +- 중요 자동차 레이더 논문을 놓치지 않는 선에서 알림 범위 축소 + +### [ ] Semantic Scholar 연결/접근 안정화 후 재활성화 +(기존 섹션 1 참조) + +### [ ] IEEE 웹사이트/API 접근 확인 후 재활성화 +(기존 섹션 1 참조) + +### [ ] provider circuit breaker 지속 운용 +현재 circuit breaker 정상 동작 중. 계속 유지. + +### [ ] Crossref 간헐적 네트워크 타임아웃 모니터링 +간헐적 타임아웃 발생 여부 실행 로그에서 주기적 확인. ### [ ] IEEE 복구 후 source 우선순위 재정의 장기 목표 예: @@ -511,30 +602,44 @@ lookback: 14일 # 우선순위 요약 -## P0 - 운영 전에 확인 -- [ ] Semantic Scholar `enabled: false` -- [ ] IEEE `enabled: false` -- [ ] `.env` secret 권한 및 IEEE key rotation 여부 확인 +## P0 - 다음 즉시 처리 +- [ ] 최종 보고서 필터링 수정 (report_count에 이전 기준 논문 포함 문제) +- [ ] 최종 보고서 선택에 Gemini 관련성 적용 (관련성 1~2 논문 제외) +- [ ] Unpaywall HTTP 404 처리 수정 (traceback 대신 not_found로 처리) -## P1 - 접근 복구 후 -- [ ] Semantic Scholar 재활성화 + 429 상태 확인 -- [ ] IEEE 웹사이트 403 해제 확인 -- [ ] IEEE API 승인 상태 확인 -- [ ] IEEE 1-query 테스트 후 재활성화 +## P1 - 필터링 품질 +- [ ] 수회 스케줄 실행 후 Google Scholar 관련성 임계값 검토 (현재: 3, 전역: 2) +- [ ] Scholar 파서 개선 (필요 시) +- [ ] `classify_and_score()` 점수 규칙 검토 (일반 MIMO/안테나/통신 전용 논문 처리) -## P2 - 기능 보강 -- [ ] Google Scholar Alert + Gmail collector -- [ ] Groq 인증 해결 + Gemini fallback -- [ ] `skipped` 재평가 정책 -- [ ] AI retry count / provider / model / processed_at DB 저장 -- [ ] PDF section-aware extraction +## P1 - AI +- [ ] Gemini 429 RetryInfo/backoff 명시적 처리 (13초 간격이 주 보호, 방어 처리 추가) +- [ ] Gemini AFC 경고 정리 (현재 동작에 문제 없음, 낮은 우선순위) +- [ ] Gemini -> Groq fallback (인증 문제 미해결, 파이프라인 차단하지 않음) -## P3 - 장기 운영 -- [ ] DB backup -- [ ] scheduler timestamp 로그 보관기간/정리 정책(예: 8주) -- [ ] dependency version pinning +## P1 - 소스 +- [ ] Semantic Scholar 연결/접근 안정화 후 재활성화 +- [ ] IEEE 웹사이트/API 접근 확인 후 재활성화 +- [ ] provider circuit breaker 지속 운용 +- [ ] Crossref 간헐적 네트워크 타임아웃 모니터링 + +## P2 - Google Scholar +- [ ] Gmail IMAP 수집 최적화 (서버 측 SINCE + FROM 필터링 검토) +- [ ] 알림 쿼리 품질 평가 (관련 없는 MIMO/DOA/안테나 논문 과다 생성 쿼리 검토) + +## P2 - 데이터베이스 / 보고서 +- [ ] 수집 관련성과 최종 AI 관련성을 DB에서 명확히 분리 +- [ ] 관련성 점수 출처 저장/보고 (rule-based / Gemini abstract / Gemini full-text) +- [ ] `ai_status=skipped` 처리 정책 결정 +- [ ] 기준 변경 시 같은 날 기존 논문을 재생성 보고서에서 제거할지 결정 +- [ ] DB backup/보존 정책 추가 +- [ ] scheduler.log 로그 로테이션 추가 + +## P3 - 운영 +- [ ] Docker Hub / Crossref / IEEE / Semantic Scholar 연결 문제 모니터링 +- [ ] 파이프라인 안정화 후 주요 Python dependency 버전 고정 - [ ] query/source 최적화 -- [ ] Joplin report layout 개선 +- [ ] Joplin 보고서 레이아웃 개선 --- @@ -554,3 +659,13 @@ scheduler-YYYY-MM-DD_HH-MM-SS.log ## Gemini 운영값 - [x] `max_papers_per_run` 운영값 확정: `20` + +## Google Scholar Alert 연동 (2026-08-21) +- [x] Google Scholar Alert Gmail IMAP 연동 구현 +- [x] Gmail App Password 인증 동작 확인 +- [x] Scholar UI/alert-label 필터링 추가 + +## Gemini AI 개선 (2026-08-21) +- [x] Gemini 13초 요청 간격 추가 (무료 5 RPM 한도 보호) +- [x] Gemini 무료 5 RPM 한도에서 429 오류 없음 확인 +- [x] 이전 실패(`ai_status=failed`) Gemini 논문 재시도 구현 diff --git a/src/paper_monitor/ai_enrich.py b/src/paper_monitor/ai_enrich.py index 95ef5fc..cca8f2a 100644 --- a/src/paper_monitor/ai_enrich.py +++ b/src/paper_monitor/ai_enrich.py @@ -1,5 +1,21 @@ import json +import time +_GEMINI_MIN_INTERVAL = 13.0 +_GEMINI_LAST_CALL = 0.0 + + +def _wait_for_gemini_rate_limit(): + global _GEMINI_LAST_CALL + + now = time.monotonic() + elapsed = now - _GEMINI_LAST_CALL + wait = _GEMINI_MIN_INTERVAL - elapsed + + if wait > 0: + time.sleep(wait) + + _GEMINI_LAST_CALL = time.monotonic() SYSTEM = '''You are a technical literature triage assistant for an automotive radar engineer. @@ -111,6 +127,8 @@ def enrich_with_gemini(paper, api_key, model): + json.dumps(payload, ensure_ascii=False) ) + _wait_for_gemini_rate_limit() + response = client.models.generate_content( model=model, contents=prompt, @@ -172,6 +190,8 @@ contribution, and relevance to automotive radar when supported by the text. + json.dumps(payload, ensure_ascii=False) ) + _wait_for_gemini_rate_limit() + response = client.models.generate_content( model=model, contents=prompt, diff --git a/src/paper_monitor/collectors/scholar_gmail.py b/src/paper_monitor/collectors/scholar_gmail.py index 62e091e..d2163d7 100644 --- a/src/paper_monitor/collectors/scholar_gmail.py +++ b/src/paper_monitor/collectors/scholar_gmail.py @@ -49,9 +49,31 @@ class ScholarGmailCollector: if html_body: soup=BeautifulSoup(html_body,'html.parser') for a in soup.find_all('a',href=True): - title=clean_text(a.get_text(' ',strip=True)); href=a['href'] - if len(title)<15: continue - if any(x in title.lower() for x in ['create alert','cancel alert','view all','unsubscribe']): continue + title = clean_text(a.get_text(' ', strip=True)) + href = a['href'] + + if len(title) < 15: + continue + + if any( + x in title.lower() + for x in [ + 'create alert', + 'cancel alert', + 'view all', + 'unsubscribe', + 'update alert', + 'receive fewer', + 'more relevant results', + 'manage alert', + ] + ): + continue + + # Skip Google Scholar alert/query labels such as: + # [automotive radar] + if re.fullmatch(r'\[[^\]]+\]', title): + continue context=clean_text(a.parent.get_text(' ',strip=True) if a.parent else title) doi_match=DOI_RE.search(context+' '+href) papers.append(Paper(title=title,abstract=context if context!=title else '',doi=normalize_doi(doi_match.group(0) if doi_match else ''),url=href,publication_date=date,source='Google Scholar Alert')) diff --git a/src/paper_monitor/oa_resolver.py b/src/paper_monitor/oa_resolver.py index 9929afb..6015efe 100644 --- a/src/paper_monitor/oa_resolver.py +++ b/src/paper_monitor/oa_resolver.py @@ -34,7 +34,11 @@ class UnpaywallResolver: response.raise_for_status() except requests.exceptions.HTTPError as exc: - status = exc.response.status_code if exc.response else None + status = ( + exc.response.status_code + if exc.response is not None + else None + ) # DOI not present in Unpaywall is not fatal. if status == 404: diff --git a/src/paper_monitor/runner.py b/src/paper_monitor/runner.py index c1c22c2..93e26ef 100644 --- a/src/paper_monitor/runner.py +++ b/src/paper_monitor/runner.py @@ -259,7 +259,20 @@ def run(config_path, dry_run=False): merged[k] = merge_paper(merged[k], p) if k in merged else p papers = [classify_and_score(p, search_cfg) for p in merged.values()] - papers = [p for p in papers if p.relevance >= int(app.get('min_relevance', 1))] + + min_rel = int(app.get("min_relevance", 1)) + scholar_min_rel = int( + app.get("google_scholar_min_relevance", min_rel) + ) + + papers = [ + p for p in papers + if p.relevance >= ( + scholar_min_rel + if p.source == "Google Scholar Alert" + else min_rel + ) + ] db_path = resolve_path(cfg, app.get('database_path', './data/papers.db')) db = PaperDB(db_path) diff --git a/state/provider_circuit_breaker_state.json b/state/provider_circuit_breaker_state.json index ba018c7..e367433 100644 --- a/state/provider_circuit_breaker_state.json +++ b/state/provider_circuit_breaker_state.json @@ -5,5 +5,12 @@ "strikes": { "http_403": 1 } + }, + "crossref": { + "open_until": 1787231271.0746372, + "reason": "network_error", + "strikes": { + "network_error": 1 + } } } \ No newline at end of file -- 2.54.0