diff --git a/agent-office/app/agents/base.py b/agent-office/app/agents/base.py index ef9653d..bd07365 100644 --- a/agent-office/app/agents/base.py +++ b/agent-office/app/agents/base.py @@ -40,6 +40,14 @@ class BaseAgent: if self._ws_manager: await self._ws_manager.send_agent_state(self.agent_id, new_state, detail, task_id) + if new_state == "working" and old != "working": + await self._ws_manager.send_notification( + self.agent_id, "task_assigned", task_id, detail or "새 작업 시작" + ) + elif new_state == "idle" and old in ("working", "reporting"): + await self._ws_manager.send_notification( + self.agent_id, "task_completed", task_id, detail or "작업 완료" + ) if new_state == "break": await self._ws_manager.send_agent_move(self.agent_id, "break_room") elif old == "break" and new_state == "idle": diff --git a/agent-office/app/agents/stock.py b/agent-office/app/agents/stock.py index effabb0..eef5caa 100644 --- a/agent-office/app/agents/stock.py +++ b/agent-office/app/agents/stock.py @@ -22,15 +22,24 @@ class StockAgent(BaseAgent): summary = self._format_news_summary(news, indices) - update_task_status(task_id, "succeeded", { - "summary": summary, - "news_count": len(news) if isinstance(news, list) else 0, - }) - await self.transition("reporting", "뉴스 요약 전송 중...") from ..telegram_bot import send_stock_summary - await send_stock_summary(summary) + tg_result = await send_stock_summary(summary) + + update_task_status(task_id, "succeeded", { + "summary": summary, + "news_count": len(news) if isinstance(news, list) else 0, + "telegram_sent": tg_result.get("ok", False), + "telegram_message_id": tg_result.get("message_id"), + }) + + if not tg_result.get("ok"): + add_log(self.agent_id, "Telegram send failed", "warning", task_id) + if self._ws_manager: + await self._ws_manager.send_notification( + self.agent_id, "telegram_failed", task_id, "텔레그램 전송 실패" + ) await self.transition("idle", "뉴스 요약 완료") @@ -40,6 +49,15 @@ class StockAgent(BaseAgent): await self.transition("idle", f"오류: {e}") async def on_command(self, command: str, params: dict) -> dict: + if command == "test_telegram": + from ..telegram_bot import send_message + result = await send_message("🔔 [주식 에이전트] 텔레그램 테스트 메시지입니다.") + return { + "ok": result.get("ok", False), + "message": "텔레그램 전송 성공" if result.get("ok") else "텔레그램 전송 실패", + "telegram_message_id": result.get("message_id"), + } + if command == "fetch_news": await self.on_schedule() return {"ok": True, "message": "뉴스 수집 시작"} diff --git a/agent-office/app/db.py b/agent-office/app/db.py index cc5a7e1..850a83f 100644 --- a/agent-office/app/db.py +++ b/agent-office/app/db.py @@ -259,3 +259,64 @@ def mark_telegram_responded(callback_id: str, action: str) -> None: "UPDATE telegram_state SET responded=1, action=? WHERE callback_id=?", (action, callback_id), ) + + +def get_activity_feed(limit: int = 50, offset: int = 0) -> dict: + with _conn() as conn: + total_row = conn.execute(""" + SELECT (SELECT COUNT(*) FROM agent_tasks) + (SELECT COUNT(*) FROM agent_logs) AS total + """).fetchone() + total = total_row["total"] if total_row else 0 + + rows = conn.execute(""" + SELECT 'task' AS type, agent_id, id AS task_id, task_type, + status, NULL AS level, + COALESCE( + json_extract(result_data, '$.summary'), + task_type + ) AS message, + created_at, completed_at, + result_data + FROM agent_tasks + UNION ALL + SELECT 'log' AS type, agent_id, task_id, NULL AS task_type, + NULL AS status, level, + message, + created_at, NULL AS completed_at, + NULL AS result_data + FROM agent_logs + ORDER BY created_at DESC + LIMIT ? OFFSET ? + """, (limit, offset)).fetchall() + + items = [] + for r in rows: + item = { + "type": r["type"], + "agent_id": r["agent_id"], + "task_id": r["task_id"], + "message": r["message"], + "created_at": r["created_at"], + } + if r["type"] == "task": + item["task_type"] = r["task_type"] + item["status"] = r["status"] + item["completed_at"] = r["completed_at"] + if r["created_at"] and r["completed_at"]: + try: + from datetime import datetime + start = datetime.fromisoformat(r["created_at"].replace("Z", "+00:00")) + end = datetime.fromisoformat(r["completed_at"].replace("Z", "+00:00")) + item["duration_seconds"] = round((end - start).total_seconds()) + except Exception: + item["duration_seconds"] = None + else: + item["duration_seconds"] = None + result_data = json.loads(r["result_data"]) if r["result_data"] else None + if result_data and "telegram_sent" in result_data: + item["telegram_sent"] = result_data["telegram_sent"] + else: + item["level"] = r["level"] + items.append(item) + + return {"items": items, "total": total} diff --git a/agent-office/app/main.py b/agent-office/app/main.py index 1928a71..c404828 100644 --- a/agent-office/app/main.py +++ b/agent-office/app/main.py @@ -4,7 +4,7 @@ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from .config import CORS_ALLOW_ORIGINS -from .db import init_db, get_all_agents, get_agent_config, update_agent_config, get_agent_tasks, get_pending_approvals, get_task, get_logs +from .db import init_db, get_all_agents, get_agent_config, update_agent_config, get_agent_tasks, get_pending_approvals, get_task, get_logs, get_activity_feed from .models import CommandRequest, ApprovalRequest, AgentConfigUpdate from .websocket_manager import ws_manager from .agents import init_agents, get_agent, get_all_agent_states, AGENT_REGISTRY @@ -150,3 +150,7 @@ async def telegram_webhook(data: dict): @app.get("/api/agent-office/states") def all_states(): return {"agents": get_all_agent_states()} + +@app.get("/api/agent-office/activity") +def activity_feed(limit: int = 50, offset: int = 0): + return get_activity_feed(limit, offset) diff --git a/agent-office/app/telegram_bot.py b/agent-office/app/telegram_bot.py index fb34670..edacf9c 100644 --- a/agent-office/app/telegram_bot.py +++ b/agent-office/app/telegram_bot.py @@ -26,7 +26,11 @@ async def send_message(text: str, reply_markup: dict = None) -> dict: } if reply_markup: payload["reply_markup"] = reply_markup - return await _api("sendMessage", payload) + result = await _api("sendMessage", payload) + return { + "ok": result.get("ok", False), + "message_id": result.get("result", {}).get("message_id") if result.get("ok") else None, + } async def send_stock_summary(summary: str) -> dict: return await send_message(summary) diff --git a/agent-office/app/websocket_manager.py b/agent-office/app/websocket_manager.py index b8d9103..a6b6ba7 100644 --- a/agent-office/app/websocket_manager.py +++ b/agent-office/app/websocket_manager.py @@ -43,4 +43,13 @@ class WebSocketManager: async def send_agent_move(self, agent_id: str, target: str) -> None: await self.broadcast({"type": "agent_move", "agent": agent_id, "target": target}) + async def send_notification(self, agent_id: str, event: str, task_id: str = None, message: str = "") -> None: + await self.broadcast({ + "type": "notification", + "agent": agent_id, + "event": event, + "task_id": task_id, + "message": message, + }) + ws_manager = WebSocketManager()