Compare commits

..
6 Commits
Author SHA1 Message Date
KG-access 1aa724e90f Merge pull request 'Fix Joplin sync by upgrading CLI and detecting sync errors' (#5) from feature/joplinupdate into main
Reviewed-on: #5
2026-10-02 09:13:38 +00:00
KG-access f5186e57b5 Fix Joplin sync by upgrading CLI and detecting sync errors 2026-10-02 18:10:15 +09:00
KG-access f39a7b5a09 Merge pull request 'feat: enable IEEE Xplore collection and document validation' (#4) from feature/IEEE_active into main
Reviewed-on: #4
2026-08-24 14:48:56 +00:00
KG-access e6d6713169 feat: enable IEEE Xplore collection and document validation
- enable IEEE source in default and NAS configurations
- record API activation, rate-limit validation, and full-run results
- update NAS operational guidance and TODO status
2026-08-24 23:46:19 +09:00
KG-access b2db177554 Merge pull request 'feat: implement Groq fallback for Gemini failures and update ops docs' (#3) from feature/add_Groq into main
Reviewed-on: #3
2026-08-22 06:21:01 +00:00
KG-access f7e31aef1c feat: implement Groq fallback for Gemini failures and update ops docs
add Groq dependency and NAS config entries for fallback provider
implement Gemini -> Groq fallback flow on quota, timeout, and API errors
validate Groq API key with models listing and runtime invocation path
update AI orchestration logic in runner and ai enrichment pipeline
update TODO document to mark Groq auth, integration, and verification as completed
2026-08-22 15:19:59 +09:00
10 changed files with 538 additions and 131 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Joplin Terminal (official npm package)
RUN npm install --loglevel=error -g joplin
RUN npm install --loglevel=error -g joplin@3.7.1
WORKDIR /app
COPY pyproject.toml requirements.txt ./
+1 -1
View File
@@ -158,7 +158,7 @@ sync-only 테스트가 정상이라면 `.env`를 수정합니다.
JOPLIN_WRITE_ENABLED=true
```
현재 IEEE key가 아직 Waiting이라면 `config.nas.yaml`에서:
IEEE developer application이 Waiting 또는 접근이 차단된 경우에만 `config.nas.yaml`에서 비활성화합니다:
```yaml
sources:
+7 -1
View File
@@ -41,7 +41,7 @@ search:
sources:
ieee:
enabled: false # Set true after the IEEE developer application becomes Active.
enabled: true
api_key_env: IEEE_API_KEY
base_url: https://ieeexploreapi.ieee.org/api/v1/search/articles
@@ -71,6 +71,12 @@ ai:
model: gemini-3.6-flash
max_papers_per_run: 20
fallback:
enabled: true
provider: groq
api_key_env: GROQ_API_KEY
model: llama-3.3-70b-versatile
# NAS mode writes to Joplin through Joplin Terminal, not the desktop Data API.
joplin:
enabled: false
+1 -1
View File
@@ -43,7 +43,7 @@ search:
sources:
ieee:
enabled: false
enabled: true
api_key_env: IEEE_API_KEY
base_url: https://ieeexploreapi.ieee.org/api/v1/search/articles
semantic_scholar:
+27 -2
View File
@@ -7,6 +7,31 @@ RESULT_JSON="/app/data/last_result.json"
log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
fail() { log "ERROR: $*"; exit 1; }
joplin_sync() {
local tmp
local status
tmp="$(mktemp)"
set +e
joplin --profile "$PROFILE" sync 2>&1 | tee "$tmp"
status=${PIPESTATUS[0]}
set -e
if [[ "$status" -ne 0 ]]; then
rm -f "$tmp"
fail "Joplin sync failed with exit code $status"
fi
if grep -Eqi 'Last error:|(^|[[:space:]])Error:' "$tmp"; then
log "ERROR: Joplin reported a sync error:"
grep -Ei 'Last error:|(^|[[:space:]])Error:' "$tmp" || true
rm -f "$tmp"
exit 1
fi
rm -f "$tmp"
}
: "${JOPLIN_WEBDAV_URL:?JOPLIN_WEBDAV_URL is required}"
: "${JOPLIN_WEBDAV_USERNAME:?JOPLIN_WEBDAV_USERNAME is required}"
@@ -33,7 +58,7 @@ fi
# Pull the current remote state first. This is important when PC/mobile also use the same WebDAV target.
log "Synchronising Joplin from WebDAV before writing..."
joplin --profile "$PROFILE" sync
joplin_sync
if [[ "${JOPLIN_WRITE_ENABLED,,}" != "true" ]]; then
log "JOPLIN_WRITE_ENABLED=false: sync-only safety mode. No paper note will be written."
@@ -86,6 +111,6 @@ log "Importing report into Joplin notebook: $JOPLIN_NOTEBOOK"
joplin --profile "$PROFILE" import "$TMP_MD" "$JOPLIN_NOTEBOOK" --format md -f
log "Synchronising Joplin changes to WebDAV..."
joplin --profile "$PROFILE" sync
joplin_sync
log "Completed: $REPORT_TITLE ($REPORT_COUNT papers)"
+41 -49
View File
@@ -1,6 +1,6 @@
# Paper Monitor 프로젝트 - 남은 작업 및 검토 TODO
최종 업데이트: 2026-08-21 (IEEE 접근 차단 해제 확인)
최종 업데이트: 2026-08-24 (IEEE API 활성화 및 전체 NAS 실행 완료 반영)
이 문서는 지금까지 구축한 Synology NAS 기반 논문 자동 모니터링 시스템에서 **아직 미정이거나 수행하지 못한 항목**, 그리고 **운영 전에 변경 또는 추가 검토가 필요한 항목**을 정리한 TODO 목록이다.
@@ -52,18 +52,13 @@
- 검색 query 수 축소
- `max_retries` 추가 축소
### [x] IEEE 접근 차단 해제 여부 확인
- 2026-08-21 확인: 집 인터넷에서 `developer.ieee.org` HTTP 200 OK - 차단 해제 완료
- API 승인 상태 및 key 교체 여부는 아래 별도 항목에서 확인 필요
- 재활성화 시 먼저 1개 query만 수동 테스트 권장
- 401 / 403 / 429 / timeout / connection error 발생 시 해당 run의 IEEE 전체 중단 유지
- 동일 run에서 IEEE 자동 retry는 하지 않음
- 요청 간 최소 간격 5초 유지
### [ ] IEEE API 승인 상태 확인
- 네트워크 차단 문제와 API 승인 문제는 별개
- IEEE API key가 최종 승인되었는지 확인 후 활성화
- 웹사이트 접속 정상화와 API 승인 상태를 각각 확인
### [x] IEEE API 승인·활성화 및 전체 실행 확인 (2026-08-24)
- IEEE developer application Active 및 `IEEE_API_KEY` 인증 확인.
- `config.yaml`, `config.nas.yaml`에서 `sources.ieee.enabled: true` 적용.
- 단일 query smoke test: IEEE Xplore API HTTP 200, JSON 응답 확인.
- NAS 운영 구성 전체 실행: 8개 IEEE query에서 15편 수집, 인증·rate-limit 오류 없음.
- 요청 간 최소 5초 간격을 유지한다. 주 1회 8 calls/run 운영은 IEEE 한도 10 calls/s, 200 calls/day보다 충분히 낮다.
- 403/429/네트워크 오류는 circuit breaker cooldown으로 후속 호출을 억제한다.
---
@@ -229,31 +224,26 @@ Direct use of automatic function calling (AFC) in Models.generate_content is not
## 5. Groq fallback
### [ ] Groq API 인증 문제 해결
현재 상태:
- Groq API key를 등록했으나 401 `Invalid API Key`
- 새 key 교체 후에도 인증 문제로 일단 보류
### [x] Groq API 인증 문제 해결
완료 상태:
- Groq API key 인증 성공 확인
- `models.list()` 및 실제 호출 기반 인증 검증 완료
- Gemini 실패 시 Groq fallback 연결 및 동작 확인
향후:
- Groq Console에서 project/key 상태 재확인
- `models.list()` 인증 테스트
- 인증 성공 후 Gemini fallback provider로 연결
목표:
적용 동작:
```text
Gemini 성공
-> 완료
-> 완료
Gemini quota/timeout/API 오류
-> Groq fallback
-> Groq fallback
Groq도 실패
-> AI 없이 논문 저장
-> AI 없이 논문 저장
```
### [ ] Groq dependency는 인증 성공 후 정식 추가
현재 Gemini는 정식 dependency 추가 완료.
Groq는 인증 정상화 후 `pyproject.toml`에 추가.
### [x] Groq dependency 정식 추가
Groq 인증 정상화 이후 dependency 반영 및 실행 검증 완료.
---
@@ -277,15 +267,9 @@ Semantic Scholar가 다시 활성화된 뒤 실제 논문에서 `pdf_url`이 정
- 404는 `not_found`로 처리, 파이프라인 중단 없이 계속 진행
- traceback 없이 조용히 처리
### [ ] Unpaywall 실제 신규 논문 경로 검증
standalone 테스트는 성공했지만 실제 신규 논문 run에서 다음 로그를 아직 충분히 확인하지 못함:
```text
Unpaywall candidates: N / M new papers
Unpaywall resolved: status=... pdf=YES ...
```
신규 논문 발생 시 확인.
### [x] Unpaywall 실제 신규 논문 경로 검증 (2026-08-24)
- 전체 NAS 실행에서 신규 논문 14편 중 DOI가 있는 10편을 Unpaywall로 조회.
- 10편 모두 `status=closed`, PDF URL 없음으로 정상 처리됐고 파이프라인은 계속 진행됐다.
### [ ] PDF 다운로드 실패 유형 통계/로그 개선
현재 예외처리는 되어 있음.
@@ -469,8 +453,6 @@ OA PDF
### [ ] Semantic Scholar 연결/접근 안정화 후 재활성화
(기존 섹션 1 참조)
### [x] IEEE 웹사이트/API 접근 확인 후 재활성화
- 2026-08-21: 네트워크 차단 해제 확인. API 승인 상태 및 key 교체 후 `IEEE enabled: true`로 전환 가능
### [ ] provider circuit breaker 지속 운용
현재 circuit breaker 정상 동작 중. 계속 유지.
@@ -478,7 +460,7 @@ OA PDF
### [ ] Crossref 간헐적 네트워크 타임아웃 모니터링
간헐적 타임아웃 발생 여부 실행 로그에서 주기적 확인.
### [ ] IEEE 복구 후 source 우선순위 재정의
### [ ] IEEE 활성화 후 source 우선순위 재정의
장기 목표 예:
```text
IEEE Xplore
@@ -559,9 +541,9 @@ pyproject.toml 변경
---
# 현재 운영 권장 설정
# 현재 운영 설정
현재 접근 제한 상황을 고려하면 당분간 다음 구성이 안전하다.
IEEE API가 활성화되어 IEEE, Crossref, Google Scholar Alert, Gemini, Unpaywall을 사용하는 현재 NAS 구성을 반영한다.
```yaml
app:
@@ -572,7 +554,7 @@ app:
sources:
ieee:
enabled: false # 네트워크 차단 해제됨. API 승인 및 key 교체 확인 후 true로 전환
enabled: true
semantic_scholar:
enabled: false
@@ -597,7 +579,7 @@ oa:
lookback: 14일
```
IEEE 네트워크 차단은 해제되었으나 API 승인 상태 및 key 교체 확인 전까지 `enabled: false` 유지. 현재 Semantic Scholar도 비활성화 상태이므로 당분간 Crossref 중심으로 수집되고, DOI가 있는 신규 논문에 대해 Unpaywall OA 검색 및 Gemini 분석이 수행된다.
IEEE API는 Active 상태이며 `enabled: true`로 운영한다. 주 1회 실행에서 IEEE 8개 query를 요청하고, 요청 간 최소 5초 간격을 유지한다. Semantic Scholar는 계속 비활성화 상태이며, Crossref·Google Scholar Alert 수집 결과에 대해 Unpaywall OA 검색 및 Gemini 분석이 수행된다.
---
@@ -616,12 +598,11 @@ IEEE 네트워크 차단은 해제되었으나 API 승인 상태 및 key 교체
## P1 - AI
- [ ] Gemini 429 RetryInfo/backoff 명시적 처리 (13초 간격이 주 보호, 방어 처리 추가)
- [ ] Gemini AFC 경고 정리 (현재 동작에 문제 없음, 낮은 우선순위)
- [ ] Gemini -> Groq fallback (인증 문제 미해결, 파이프라인 차단하지 않음)
- [x] Gemini -> Groq fallback 구현/검증 완료 (Gemini 실패 시 Groq로 자동 전환)
## P1 - 소스
- [ ] Semantic Scholar 연결/접근 안정화 후 재활성화
- [x] IEEE 네트워크 차단 해제 확인 (2026-08-21)
- [ ] IEEE API 승인 상태 확인 및 활성화
- [x] IEEE API 승인 완료·활성화·전체 NAS 실행 확인 (2026-08-24, 8 queries / 15 papers)
- [ ] provider circuit breaker 지속 운용
- [ ] Crossref 간헐적 네트워크 타임아웃 모니터링
@@ -645,7 +626,7 @@ IEEE 네트워크 차단은 해제되었으나 API 승인 상태 및 key 교체
---
# 완료 항목 (2026-08-15 기준)
# 완료 항목
## 스케줄러 / 운영
- [x] DSM Task Scheduler 최종 등록 확인
@@ -672,6 +653,17 @@ scheduler-YYYY-MM-DD_HH-MM-SS.log
- [x] Gemini 무료 5 RPM 한도에서 429 오류 없음 확인
- [x] 이전 실패(`ai_status=failed`) Gemini 논문 재시도 구현
## Groq fallback (2026-08-22)
- [x] Groq API key 인증 성공 확인
- [x] Groq dependency 정식 반영
- [x] Gemini 실패 시 Groq fallback 동작 검증 완료
## IEEE API 활성화 (2026-08-24)
- [x] IEEE developer application Active 및 API key 인증 확인
- [x] `config.yaml`, `config.nas.yaml`에서 IEEE source 활성화
- [x] IEEE Xplore smoke test HTTP 200 JSON 응답 확인
- [x] NAS 전체 실행에서 8개 query, 15편 수집 및 rate-limit 오류 없음 확인
## IEEE 접근 (2026-08-21)
- [x] IEEE 네트워크 차단 해제 확인 (`developer.ieee.org` HTTP 200 OK)
+1
View File
@@ -14,6 +14,7 @@ dependencies = [
"openai>=1.0",
"tzdata",
"google-genai",
"groq",
"pypdf",
]
+263 -26
View File
@@ -1,6 +1,11 @@
import json
import time
# ============================================================
# Gemini rate limiter
# ============================================================
_GEMINI_MIN_INTERVAL = 13.0
_GEMINI_LAST_CALL = 0.0
@@ -17,10 +22,31 @@ def _wait_for_gemini_rate_limit():
_GEMINI_LAST_CALL = time.monotonic()
# ============================================================
# Common AI prompt
# ============================================================
SYSTEM = '''You are a technical literature triage assistant for an automotive radar engineer.
Analyze ONLY the supplied metadata.
Do not invent experimental results, methods, conclusions, or claims that are not supported by the supplied metadata.
Analyze ONLY the supplied metadata or full text.
Do not invent experimental results, methods, conclusions, or claims that are not supported by the supplied information.
The primary technical scope is radar engineering, especially:
- automotive radar
- FMCW radar
- radar waveform design
- radar MIMO
- radar DOA / angle estimation
- radar super-resolution
- radar interference
- automotive radar antenna
- imaging radar / 4D radar
- radar target modeling
- radar signal processing
- radar hardware and RF impairments
Generic communications, acoustic DOA, sonar, medical sensing, or non-radar MIMO/antenna research should receive low relevance unless there is a clear technical connection to radar.
Return:
- relevance: integer from 1 to 5
@@ -69,10 +95,14 @@ AI_RESPONSE_SCHEMA = {
"categories",
"summary",
"reason",
],
],
}
# ============================================================
# Common helpers
# ============================================================
def _make_payload(paper):
return {
"title": paper.title,
@@ -84,6 +114,18 @@ def _make_payload(paper):
}
def _make_full_text_payload(paper, full_text):
return {
"title": paper.title,
"abstract": paper.abstract,
"venue": paper.venue,
"year": paper.year,
"full_text": full_text,
"current_categories": paper.categories,
"current_relevance": paper.relevance,
}
def _apply_result(paper, data):
paper.relevance = max(
1,
@@ -110,6 +152,34 @@ def _apply_result(paper, data):
return paper
def _parse_json_text(text):
if not text:
raise RuntimeError("AI returned empty response")
text = text.strip()
# Defensive handling for models that wrap JSON in Markdown fences.
if text.startswith("```"):
lines = text.splitlines()
if lines:
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
text = "\n".join(lines).strip()
if text.lower().startswith("json"):
text = text[4:].strip()
return json.loads(text)
# ============================================================
# Gemini - metadata / abstract
# ============================================================
def enrich_with_gemini(paper, api_key, model):
if not api_key:
return paper
@@ -139,13 +209,18 @@ def enrich_with_gemini(paper, api_key, model):
),
)
data = json.loads(response.text)
data = _parse_json_text(response.text)
paper = _apply_result(paper, data)
paper.ai_analysis_level = "abstract"
return paper
# ============================================================
# Gemini - full text
# ============================================================
def enrich_with_gemini_full_text(
paper,
full_text,
@@ -163,15 +238,10 @@ def enrich_with_gemini_full_text(
client = genai.Client(api_key=api_key)
payload = {
"title": paper.title,
"abstract": paper.abstract,
"venue": paper.venue,
"year": paper.year,
"full_text": full_text,
"current_categories": paper.categories,
"current_relevance": paper.relevance,
}
payload = _make_full_text_payload(
paper,
full_text,
)
prompt = (
SYSTEM
@@ -202,14 +272,184 @@ contribution, and relevance to automotive radar when supported by the text.
),
)
data = json.loads(response.text)
data = _parse_json_text(response.text)
paper = _apply_result(paper, data)
paper.ai_analysis_level = "full_text"
return paper
def enrich_with_openai(paper, api_key, model):
# ============================================================
# Groq common prompt
# ============================================================
def _make_groq_prompt(payload, full_text=False):
if full_text:
instruction = """
You are given extracted full text from the paper.
Base the analysis primarily on the supplied full text.
Do not invent information that is not present in the supplied text.
If the extracted text appears incomplete or corrupted, be conservative.
The summary should describe the paper's main technical approach,
contribution, and relevance to automotive radar when supported by the text.
"""
else:
instruction = """
Analyze the supplied paper metadata only.
Do not infer details that are not present in the metadata.
"""
output_instruction = """
Return ONLY one valid JSON object.
The JSON object must have exactly these fields:
{
"relevance": 1,
"categories": ["Signal Processing"],
"summary": "한국어 요약",
"reason": "한국어 관련성 설명"
}
Rules:
- relevance must be an integer from 1 to 5
- categories must be a JSON array of strings
- summary must be written in Korean
- reason must be written in Korean
- do not use Markdown code fences
- do not include text before or after the JSON object
"""
return (
SYSTEM
+ instruction
+ output_instruction
+ "\n\nPaper data:\n"
+ json.dumps(payload, ensure_ascii=False)
)
# ============================================================
# Groq - metadata / abstract
# ============================================================
def enrich_with_groq(
paper,
api_key,
model,
):
if not api_key:
return paper
from groq import Groq
client = Groq(api_key=api_key)
payload = _make_payload(paper)
prompt = _make_groq_prompt(
payload,
full_text=False,
)
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": SYSTEM,
},
{
"role": "user",
"content": prompt,
},
],
temperature=0.1,
response_format={
"type": "json_object"
},
)
content = response.choices[0].message.content
data = _parse_json_text(content)
paper = _apply_result(paper, data)
paper.ai_analysis_level = "abstract"
return paper
# ============================================================
# Groq - full text
# ============================================================
def enrich_with_groq_full_text(
paper,
full_text,
api_key,
model,
):
if not api_key:
return paper
if not full_text or not full_text.strip():
return paper
from groq import Groq
client = Groq(api_key=api_key)
payload = _make_full_text_payload(
paper,
full_text,
)
prompt = _make_groq_prompt(
payload,
full_text=True,
)
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": SYSTEM,
},
{
"role": "user",
"content": prompt,
},
],
temperature=0.1,
response_format={
"type": "json_object"
},
)
content = response.choices[0].message.content
data = _parse_json_text(content)
paper = _apply_result(paper, data)
paper.ai_analysis_level = "full_text"
return paper
# ============================================================
# OpenAI - legacy / optional
# ============================================================
def enrich_with_openai(
paper,
api_key,
model,
):
if not api_key:
return paper
@@ -224,18 +464,15 @@ def enrich_with_openai(paper, api_key, model):
instructions=SYSTEM,
input=json.dumps(
payload,
ensure_ascii=False
ensure_ascii=False,
),
)
text = response.output_text.strip()
data = _parse_json_text(
response.output_text
)
if text.startswith("```"):
text = text.strip("`")
if text.lower().startswith("json"):
text = text[4:].strip()
data = json.loads(text)
return _apply_result(paper, data)
return _apply_result(
paper,
data,
)
+189 -50
View File
@@ -18,6 +18,8 @@ from .joplin import JoplinClient
from .ai_enrich import (
enrich_with_gemini,
enrich_with_gemini_full_text,
enrich_with_groq,
enrich_with_groq_full_text,
)
from .collectors.ieee import (
IEEECollector,
@@ -351,23 +353,57 @@ def run(config_path, dry_run=False):
# A. Papers discovered for the first time in this run
# B. Papers from earlier runs whose ai_status == "failed"
#
# Primary provider:
# Gemini
#
# Fallback provider:
# Groq
#
# Full-text flow:
# Gemini full-text
# -> Groq full-text
# -> Gemini abstract
# -> Groq abstract
#
# Abstract-only flow:
# Gemini abstract
# -> Groq abstract
#
# Historical papers with ai_status == "" are NOT automatically
# processed. This prevents unexpectedly processing the entire DB.
# ============================================================
ai = cfg.get('ai', {})
if ai.get('enabled'):
api_key = env_value(
gemini_api_key = env_value(
ai.get('api_key_env', 'GEMINI_API_KEY')
)
model = ai.get(
gemini_model = ai.get(
'model',
'gemini-3.6-flash',
)
fallback = ai.get('fallback', {})
groq_enabled = bool(
fallback.get('enabled', False)
) and fallback.get('provider', 'groq').lower() == 'groq'
groq_api_key = ''
groq_model = fallback.get(
'model',
'llama-3.3-70b-versatile',
)
if groq_enabled:
groq_api_key = env_value(
fallback.get(
'api_key_env',
'GROQ_API_KEY',
)
)
max_papers = int(
ai.get('max_papers_per_run', 20)
)
@@ -383,7 +419,9 @@ def run(config_path, dry_run=False):
candidate_map = {}
for p in new_candidates:
candidate_map[paper_key(p.doi, p.title)] = p
candidate_map[
paper_key(p.doi, p.title)
] = p
for p in retry_candidates:
k = paper_key(p.doi, p.title)
@@ -401,24 +439,28 @@ def run(config_path, dry_run=False):
reverse=True,
)[:max_papers]
if not api_key:
if not gemini_api_key and not groq_api_key:
log.warning(
'Gemini API key missing; AI enrichment skipped'
'No usable AI API key configured; '
'AI enrichment skipped'
)
else:
log.info(
'Gemini enrichment candidates: %d '
'(new=%d retry=%d)',
'AI enrichment candidates: %d '
'(new=%d retry=%d, gemini=%s groq=%s)',
len(ai_candidates),
len(new_candidates),
len(retry_candidates),
'YES' if gemini_api_key else 'NO',
'YES' if groq_api_key else 'NO',
)
for p in ai_candidates:
full_text_done = False
abstract_done = False
had_failure = False
full_text = ''
# -----------------------------------------
# 1. Prefer OA PDF full-text analysis
@@ -440,33 +482,89 @@ def run(config_path, dry_run=False):
p.title,
)
enrich_with_gemini_full_text(
p,
full_text,
api_key,
model,
)
full_text_done = True
p.ai_status = 'done'
log.info(
'Gemini full-text enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
except Exception:
had_failure = True
log.exception(
'Full-text AI failed; '
'PDF download/extraction failed; '
'trying abstract fallback: %s',
p.title,
)
# -------------------------------------
# 1-A. Gemini full-text
# -------------------------------------
if (
full_text
and full_text.strip()
and gemini_api_key
):
try:
enrich_with_gemini_full_text(
p,
full_text,
gemini_api_key,
gemini_model,
)
full_text_done = True
p.ai_status = 'done'
log.info(
'Gemini full-text enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
except Exception:
had_failure = True
log.exception(
'Gemini full-text enrichment failed; '
'trying Groq full-text fallback: %s',
p.title,
)
# -------------------------------------
# 1-B. Groq full-text fallback
# -------------------------------------
if (
not full_text_done
and full_text
and full_text.strip()
and groq_enabled
and groq_api_key
):
try:
enrich_with_groq_full_text(
p,
full_text,
groq_api_key,
groq_model,
)
full_text_done = True
p.ai_status = 'done'
log.info(
'Groq full-text enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
except Exception:
had_failure = True
log.exception(
'Groq full-text enrichment failed; '
'trying abstract fallback: %s',
p.title,
)
# -----------------------------------------
# 2. Abstract fallback
# -----------------------------------------
@@ -475,32 +573,71 @@ def run(config_path, dry_run=False):
and p.abstract
and p.abstract.strip()
):
try:
enrich_with_gemini(
p,
api_key,
model,
)
# -------------------------------------
# 2-A. Gemini abstract
# -------------------------------------
if gemini_api_key:
try:
enrich_with_gemini(
p,
gemini_api_key,
gemini_model,
)
abstract_done = True
p.ai_status = 'done'
abstract_done = True
p.ai_status = 'done'
log.info(
'Gemini abstract enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
log.info(
'Gemini abstract enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
except Exception:
had_failure = True
except Exception:
had_failure = True
log.exception(
'Gemini abstract enrichment failed; '
'keeping original metadata: %s',
p.title,
)
log.exception(
'Gemini abstract enrichment failed; '
'trying Groq abstract fallback: %s',
p.title,
)
# -------------------------------------
# 2-B. Groq abstract fallback
# -------------------------------------
if (
not abstract_done
and groq_enabled
and groq_api_key
):
try:
enrich_with_groq(
p,
groq_api_key,
groq_model,
)
abstract_done = True
p.ai_status = 'done'
log.info(
'Groq abstract enriched: '
'relevance=%d categories=%s title=%s',
p.relevance,
p.categories,
p.title,
)
except Exception:
had_failure = True
log.exception(
'Groq abstract enrichment failed; '
'keeping original metadata: %s',
p.title,
)
# -----------------------------------------
# 3. Final AI status
@@ -525,6 +662,8 @@ def run(config_path, dry_run=False):
k = paper_key(p.doi, p.title)
# Retry candidates are existing DB rows, so persist
# their updated AI status immediately.
if k not in new_keys:
db.upsert(
p,
@@ -589,4 +728,4 @@ def cli():
p.add_argument('--config', default='config.yaml')
p.add_argument('--dry-run', action='store_true')
a = p.parse_args()
run(a.config, a.dry_run)
run(a.config, a.dry_run)
@@ -10,5 +10,12 @@
"open_until": 0.0,
"reason": null,
"strikes": {}
},
"ieee": {
"open_until": 1790933827.9323854,
"reason": "http_403",
"strikes": {
"http_403": 1
}
}
}