news-digest: fixes from first deploy on ana-docker
Three iterations to get end-to-end:
1. Dockerfile missed COPY run-digest.sh — cron's exec target wasn't
in the image, every fire failed. Added COPY + chmod.
2. Jinja template used {{ list|sum(attribute='items') }} which
sum()s lists with start=0 → TypeError int+list. Switched to
computing reddit_total / tech_total in Python and passing as
template args.
3. LLM defaulted to qwen3.5-35-a3b which (a) is broken in
llama-swap (model process exits on launch), (b) when working,
defaults to extended-thinking mode that eats the entire token
budget without producing any visible content. Same pattern with
qwen3.6-35-a3b. Switched default to granite-4-small — small (4B),
fast (~1s/call), no thinking-mode pathology, returns clean JSON.
Whole pipeline now runs in ~35s total across 8 sources.
Also hardened the LLM response parser to fall back to
reasoning_content when content is empty — catches the thinking-mode
case if anyone ever points the digest at one of those models. Plus
the deploy playbook gained DOCKER_BUILDKIT=0 because ana-docker is
on docker 20.10 which doesn't carry the buildx driver versions our
newer client expects ("client version 1.52 is too new"). Real fix is
upgrading docker on the fleet — separate workstream.
This commit is contained in:
@@ -94,10 +94,15 @@ steps:
|
||||
# /output, so the worker writes a copy of style.css into /output too.
|
||||
|
||||
- name: docker compose build (~2-3 min first time)
|
||||
# DOCKER_BUILDKIT=0 forces the legacy build path. ana-docker is on
|
||||
# docker 20.10 (Debian package) which doesn't carry the buildx
|
||||
# driver versions our newer client expects → "client version 1.52
|
||||
# is too new" without this fallback. Strip the noisy per-step
|
||||
# download lines for log readability.
|
||||
shell: |
|
||||
set -o pipefail
|
||||
cd {{ compose_dir }} && docker compose build --progress=plain 2>&1 \
|
||||
| grep -vE '^#[0-9]+ [0-9.]+ (Downloading|Collecting|Requirement|Using cached|Installing collected|Successfully (installed|built)|Saved /|━|Resolved|Prepared|Built)'
|
||||
cd {{ compose_dir }} && DOCKER_BUILDKIT=0 docker compose build 2>&1 \
|
||||
| grep -vE '^Step [0-9]+/[0-9]+ : (RUN|COPY)|^Removing intermediate|^ ---> |^ ---> Running|Collecting|Downloading|Requirement|Using cached|Installing collected|Successfully (installed|built)|━'
|
||||
|
||||
- name: Pre-stage style.css into /output so the first page render works
|
||||
# Worker writes index.html which references "style.css" (relative).
|
||||
|
||||
@@ -21,7 +21,13 @@ NEWS_DIGEST_TZ=America/Los_Angeles
|
||||
# Model picked for one-shot summarization quality + low VRAM impact.
|
||||
# qwen3.5-35-a3b is loaded in the persistent group on ana-ml2.
|
||||
LLAMA_SWAP_URL=http://10.250.50.54:9292
|
||||
LLAMA_SWAP_MODEL=qwen3.5-35-a3b
|
||||
# granite-4-small — small (~4B), fast (~1s/call), no extended-thinking
|
||||
# phase that eats the token budget like qwen3.x do. Plenty of capability
|
||||
# for the one-sentence-tldr + one-word-tag task. To swap to a larger
|
||||
# model later, ones currently working: gemma4-26b-a4b, granite-4-small.
|
||||
# Avoid: qwen3.5-35-a3b (model file broken — process exits on launch),
|
||||
# qwen3.6-35-a3b (defaults to thinking mode, eats budget without output).
|
||||
LLAMA_SWAP_MODEL=granite-4-small
|
||||
LLAMA_SWAP_TIMEOUT=180
|
||||
|
||||
# ── miniflux (feed source for Tech aggregators + subreddit list) ─────
|
||||
|
||||
@@ -19,8 +19,9 @@ WORKDIR /app
|
||||
COPY digest.py /app/digest.py
|
||||
COPY templates /app/templates
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY run-digest.sh /usr/local/bin/run-digest.sh
|
||||
COPY crontab /etc/crontabs/root
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-digest.sh
|
||||
|
||||
# Sentinel + first-run output dir
|
||||
VOLUME /output
|
||||
|
||||
@@ -275,7 +275,11 @@ def summarize_source(src: Source) -> None:
|
||||
timeout=LLAMA_SWAP_TIMEOUT,
|
||||
)
|
||||
r.raise_for_status()
|
||||
content = r.json()["choices"][0]["message"]["content"].strip()
|
||||
msg = r.json()["choices"][0]["message"]
|
||||
# Models in extended-thinking mode (e.g. Qwen3.x defaults) put
|
||||
# output in reasoning_content and leave content empty until they
|
||||
# exit thinking — fall back so we get *something* to parse.
|
||||
content = (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
||||
# Some models wrap JSON in ```...``` even when told not to.
|
||||
content = re.sub(r"^```(?:json)?\s*|\s*```$", "", content, flags=re.M).strip()
|
||||
mapped = {x.get("id"): x for x in json.loads(content)}
|
||||
@@ -306,9 +310,13 @@ def render(reddit_sources: list[Source], tech_sources: list[Source],
|
||||
env.filters["domain"] = _domain
|
||||
template = env.get_template("digest.html.j2")
|
||||
edition = "morning" if generated_at.hour < 14 else "evening"
|
||||
reddit_kept = [s for s in reddit_sources if s.items]
|
||||
tech_kept = [s for s in tech_sources if s.items]
|
||||
return template.render(
|
||||
reddit_sources=[s for s in reddit_sources if s.items],
|
||||
tech_sources=[s for s in tech_sources if s.items],
|
||||
reddit_sources=reddit_kept,
|
||||
tech_sources=tech_kept,
|
||||
reddit_total=sum(len(s.items) for s in reddit_kept),
|
||||
tech_total=sum(len(s.items) for s in tech_kept),
|
||||
generated_at=generated_at,
|
||||
edition=edition,
|
||||
edition_short="AM" if edition == "morning" else "PM",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<span class="desk-num">01</span>
|
||||
<h2 class="desk-title">Reddit</h2>
|
||||
<span class="desk-sub">top of last {{ generated_at.hour < 14 and "12" or "12" }} hours · score-filtered</span>
|
||||
<span class="desk-count">{{ reddit_sources|sum(attribute='items')|length }} items</span>
|
||||
<span class="desk-count">{{ reddit_total }} items</span>
|
||||
</header>
|
||||
|
||||
{% for src in reddit_sources %}
|
||||
@@ -122,7 +122,7 @@
|
||||
<span class="desk-num">02</span>
|
||||
<h2 class="desk-title">Tech Feeds</h2>
|
||||
<span class="desk-sub">non-reddit · last {{ generated_at.hour < 14 and "12" or "12" }} hours</span>
|
||||
<span class="desk-count">{{ tech_sources|sum(attribute='items')|length }} items</span>
|
||||
<span class="desk-count">{{ tech_total }} items</span>
|
||||
</header>
|
||||
|
||||
{% for src in tech_sources %}
|
||||
|
||||
Reference in New Issue
Block a user