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, 인증 필요) ---