Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ collect → gate → filter → rank → summarize → greet → render → deli
- **SSM Parameter Store**. LangChain API 키와 Batch 큐/정의 이름을 저장합니다.
- **SES**로 뉴스레터를 전달하고, **SNS**로 실행·헬스 알림을 보냅니다.
- **Bedrock(us-west-2)**. GPT-6.1 Sol(필터), Claude Sonnet 5.5(요약), Claude Haiku 5.5(인사말,
출력 복구). [`app/src/constants.py`](./app/src/constants.py)의 `LanguageModelId`
제목 Title Case, 출력 복구). [`app/src/constants.py`](./app/src/constants.py)의 `LanguageModelId`
카탈로그에 있는 모델은 OpenAI GPT-6 모델을 포함해 스테이지별로 골라 쓸 수 있습니다.
모델별 요청 규칙(thinking 끄기 값, effort, GPT 호출 방식)은 design.md §8에 있습니다.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ collect → gate → filter → rank → summarize → greet → render → deli
- **SSM Parameter Store**. LangChain API key and Batch queue/definition names.
- **SES**. newsletter delivery. **SNS** — run/health notifications.
- **Bedrock (us-west-2)**. GPT-6.1 Sol (filter), Claude Sonnet 5.5 (summarize), Claude
Haiku 5.5 (greeting and output repair). Any model in the `LanguageModelId`
Haiku 5.5 (greeting, title casing and output repair). Any model in the `LanguageModelId`
catalog in [`app/src/constants.py`](./app/src/constants.py) is selectable per
stage, including OpenAI GPT-6 models; per-model request rules (thinking
off-switches, effort, the GPT dialect) are in design.md §8.
Expand Down
8 changes: 4 additions & 4 deletions app/src/title_caser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import re

import boto3
from langchain_core.output_parsers import StrOutputParser
Expand All @@ -10,8 +9,6 @@
from .prompts import TitleCasePrompt
from .utils import retry_with_backoff

_CODE_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$")


def accept_recased(original: str, recased: object) -> str:
"""``recased`` if it differs from ``original`` in letter case only, else
Expand All @@ -34,7 +31,10 @@ def parse_recased(answer: str, originals: list[str]) -> list[str]:
Raises:
ValueError: The answer is not a JSON array of the input's length.
"""
parsed = json.loads(_CODE_FENCE.sub("", answer.strip()))
# Only the array is parsed, so a code fence or a sentence around it does not
# cost the whole issue its casing.
start, end = answer.find("["), answer.rfind("]")
parsed = json.loads(answer[start : end + 1] if 0 <= start < end else answer)
if not isinstance(parsed, list) or len(parsed) != len(originals):
raise ValueError(f"expected a JSON array of {len(originals)} titles")
return [accept_recased(o, r) for o, r in zip(originals, parsed, strict=True)]
Expand Down
3 changes: 2 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ tech-digest/
- `filtering_model_id` / `summarization_model_id` / `greeting_model_id`: 단계별
Bedrock 모델 ID(Claude 또는 GPT). **세 값 모두 필수이고 기본값이 없습니다.** 템플릿과
배포된 두 스테이지는 GPT-6.1 Sol / Sonnet 5.5 / Haiku 5.5를 씁니다(필터 모델을 고른 이유는 §9).
`greeting_model_id`는 카드 제목 Title Case 정리(§12)에도 쓰입니다.
- `filtering_enable_thinking` / `summarization_enable_thinking`(기본 False): 해당
단계에서 확장 사고(extended thinking)를 켭니다.
- `filtering_thinking_budget_tokens` / `summarization_thinking_budget_tokens`(기본 비움 =
Expand Down Expand Up @@ -693,7 +694,7 @@ GPT-6 Astra를 Converse로 호출합니다. 직접 호출해 확인한 요청
### `TokenUsageLogger`
모든 LLM 호출의 토큰 사용량을 **어느 단계가 호출했는지**와 함께 로그로 남깁니다.
Cost Explorer는 모델 단위로만 청구하는데, 필터링·요약·출력 교정이 같은 모델을 공유하고
인사말과 교정이 또 같은 모델을 공유합니다. 특히 필터링(수집된 글마다 1회, 각각 전문을 실어
인사말·제목 정리·교정이 또 같은 모델을 공유합니다. 특히 필터링(수집된 글마다 1회, 각각 전문을 실어
보냄)은 요약(몇 건)과 비용 형태가 완전히 다르므로, 이 로그 없이 하는 최적화는 추측입니다.
`get_model(stage=...)`로 붙이며, 콜백은 어떤 경우에도 생성을 실패시킬 수 없도록 모든 읽기를
방어적으로 처리합니다.
Expand Down
23 changes: 23 additions & 0 deletions tests/test_pipeline_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ class summarization:
main._title_case_titles([post], _Config(), None)
assert post.title == "a guide"

def test_recased_titles_replace_the_feed_titles_in_order(self, monkeypatch):
from src.feed_parser import Post

class _Caser:
def __init__(self, *args, **kwargs):
pass

def recase(self, titles):
return [t.title() for t in titles]

monkeypatch.setattr(main, "TitleCaser", _Caser)

class _Config:
class summarization:
greeting_model_id = "unused"

posts = [
Post(title=t, link=f"https://x.example/{i}", published_date="2026-10-09")
for i, t in enumerate(["first post", "second post"])
]
main._title_case_titles(posts, _Config(), None)
assert [p.title for p in posts] == ["First Post", "Second Post"]


class TestTestRunsKeepOutOfTheLogFile:
def test_no_file_handler_under_pytest(self):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_title_caser.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def test_pairs_titles_in_order(self):
"Fine-Tuning gpt-oss with LoRA",
]

def test_a_sentence_around_the_array_is_tolerated(self):
answer = "Here are the titles:\n" + json.dumps(TITLES) + "\nDone."
assert parse_recased(answer, TITLES) == TITLES

def test_a_code_fence_is_tolerated(self):
answer = "```json\n" + json.dumps([t.upper() for t in TITLES]) + "\n```"
assert parse_recased(answer, TITLES) == [t.upper() for t in TITLES]
Expand Down Expand Up @@ -84,3 +88,11 @@ def test_malformed_answer_keeps_the_feed_titles(self):

def test_no_titles_makes_no_call(self):
assert self._caser(lambda _t: pytest.fail("called")).recase([]) == []


def test_titles_with_braces_reach_the_prompt_verbatim():
from app.src.prompts import TitleCasePrompt

titles = json.dumps(["Using {placeholders} in f-strings"])
rendered = TitleCasePrompt.get_prompt().format_messages(titles=titles)
assert "Using {placeholders} in f-strings" in rendered[-1].content
Loading