diff --git a/agent-office/app/agents/stock.py b/agent-office/app/agents/stock.py
index 87934d8..f023e08 100644
--- a/agent-office/app/agents/stock.py
+++ b/agent-office/app/agents/stock.py
@@ -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'• {title_esc}{press_suffix}')
+ else:
+ headlines.append(f"• {title_esc}{press_suffix}")
+
+ if headlines:
+ parts.append("📰 주요 뉴스\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"📈 아침 시장 브리핑\n\n{result['summary']}"
+ wife_text = f"📈 아침 시장 브리핑\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"
diff --git a/agent-office/app/telegram/formatter.py b/agent-office/app/telegram/formatter.py
index 4345a24..a22c546 100644
--- a/agent-office/app/telegram/formatter.py
+++ b/agent-office/app/telegram/formatter.py
@@ -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} [{_h(meta['emoji'])} {_h(meta['display_name'])}] {_h(title)}"
# Telegram 단일 메시지 4096자 제한 대응 (헤더/푸터 여유 512자 확보)
- safe_body = _h(body)
+ # body_is_html=True 면 호출자가 이미 HTML-safe하게 구성한 것으로 간주 (예: 링크 포함)
+ safe_body = body if body_is_html else _h(body)
if len(safe_body) > 3500:
safe_body = safe_body[:3500] + "\n…(생략)"
diff --git a/agent-office/app/telegram/messaging.py b/agent-office/app/telegram/messaging.py
index 49a3ee6..969bd28 100644
--- a/agent-office/app/telegram/messaging.py
+++ b/agent-office/app/telegram/messaging.py
@@ -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 포맷(링크 등) 구성한 경우.
+ """
+ text = format_agent_message(agent_id, kind, title, body, metadata, body_is_html=body_is_html)
reply_markup = None
if actions:
buttons = []
diff --git a/stock-lab/app/main.py b/stock-lab/app/main.py
index 69397b3..fdb9b8c 100644
--- a/stock-lab/app/main.py
+++ b/stock-lab/app/main.py
@@ -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, 인증 필요) ---