feat(agent-office): YouTubeResearchAgent + 스케줄러 + /youtube/research API

- db.py: youtube_research_jobs 테이블 추가 + CRUD 3종 (add/update/get_latest)
- agents/youtube.py: YouTubeResearchAgent 신규 구현 (on_schedule/on_command/on_approval/_run_research/send_weekly_report)
- agents/__init__.py: YouTubeResearchAgent 등록
- scheduler.py: youtube_research(매일 09:00) + youtube_weekly_report(월 08:00) cron 추가
- main.py: POST /api/agent-office/youtube/research + GET /api/agent-office/youtube/research/status 엔드포인트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 12:13:12 +09:00
parent 8604c6292d
commit 1d4354e402
5 changed files with 180 additions and 1 deletions

View File

@@ -86,6 +86,17 @@ def init_db() -> None:
CREATE INDEX IF NOT EXISTS idx_conv_chat
ON conversation_messages(chat_id, created_at DESC)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS youtube_research_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT NOT NULL DEFAULT 'running',
countries TEXT NOT NULL DEFAULT '[]',
trends_collected INTEGER NOT NULL DEFAULT 0,
error TEXT,
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
completed_at TEXT
)
""")
# Seed default agent configs
for agent_id, name in [
("stock", "주식 트레이더"),
@@ -93,6 +104,7 @@ def init_db() -> None:
("blog", "블로그 마케터"),
("realestate", "청약 애널리스트"),
("lotto", "로또 큐레이터"),
("youtube", "YouTube 리서치"),
]:
conn.execute(
"INSERT OR IGNORE INTO agent_config(agent_id, display_name) VALUES(?,?)",
@@ -501,3 +513,45 @@ def get_activity_feed(limit: int = 50, offset: int = 0) -> dict:
items.append(item)
return {"items": items, "total": total}
# ── youtube_research_jobs CRUD ────────────────────────────────────────────────
def add_youtube_research_job(countries: list) -> int:
with _conn() as conn:
conn.execute(
"INSERT INTO youtube_research_jobs (countries) VALUES (?)",
(json.dumps(countries),),
)
return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
def update_youtube_research_job(
job_id: int, status: str, trends_collected: int, error: Optional[str] = None
) -> None:
with _conn() as conn:
conn.execute(
"""UPDATE youtube_research_jobs
SET status=?, trends_collected=?, error=?,
completed_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id=?""",
(status, trends_collected, error, job_id),
)
def get_latest_youtube_research_job() -> Optional[Dict[str, Any]]:
with _conn() as conn:
row = conn.execute(
"SELECT * FROM youtube_research_jobs ORDER BY started_at DESC LIMIT 1"
).fetchone()
if not row:
return None
return {
"id": row["id"],
"status": row["status"],
"countries": json.loads(row["countries"]),
"trends_collected": row["trends_collected"],
"error": row["error"],
"started_at": row["started_at"],
"completed_at": row["completed_at"],
}