Skip to content

feat: env-based Gemini model selection, refined summarization prompt and sending reliability fixes - #2

Merged
cuogne merged 3 commits into
mainfrom
dev
Aug 16, 2026
Merged

cuogne merged 3 commits into
mainfrom
dev

Conversation

@cuogne

@cuogne cuogne commented Aug 16, 2026

Copy link
Copy Markdown
Owner

What changed

Gemini model selection via env (5a0c8fc)

Before: the Gemini model list was hardcoded in config/models.go. Every time Gemini added a new model or deprecated an old one, the flow was: edit code → push → rebuild Docker image → redeploy — slow and wasteful for a 4-line change.

After: the model list is read from the GEMINI_MODELS environment variable at startup:

  • Format: comma-separated, tried in order (first = primary, rest = fallbacks), e.g.
    GEMINI_MODELS=gemini-3.5-flash-lite,gemini-3.7-flash,gemini-3.6-flash,gemini-3.5-flash
  • Loaded once via sync.Once and cached for the process lifetime
  • If the variable is unset or empty, the built-in default list is used — so existing deployments keep working with no changes

What this means day-to-day:

  • To switch/add/remove models, just set/update GEMINI_MODELS (in .env or via docker run -e GEMINI_MODELS=...) and restart the container:

    # docker
    docker run -d --env-file .env --name tele-bot --restart unless-stopped \
      -e GEMINI_MODELS="new-gemini-model,gemini-3.7-flash" \
      hcmus-news-tele-bot
    
    # or edit GEMINI_MODELS in .env (IDE, Vim or Nano), then:
    docker restart tele-bot
  • No code change, no push, no image rebuild, no redeploy — just restart

  • Only restart is needed because the env is read once at startup (models are process-lifetime config, so hot-reloading wasn't worth the complexity)
    Other notes:

  • .env.example updated with GEMINI_MODELS

Summarization prompt improved (5a0c8fc)

Before: the summarization prompt was a 60+ line inline string inside SummarizeContentWithGemini(), making summarize.go hard to read and review, and the prompt itself had a few loose ends.

After:

  • Prompt moved to a dedicated internal/service/prompt.go — summarize.go now just does fmt.Sprintf(summarizePrompt, content), so the summarization logic and the prompt text are cleanly separated
  • The "return empty string" rule was rewritten stricter: "return a completely empty string without any characters or explanation" — previously the model sometimes returned junk like "(không có nội dung)" instead of truly empty, which slipped past the summary == "" check and got sent to users.

No behavior change expected for normal articles; mostly cleaner code + fewer edge-case failures.

Sending reliability fixes (d509dac)

1. HTML-escape before sending

The bot sends messages in Telegram HTML mode (ModeHTML). Title, summary, and link were inserted raw into the message. If an article title from RSS/API contained <, &, or unbalanced tags, Telegram rejected the message and the user silently lost that news item. Now title/summary/link all go through html.EscapeString() before formatting, so malformed text renders as literal characters instead of breaking the send.

2. Auto-unsubscribe unreachable users

Every 10-minute cycle, the bot tries to send to all subscribed users. If a user blocked the bot (or deleted their account), every send fails with a 403 and the error was just logged — meaning endless retry+log spam forever for a user who will never receive anything again. Now SendTele detects the 403 errors (ErrBlockedByUser, ErrUserIsDeactivated, ErrNotStartedByUser) and calls repository.UnsubscribeUser right away → is_subscribed = false. That user is excluded from all future cycles until they /subscribe again manually. Logic lives in internal/service/handle_unreachable_user.go.

3. Retry failed content fetches instead of dropping them

Before: if GetContentFromURL failed (site slow/down), the worker immediately sent the title-only fallback message and saved the article to DB. The article was then marked "sent" and never processed again — a transient outage permanently degraded that news item into a bare link.

After: fetch failures are retried on subsequent cron cycles:

  • An in-memory counter in internal/service/retry_fetch.go tracks attempts per URL
  • On failure, if attempts < 3 → the result is marked Retryable, and the handler skips both sending and saving — the article stays "new", so it's picked up again next cycle
  • After 3 failures → falls back to the old behavior (send title-only + save) so genuinely broken pages don't retry forever and the news isn't lost entirely

Note: the counter is in-memory only, so it resets on bot restart. If retry state needs to survive restarts later, a retry_count column in the DB schema would be the natural next step.

Docs (76ce8ac)

  • README.md updated to reflect the new env-based config and reliability behavior:
    • Added GEMINI_MODELS to the .env example and a short explanation (optional, comma-separated, tried in order, built-in default used when unset)

Testing

  • go build ./..., go vet ./..., gofmt -l . all pass
  • Unit tests passed: go test ./... -v

@cuogne
cuogne merged commit 816b4ca into main Aug 16, 2026
1 check passed
@cuogne
cuogne deleted the dev branch August 16, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant