Fix date framing in briefings and make Blogger posts idempotent

The 9/1 briefing called the US 8/31 session "9월 첫 거래일" - the LLM
was anchoring on the KRX date. Both prompts now pin every temporal
phrase (weekday, first/last trading day, month boundaries) to the US
session date, and the verifier treats a mismatch as a major issue.
BloggerDeliverer now updates the existing post for the same KRX date
instead of creating a duplicate on resends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-09-01 14:13:01 +09:00
co-authored by Claude Fable 5
parent b75bf0b887
commit 830e4e0522
3 changed files with 131 additions and 29 deletions
+56 -13
View File
@@ -38,12 +38,16 @@ def briefing_html(markdown_text: str) -> str:
return _MD.convert("\n".join(lines).strip())
def post_title_prefix(result: BriefingResult) -> str:
"""Stable per-day prefix - resends update the existing post instead of creating a duplicate."""
return f"미국 마감 브리핑 {result.payload.krx_date.isoformat()}"
def post_title(result: BriefingResult) -> str:
"""'미국 마감 브리핑 YYYY-MM-DD — 헤드라인' (falls back to the session date without a report)."""
p = result.payload
base = f"미국 마감 브리핑 {p.krx_date.isoformat()}"
base = post_title_prefix(result)
headline = (result.report.headline or "").strip() if result.report else ""
return f"{base} — {headline}" if headline else f"{base} (미 {p.session_date.isoformat()} 세션)"
return f"{base} — {headline}" if headline else f"{base} (미 {result.payload.session_date.isoformat()} 세션)"
class BloggerDeliverer:
@@ -101,7 +105,7 @@ class BloggerDeliverer:
# ------------------------------------------------------------------ delivery
def deliver(self, result: BriefingResult) -> str:
"""Create the post and return its public URL."""
"""Create the day's post - or update it when one already exists - and return its public URL."""
body: dict[str, Any] = {
"kind": "blogger#post",
"title": post_title(result),
@@ -109,31 +113,70 @@ class BloggerDeliverer:
}
if self.labels:
body["labels"] = self.labels
url = f"{API_BASE}/blogs/{self.blog_id}/posts/"
existing_id = self._find_existing(post_title_prefix(result))
if existing_id:
data = self._request("PUT", f"{API_BASE}/blogs/{self.blog_id}/posts/{existing_id}", json_body=body)
action = "updated"
else:
data = self._request("POST", f"{API_BASE}/blogs/{self.blog_id}/posts/", json_body=body)
action = "published"
post_url = data.get("url") or f"post id {data.get('id', '?')}"
log.info("blogger post %s: %s", action, post_url)
return post_url
def _find_existing(self, title_prefix: str) -> str | None:
"""Id of the newest post whose title carries this day's prefix; None on miss or lookup failure."""
try:
data = self._request(
"GET",
f"{API_BASE}/blogs/{self.blog_id}/posts",
params={"fetchBodies": "false", "maxResults": "25"},
)
except DeliveryError as exc:
if "token refresh failed" in str(exc):
raise # a broken token will fail the publish too - surface it once, immediately
# any other lookup failure must not block a fresh publish
log.warning("blogger post lookup failed, publishing as a new post: %s", exc)
return None
for item in data.get("items", []):
if str(item.get("title", "")).startswith(title_prefix):
return str(item["id"])
return None
def _request(
self,
method: str,
url: str,
*,
params: dict[str, str] | None = None,
json_body: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""One authorized API call with retries (fresh token on 401, backoff on 429/5xx)."""
force_token = False
last_error = "unknown error"
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
resp = self._client.post(
url, json=body, headers={"Authorization": f"Bearer {self._token(force=force_token)}"}
resp = self._client.request(
method,
url,
params=params,
json=json_body,
headers={"Authorization": f"Bearer {self._token(force=force_token)}"},
)
except httpx.HTTPError as exc:
last_error = f"{type(exc).__name__}: {exc}"
log.warning("blogger post attempt %d/%d failed: %s", attempt, MAX_ATTEMPTS, last_error)
log.warning("blogger %s attempt %d/%d failed: %s", method, attempt, MAX_ATTEMPTS, last_error)
time.sleep(BACKOFF_BASE_SECONDS * attempt)
continue
if resp.status_code == 200:
data = resp.json()
post_url = data.get("url") or f"post id {data.get('id', '?')}"
log.info("blogger post published: %s", post_url)
return post_url
return resp.json()
last_error = f"HTTP {resp.status_code} {resp.text[:200]}"
if resp.status_code == 401 and not force_token:
force_token = True # cached token stale/revoked - mint a fresh one and retry
continue
if resp.status_code in (429, 500, 502, 503, 504) and attempt < MAX_ATTEMPTS:
log.warning("blogger post attempt %d/%d failed: %s", attempt, MAX_ATTEMPTS, last_error)
log.warning("blogger %s attempt %d/%d failed: %s", method, attempt, MAX_ATTEMPTS, last_error)
time.sleep(BACKOFF_BASE_SECONDS * attempt)
continue
break # non-retryable (4xx)