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:
@@ -1,10 +1,44 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import html
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .base import BaseAgent
|
from .base import BaseAgent
|
||||||
from ..db import create_task, update_task_status, get_agent_config, add_log
|
from ..db import create_task, update_task_status, get_agent_config, add_log
|
||||||
from .. import service_proxy
|
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):
|
class StockAgent(BaseAgent):
|
||||||
agent_id = "stock"
|
agent_id = "stock"
|
||||||
display_name = "주식 트레이더"
|
display_name = "주식 트레이더"
|
||||||
@@ -22,13 +56,16 @@ class StockAgent(BaseAgent):
|
|||||||
|
|
||||||
await self.transition("reporting", "뉴스 요약 전송 중...")
|
await self.transition("reporting", "뉴스 요약 전송 중...")
|
||||||
|
|
||||||
|
body = _build_briefing_body(result)
|
||||||
|
|
||||||
# 새 통합 텔레그램 API 사용
|
# 새 통합 텔레그램 API 사용
|
||||||
from ..telegram import send_agent_message
|
from ..telegram import send_agent_message
|
||||||
tg_result = await send_agent_message(
|
tg_result = await send_agent_message(
|
||||||
agent_id=self.agent_id,
|
agent_id=self.agent_id,
|
||||||
kind="report",
|
kind="report",
|
||||||
title="아침 시장 브리핑",
|
title="아침 시장 브리핑",
|
||||||
body=result["summary"],
|
body=body,
|
||||||
|
body_is_html=True,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
metadata={
|
metadata={
|
||||||
"tokens": result["tokens"]["total"],
|
"tokens": result["tokens"]["total"],
|
||||||
@@ -41,7 +78,7 @@ class StockAgent(BaseAgent):
|
|||||||
from ..config import TELEGRAM_WIFE_CHAT_ID
|
from ..config import TELEGRAM_WIFE_CHAT_ID
|
||||||
if TELEGRAM_WIFE_CHAT_ID:
|
if TELEGRAM_WIFE_CHAT_ID:
|
||||||
from ..telegram.messaging import send_raw
|
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)
|
wife_result = await send_raw(wife_text, chat_id=TELEGRAM_WIFE_CHAT_ID)
|
||||||
if not wife_result.get("ok"):
|
if not wife_result.get("ok"):
|
||||||
desc = wife_result.get("description") or "unknown"
|
desc = wife_result.get("description") or "unknown"
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ def format_agent_message(
|
|||||||
title: str,
|
title: str,
|
||||||
body: str,
|
body: str,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
|
body_is_html: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
meta = get_agent_meta(agent_id)
|
meta = get_agent_meta(agent_id)
|
||||||
icon = KIND_ICONS.get(kind, "")
|
icon = KIND_ICONS.get(kind, "")
|
||||||
header = f"{icon} <b>[{_h(meta['emoji'])} {_h(meta['display_name'])}]</b> {_h(title)}"
|
header = f"{icon} <b>[{_h(meta['emoji'])} {_h(meta['display_name'])}]</b> {_h(title)}"
|
||||||
|
|
||||||
# Telegram 단일 메시지 4096자 제한 대응 (헤더/푸터 여유 512자 확보)
|
# 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:
|
if len(safe_body) > 3500:
|
||||||
safe_body = safe_body[:3500] + "\n…(생략)"
|
safe_body = safe_body[:3500] + "\n…(생략)"
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,13 @@ async def send_agent_message(
|
|||||||
task_id: Optional[str] = None,
|
task_id: Optional[str] = None,
|
||||||
actions: Optional[list] = None,
|
actions: Optional[list] = None,
|
||||||
metadata: Optional[dict] = None,
|
metadata: Optional[dict] = None,
|
||||||
|
body_is_html: bool = False,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""통합 에이전트 메시지 API. 모든 에이전트가 이걸 씀."""
|
"""통합 에이전트 메시지 API. 모든 에이전트가 이걸 씀.
|
||||||
text = format_agent_message(agent_id, kind, title, body, metadata)
|
|
||||||
|
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
|
reply_markup = None
|
||||||
if actions:
|
if actions:
|
||||||
buttons = []
|
buttons = []
|
||||||
|
|||||||
@@ -167,9 +167,20 @@ async def summarize_latest_news(req: NewsSummarizeRequest = NewsSummarizeRequest
|
|||||||
logger.exception("뉴스 요약 중 예상치 못한 오류")
|
logger.exception("뉴스 요약 중 예상치 못한 오류")
|
||||||
raise HTTPException(status_code=500, detail=f"뉴스 요약 실패: {e}")
|
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 {
|
return {
|
||||||
**result,
|
**result,
|
||||||
"article_count": len(articles),
|
"article_count": len(articles),
|
||||||
|
"articles": top_articles,
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Trading API (Windows Proxy, 인증 필요) ---
|
# --- Trading API (Windows Proxy, 인증 필요) ---
|
||||||
|
|||||||
Reference in New Issue
Block a user