Compare commits
4 Commits
f3c7ce72de
...
7acc1979c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 7acc1979c8 | |||
| 3152bc23f4 | |||
| b23346143f | |||
| b867b8ce13 |
@@ -427,7 +427,7 @@ docker compose up -d
|
|||||||
- `TELEGRAM_WEBHOOK_URL`: 텔레그램 Webhook URL
|
- `TELEGRAM_WEBHOOK_URL`: 텔레그램 Webhook URL
|
||||||
|
|
||||||
**스케줄러 job**
|
**스케줄러 job**
|
||||||
- 08:00 매일 — 주식 뉴스 요약 (`stock_news_job`)
|
- 07:30 매일 — 주식 뉴스 요약 (`stock_news_job`)
|
||||||
- 60초 간격 — 유휴 에이전트 휴식 체크 (`idle_check_job`)
|
- 60초 간격 — 유휴 에이전트 휴식 체크 (`idle_check_job`)
|
||||||
|
|
||||||
**agent-office API 목록**
|
**agent-office API 목록**
|
||||||
|
|||||||
@@ -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"],
|
||||||
@@ -37,6 +74,16 @@ class StockAgent(BaseAgent):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 아내 chat 추가 전송 (설정된 경우) — 제목 + 본문만 간결하게
|
||||||
|
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{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"
|
||||||
|
add_log(self.agent_id, f"Wife telegram send failed: {desc}", "warning", task_id)
|
||||||
|
|
||||||
update_task_status(task_id, "succeeded", {
|
update_task_status(task_id, "succeeded", {
|
||||||
"summary": result["summary"],
|
"summary": result["summary"],
|
||||||
"article_count": result.get("article_count", 0),
|
"article_count": result.get("article_count", 0),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ REALESTATE_LAB_URL = os.getenv("REALESTATE_LAB_URL", "http://localhost:18800")
|
|||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
TELEGRAM_WEBHOOK_URL = os.getenv("TELEGRAM_WEBHOOK_URL", "")
|
TELEGRAM_WEBHOOK_URL = os.getenv("TELEGRAM_WEBHOOK_URL", "")
|
||||||
|
TELEGRAM_WIFE_CHAT_ID = os.getenv("TELEGRAM_WIFE_CHAT_ID", "")
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DB_PATH = os.getenv("AGENT_OFFICE_DB_PATH", "/app/data/agent_office.db")
|
DB_PATH = os.getenv("AGENT_OFFICE_DB_PATH", "/app/data/agent_office.db")
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async def _run_blog_schedule():
|
|||||||
await agent.on_schedule()
|
await agent.on_schedule()
|
||||||
|
|
||||||
def init_scheduler():
|
def init_scheduler():
|
||||||
scheduler.add_job(_run_stock_schedule, "cron", hour=8, minute=0, id="stock_news")
|
scheduler.add_job(_run_stock_schedule, "cron", hour=7, minute=30, id="stock_news")
|
||||||
scheduler.add_job(_run_realestate_schedule, "cron", hour=9, minute=15, id="realestate_report")
|
scheduler.add_job(_run_realestate_schedule, "cron", hour=9, minute=15, id="realestate_report")
|
||||||
scheduler.add_job(_run_blog_schedule, "cron", hour=10, minute=0, id="blog_pipeline")
|
scheduler.add_job(_run_blog_schedule, "cron", hour=10, minute=0, id="blog_pipeline")
|
||||||
scheduler.add_job(_check_idle_breaks, "interval", seconds=60, id="idle_check")
|
scheduler.add_job(_check_idle_breaks, "interval", seconds=60, id="idle_check")
|
||||||
|
|||||||
@@ -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…(생략)"
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from .client import _enabled, api_call
|
|||||||
from .formatter import MessageKind, format_agent_message
|
from .formatter import MessageKind, format_agent_message
|
||||||
|
|
||||||
|
|
||||||
async def send_raw(text: str, reply_markup: Optional[dict] = None) -> dict:
|
async def send_raw(text: str, reply_markup: Optional[dict] = None, chat_id: Optional[str] = None) -> dict:
|
||||||
"""가장 저수준. 원문 텍스트 그대로 전송."""
|
"""가장 저수준. 원문 텍스트 그대로 전송. chat_id 생략 시 기본 TELEGRAM_CHAT_ID로."""
|
||||||
if not _enabled():
|
if not _enabled():
|
||||||
return {"ok": False, "message_id": None}
|
return {"ok": False, "message_id": None}
|
||||||
payload = {
|
payload = {
|
||||||
"chat_id": TELEGRAM_CHAT_ID,
|
"chat_id": chat_id or TELEGRAM_CHAT_ID,
|
||||||
"text": text,
|
"text": text,
|
||||||
"parse_mode": "HTML",
|
"parse_mode": "HTML",
|
||||||
}
|
}
|
||||||
@@ -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