Wiki Jokes: daily LLM jokes from Wikipedia featured article
- FastAPI + APScheduler + SQLite, single container - Daily generation at 06:00 Europe/Copenhagen + cold-start generation - Retry with backoff (3 attempts/15 min), stale fallback with banner - OpenAI-compatible endpoint via env (OPENAI_BASE_URL/MODEL/API_KEY) - docker-compose with named volume for joke persistence
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Daily joke generation job with retry/backoff and stale-fallback semantics."""
|
||||
import logging
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from . import db
|
||||
from .config import settings
|
||||
from .llm_client import generate_jokes
|
||||
from .wiki_client import get_featured_article
|
||||
|
||||
log = logging.getLogger("jokes.generator")
|
||||
|
||||
# In-process retry bookkeeping (resets on container restart; cold-start
|
||||
# generation covers the restart case anyway).
|
||||
_attempts_today: dict[str, int] = {}
|
||||
|
||||
|
||||
def today_local() -> date:
|
||||
return datetime.now(ZoneInfo(settings.timezone)).date()
|
||||
|
||||
|
||||
def _attempt_key(day: date) -> str:
|
||||
return day.isoformat()
|
||||
|
||||
|
||||
def generation_exhausted(day: date) -> bool:
|
||||
return _attempts_today.get(_attempt_key(day), 0) >= settings.max_attempts
|
||||
|
||||
|
||||
def run_generation() -> bool:
|
||||
"""Try to generate today's jokes. Returns True on success."""
|
||||
day = today_local()
|
||||
if db.get_day(day) is not None:
|
||||
log.info("Jokes for %s already exist, skipping", day)
|
||||
return True
|
||||
|
||||
attempts = _attempts_today.get(_attempt_key(day), 0)
|
||||
if attempts >= settings.max_attempts:
|
||||
log.warning("Generation for %s already exhausted (%d attempts)", day, attempts)
|
||||
return False
|
||||
|
||||
_attempts_today[_attempt_key(day)] = attempts + 1
|
||||
try:
|
||||
article = get_featured_article(day)
|
||||
log.info("Fetched featured article: %s", article.title)
|
||||
jokes = generate_jokes(article.title, article.extract)
|
||||
db.save_batch(day, article.title, article.url, jokes)
|
||||
log.info("Saved %d jokes for %s", len(jokes), day)
|
||||
return True
|
||||
except Exception as exc: # noqa: BLE001 — job must never crash the scheduler
|
||||
used = _attempts_today[_attempt_key(day)]
|
||||
log.error(
|
||||
"Generation attempt %d/%d for %s failed: %s",
|
||||
used, settings.max_attempts, day, exc,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def next_retry_delay() -> timedelta:
|
||||
return timedelta(minutes=settings.retry_minutes)
|
||||
Reference in New Issue
Block a user