feat(agent-office): 주식 브리핑 본문에 주요 뉴스 헤드라인+링크 추가

- stock-lab /news/summarize 응답에 top 8 기사(title/link/press) 포함
- agent-office stock.py: _build_briefing_body() 헬퍼 분리 — LLM 요약 + 📰 주요 뉴스 섹션(HTML <a> 링크). 향후 본문 고도화 시 이 함수만 수정
- telegram 포맷터/메시징에 body_is_html 플래그 추가 (링크 포함 메시지는 이중 escape 회피)
- 아내 전송도 동일 본문 재사용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 00:56:20 +09:00
parent b867b8ce13
commit b23346143f
4 changed files with 59 additions and 5 deletions

View File

@@ -1,10 +1,44 @@
import asyncio
import html
from typing import Optional
from .base import BaseAgent
from ..db import create_task, update_task_status, get_agent_config, add_log
from .. import service_proxy
def _build_briefing_body(result: dict, max_headlines: int = 5) -> str:
"""아침 시장 브리핑 본문 조립.
LLM 요약 + 주요 뉴스 헤드라인(링크) 섹션을 합친다.
향후 본문 고도화 시 이 함수만 수정하면 됨 (텔레그램 HTML parse_mode).
"""
summary = (result.get("summary") or "").strip()
articles = result.get("articles") or []
# body_is_html=True 로 보낼 예정이므로 LLM 요약(plain text)도 escape
parts = [html.escape(summary)] if summary else []
headlines = []
for a in articles[:max_headlines]:
title = (a.get("title") or "").strip()
if not title:
continue
title_esc = html.escape(title)
link = (a.get("link") or "").strip()
press = (a.get("press") or "").strip()
press_suffix = f"{html.escape(press)}" if press else ""
if link:
headlines.append(f'• <a href="{html.escape(link, quote=True)}">{title_esc}</a>{press_suffix}')
else:
headlines.append(f"{title_esc}{press_suffix}")
if headlines:
parts.append("📰 <b>주요 뉴스</b>\n" + "\n".join(headlines))
return "\n\n".join(parts)
class StockAgent(BaseAgent):
agent_id = "stock"
display_name = "주식 트레이더"
@@ -22,13 +56,16 @@ class StockAgent(BaseAgent):
await self.transition("reporting", "뉴스 요약 전송 중...")
body = _build_briefing_body(result)
# 새 통합 텔레그램 API 사용
from ..telegram import send_agent_message
tg_result = await send_agent_message(
agent_id=self.agent_id,
kind="report",
title="아침 시장 브리핑",
body=result["summary"],
body=body,
body_is_html=True,
task_id=task_id,
metadata={
"tokens": result["tokens"]["total"],
@@ -41,7 +78,7 @@ class StockAgent(BaseAgent):
from ..config import TELEGRAM_WIFE_CHAT_ID
if TELEGRAM_WIFE_CHAT_ID:
from ..telegram.messaging import send_raw
wife_text = f"📈 <b>아침 시장 브리핑</b>\n\n{result['summary']}"
wife_text = f"📈 <b>아침 시장 브리핑</b>\n\n{body}"
wife_result = await send_raw(wife_text, chat_id=TELEGRAM_WIFE_CHAT_ID)
if not wife_result.get("ok"):
desc = wife_result.get("description") or "unknown"

View File

@@ -21,13 +21,15 @@ def format_agent_message(
title: str,
body: str,
metadata: Optional[dict] = None,
body_is_html: bool = False,
) -> str:
meta = get_agent_meta(agent_id)
icon = KIND_ICONS.get(kind, "")
header = f"{icon} <b>[{_h(meta['emoji'])} {_h(meta['display_name'])}]</b> {_h(title)}"
# Telegram 단일 메시지 4096자 제한 대응 (헤더/푸터 여유 512자 확보)
safe_body = _h(body)
# body_is_html=True 면 호출자가 이미 HTML-safe하게 구성한 것으로 간주 (예: <a> 링크 포함)
safe_body = body if body_is_html else _h(body)
if len(safe_body) > 3500:
safe_body = safe_body[:3500] + "\n…(생략)"

View File

@@ -37,9 +37,13 @@ async def send_agent_message(
task_id: Optional[str] = None,
actions: Optional[list] = None,
metadata: Optional[dict] = None,
body_is_html: bool = False,
) -> dict:
"""통합 에이전트 메시지 API. 모든 에이전트가 이걸 씀."""
text = format_agent_message(agent_id, kind, title, body, metadata)
"""통합 에이전트 메시지 API. 모든 에이전트가 이걸 씀.
body_is_html=True: 호출자가 이미 HTML-safe 포맷(링크 <a> 등) 구성한 경우.
"""
text = format_agent_message(agent_id, kind, title, body, metadata, body_is_html=body_is_html)
reply_markup = None
if actions:
buttons = []

View File

@@ -167,9 +167,20 @@ async def summarize_latest_news(req: NewsSummarizeRequest = NewsSummarizeRequest
logger.exception("뉴스 요약 중 예상치 못한 오류")
raise HTTPException(status_code=500, detail=f"뉴스 요약 실패: {e}")
# 상위 기사 메타 일부만 노출 (클라이언트가 본문 조립에 사용)
top_articles = [
{
"title": (a.get("title") or "").strip(),
"link": a.get("link") or "",
"press": a.get("press") or "",
"pub_date": a.get("pub_date") or "",
}
for a in articles[:8]
]
return {
**result,
"article_count": len(articles),
"articles": top_articles,
}
# --- Trading API (Windows Proxy, 인증 필요) ---