Add a Monday-morning weekly recap briefing
No US session closes into Monday KST, so Mondays (default 07:00, cron in the daemon with restart catch-up) now publish a weekly review built entirely from stored data: the quotes table collapsed into week-over- week changes and the week's daily briefing headlines as the narrative. The recap is a BriefingResult kind="weekly" reusing MarketPayload, so the store, verification pass, all delivery channels (Blogger title "미국 주간 리뷰 <monday>") and the dashboard (주간 리뷰 badge) work unchanged. Manual runs via `run-once --weekly [--date <monday>]`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -16,12 +16,24 @@ class LLMProvider(Protocol):
|
||||
"""Return (validated Report, model id that produced it). Raise LLMError on total failure."""
|
||||
...
|
||||
|
||||
def verify(self, payload: MarketPayload, report: Report) -> tuple[Verification, str]:
|
||||
"""Fact-check `report` against the payload. Raise LLMError on total failure."""
|
||||
def summarize_weekly(self, payload: MarketPayload, week_sessions: list) -> tuple[Report, str]:
|
||||
"""Weekly recap over a payload whose quotes carry week-over-week changes."""
|
||||
...
|
||||
|
||||
def verify(
|
||||
self, payload: MarketPayload, report: Report, data_message: str | None = None
|
||||
) -> tuple[Verification, str]:
|
||||
"""Fact-check `report` against the payload (or `data_message`). Raise LLMError on total failure."""
|
||||
...
|
||||
|
||||
def revise(
|
||||
self, payload: MarketPayload, report: Report, issues: list[VerificationIssue]
|
||||
self,
|
||||
payload: MarketPayload,
|
||||
report: Report,
|
||||
issues: list[VerificationIssue],
|
||||
*,
|
||||
system_prompt: str | None = None,
|
||||
data_message: str | None = None,
|
||||
) -> tuple[Report, str]:
|
||||
"""Return a corrected Report addressing `issues`. Raise LLMError on total failure."""
|
||||
...
|
||||
|
||||
+26
-7
@@ -17,8 +17,10 @@ from us2kr.llm.prompts import (
|
||||
REVISE_INSTRUCTION,
|
||||
SYSTEM_PROMPT,
|
||||
VERIFY_SYSTEM_PROMPT,
|
||||
WEEKLY_SYSTEM_PROMPT,
|
||||
build_user_message,
|
||||
build_verify_message,
|
||||
build_verify_message_from,
|
||||
build_weekly_user_message,
|
||||
format_issues,
|
||||
)
|
||||
from us2kr.models import MarketPayload, Report, Verification, VerificationIssue
|
||||
@@ -216,21 +218,38 @@ class MiniMaxProvider:
|
||||
]
|
||||
return self._run(messages, Report, "report", task="summarize")
|
||||
|
||||
def verify(self, payload: MarketPayload, report: Report) -> tuple[Verification, str]:
|
||||
"""Fact-check `report` against the payload. Raise LLMError on total failure."""
|
||||
def summarize_weekly(self, payload: MarketPayload, week_sessions: list) -> tuple[Report, str]:
|
||||
"""Weekly recap: payload.quotes carry week-over-week changes, history_notes the daily headlines."""
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": WEEKLY_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": build_weekly_user_message(payload, week_sessions)},
|
||||
]
|
||||
return self._run(messages, Report, "report", task="summarize-weekly")
|
||||
|
||||
def verify(
|
||||
self, payload: MarketPayload, report: Report, data_message: str | None = None
|
||||
) -> tuple[Verification, str]:
|
||||
"""Fact-check `report` against the payload (or an explicit serialisation of it)."""
|
||||
source = data_message if data_message is not None else build_user_message(payload)
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": VERIFY_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": build_verify_message(payload, report)},
|
||||
{"role": "user", "content": build_verify_message_from(source, report)},
|
||||
]
|
||||
return self._run(messages, Verification, "verification", task="verify")
|
||||
|
||||
def revise(
|
||||
self, payload: MarketPayload, report: Report, issues: list[VerificationIssue]
|
||||
self,
|
||||
payload: MarketPayload,
|
||||
report: Report,
|
||||
issues: list[VerificationIssue],
|
||||
*,
|
||||
system_prompt: str | None = None,
|
||||
data_message: str | None = None,
|
||||
) -> tuple[Report, str]:
|
||||
"""Ask for a corrected Report given verification issues. Raise LLMError on total failure."""
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": build_user_message(payload)},
|
||||
{"role": "system", "content": system_prompt or SYSTEM_PROMPT},
|
||||
{"role": "user", "content": data_message or build_user_message(payload)},
|
||||
{"role": "assistant", "content": report.model_dump_json()},
|
||||
{"role": "user", "content": REVISE_INSTRUCTION + format_issues(issues)},
|
||||
]
|
||||
|
||||
+63
-6
@@ -169,6 +169,33 @@ def _bullets(items: list[str]) -> list[str]:
|
||||
return [f"- {item}" for item in items] or ["(없음)"]
|
||||
|
||||
|
||||
WEEKLY_SYSTEM_PROMPT: str = f"""당신은 한국 투자자를 위한 미국 시장 주간 리뷰 작성자입니다.
|
||||
주말이 지난 월요일 아침, 한국 장이 열리기 전에 읽히는 "지난주 미국 시장 정리"를 작성합니다.
|
||||
|
||||
## 작성 규칙
|
||||
1. 숫자는 사용자 메시지에 제공된 값만 사용합니다. 시세의 변동률은 **한 주 누적 변동**이므로 반드시
|
||||
"주간" 관점으로 서술하고, 하루 변동처럼 쓰지 않습니다. 제공되지 않은 숫자를 만들거나 추정하지 않습니다.
|
||||
2. "일별 요약"은 그 주의 일일 브리핑 헤드라인입니다. 사건 서술은 이 범위 안에서만 하고, 헤드라인만으로
|
||||
확인되지 않는 배경·인과는 "보도에 따르면"으로 표기하거나 생략합니다.
|
||||
3. 투자 조언(매수/매도/비중 조절 권고)을 하지 않습니다. 사실과 흐름을 정리하는 정보 요약 톤을 유지합니다.
|
||||
4. 한국 장 관점을 반드시 포함합니다: 반도체(SOX 등)·환율·한국 ADR/EWY의 주간 흐름과, 이번 주 한국 시장에
|
||||
주는 시사점을 다룹니다.
|
||||
5. 간결한 한국어 경어체(~습니다)로 작성합니다. 모든 필드는 한국어로만 작성합니다(고유명사 예외).
|
||||
6. 날짜·요일·시점 표현은 전부 **미국 거래일** 기준입니다. 세션 정보에 주어진 주간 범위(첫 세션~마지막 세션)
|
||||
밖의 시점을 서술하지 않습니다.
|
||||
7. 출력 스키마는 일일 브리핑과 같지만 의미를 주간으로 바꿔 씁니다:
|
||||
headline=주간 총평 한 줄, market_overview=주간 시장 개요, key_drivers=지난주 주요 사건/요인,
|
||||
rates_fx_commodities=금리·환율·원자재의 주간 흐름, korea_implications=이번 주 한국 장 시사점,
|
||||
watchlist_today=이번 주 한국 장에서 확인할 것, risk_flags=이번 주 리스크.
|
||||
8. 출력은 지정된 JSON 스키마만 반환합니다. 배열 필드는 반드시 JSON 배열(각 항목 한 문장)로 출력합니다.
|
||||
|
||||
## 입력 데이터 용어집
|
||||
시세는 아래 그룹으로 나뉘어 제공되며, 각 줄의 형식은 `심볼 이름: 주말 종가 (주간 변동)` 입니다.
|
||||
국채 금리의 변동은 주간 수익률 변화폭(%p)입니다. 값이 없는 항목은 "자료 없음"으로 표시됩니다.
|
||||
{_GROUP_GLOSSARY}
|
||||
"""
|
||||
|
||||
|
||||
VERIFY_SYSTEM_PROMPT: str = """당신은 미국 시장 브리핑의 팩트체커입니다. 브리핑 초안이 함께 제공된 원본 데이터와
|
||||
모순되지 않는지만 검사합니다. 문체·구성·의견은 평가하지 않습니다.
|
||||
|
||||
@@ -195,14 +222,14 @@ REVISE_INSTRUCTION: str = (
|
||||
)
|
||||
|
||||
|
||||
def build_verify_message_from(data_message: str, report: Report) -> str:
|
||||
"""Fact-check user message from an explicit data serialisation plus the draft report."""
|
||||
return data_message + "\n## 검증 대상 브리핑 초안 (JSON)\n" + report.model_dump_json(indent=2) + "\n"
|
||||
|
||||
|
||||
def build_verify_message(payload: MarketPayload, report: Report) -> str:
|
||||
"""User message for the fact-check pass: the payload serialisation plus the draft report."""
|
||||
return (
|
||||
build_user_message(payload)
|
||||
+ "\n## 검증 대상 브리핑 초안 (JSON)\n"
|
||||
+ report.model_dump_json(indent=2)
|
||||
+ "\n"
|
||||
)
|
||||
return build_verify_message_from(build_user_message(payload), report)
|
||||
|
||||
|
||||
def format_issues(issues: list) -> str:
|
||||
@@ -213,6 +240,36 @@ def format_issues(issues: list) -> str:
|
||||
return "\n".join(lines) or "(없음)"
|
||||
|
||||
|
||||
def build_weekly_user_message(payload: MarketPayload, week_sessions: list) -> str:
|
||||
"""Serialise a weekly payload (quotes = week-over-week changes, notes = daily headlines)."""
|
||||
out: list[str] = []
|
||||
first = week_sessions[0].isoformat() if week_sessions else "?"
|
||||
last = week_sessions[-1].isoformat() if week_sessions else "?"
|
||||
|
||||
out.append("## 세션 정보 (주간)")
|
||||
out.append(f"- 지난주 미국 거래일: {first} ~ {last} ({len(week_sessions)}거래일)")
|
||||
out.append(f"- 한국 거래일(발행일): {payload.krx_date.isoformat()} (월요일)")
|
||||
out.append(f"- 오늘 한국 장: {'개장' if payload.krx_open_today else '휴장'}")
|
||||
out.append("")
|
||||
|
||||
out.append("## 주간 시세 (변동 = 주간 누적)")
|
||||
by_group = payload.quotes_by_group()
|
||||
if not by_group:
|
||||
out.append("(시세 자료 없음)")
|
||||
for group in _group_order(list(by_group)):
|
||||
out.append(f"### {GROUP_TITLES_KO.get(group, group)}")
|
||||
out.extend(_fmt_quote(q) for q in by_group[group])
|
||||
out.append("")
|
||||
|
||||
out.append("## 일별 요약 (그 주의 일일 브리핑 헤드라인)")
|
||||
out.extend(_bullets(payload.history_notes))
|
||||
out.append("")
|
||||
|
||||
out.append("## 수집 누락")
|
||||
out.extend(_bullets(payload.data_gaps))
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def build_user_message(payload: MarketPayload) -> str:
|
||||
"""Serialise the payload into a deterministic Korean text block for the user message."""
|
||||
out: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user